1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 00:03:56 +00:00

Merge branch 'master' into feature/org-admin-refresh

This commit is contained in:
Shane Melton
2022-09-28 11:48:53 -07:00
152 changed files with 1501 additions and 980 deletions

View File

@@ -0,0 +1,8 @@
<span class="tw-relative">
<span [ngClass]="{ 'tw-invisible': loading }">
<ng-content></ng-content>
</span>
<span class="tw-absolute tw-inset-0" [ngClass]="{ 'tw-invisible': !loading }">
<i class="bwi bwi-spinner bwi-lg bwi-spin tw-align-baseline" aria-hidden="true"></i>
</span>
</span>

View File

@@ -8,6 +8,7 @@ describe("Button", () => {
let fixture: ComponentFixture<TestApp>;
let testAppComponent: TestApp;
let buttonDebugElement: DebugElement;
let disabledButtonDebugElement: DebugElement;
let linkDebugElement: DebugElement;
beforeEach(waitForAsync(() => {
@@ -20,6 +21,7 @@ describe("Button", () => {
fixture = TestBed.createComponent(TestApp);
testAppComponent = fixture.debugElement.componentInstance;
buttonDebugElement = fixture.debugElement.query(By.css("button"));
disabledButtonDebugElement = fixture.debugElement.query(By.css("button#disabled"));
linkDebugElement = fixture.debugElement.query(By.css("a"));
}));
@@ -60,16 +62,67 @@ describe("Button", () => {
expect(buttonDebugElement.nativeElement.classList.contains("tw-block")).toBe(false);
expect(linkDebugElement.nativeElement.classList.contains("tw-block")).toBe(false);
});
it("should not be disabled when loading and disabled are false", () => {
testAppComponent.loading = false;
testAppComponent.disabled = false;
fixture.detectChanges();
expect(buttonDebugElement.attributes["loading"]).toBeFalsy();
expect(linkDebugElement.attributes["loading"]).toBeFalsy();
expect(buttonDebugElement.nativeElement.disabled).toBeFalsy();
});
it("should be disabled when disabled is true", () => {
testAppComponent.disabled = true;
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.disabled).toBeTruthy();
// Anchor tags cannot be disabled.
});
it("should be disabled when attribute disabled is true", () => {
expect(disabledButtonDebugElement.nativeElement.disabled).toBeTruthy();
});
it("should be disabled when loading is true", () => {
testAppComponent.loading = true;
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.disabled).toBeTruthy();
});
});
@Component({
selector: "test-app",
template: `
<button type="button" bitButton [buttonType]="buttonType" [block]="block">Button</button>
<a href="#" bitButton [buttonType]="buttonType" [block]="block"> Link </a>
<button
type="button"
bitButton
[buttonType]="buttonType"
[block]="block"
[disabled]="disabled"
[loading]="loading"
>
Button
</button>
<a
href="#"
bitButton
[buttonType]="buttonType"
[block]="block"
[disabled]="disabled"
[loading]="loading"
>
Link
</a>
<button id="disabled" type="button" bitButton disabled>Button</button>
`,
})
class TestApp {
buttonType: string;
block: boolean;
disabled: boolean;
loading: boolean;
}

View File

@@ -1,4 +1,4 @@
import { Input, HostBinding, Directive } from "@angular/core";
import { Input, HostBinding, Component } from "@angular/core";
export type ButtonTypes = "primary" | "secondary" | "danger";
@@ -38,10 +38,11 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
],
};
@Directive({
@Component({
selector: "button[bitButton], a[bitButton]",
templateUrl: "button.component.html",
})
export class ButtonDirective {
export class ButtonComponent {
@HostBinding("class") get classList() {
return [
"tw-font-semibold",
@@ -65,6 +66,14 @@ export class ButtonDirective {
.concat(buttonStyles[this.buttonType ?? "secondary"]);
}
@HostBinding("attr.disabled")
get disabledAttr() {
const disabled = this.disabled != null && this.disabled !== false;
return disabled || this.loading ? true : null;
}
@Input() buttonType: ButtonTypes = null;
@Input() block?: boolean;
@Input() loading = false;
@Input() disabled = false;
}

View File

@@ -1,11 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { ButtonDirective } from "./button.directive";
import { ButtonComponent } from "./button.component";
@NgModule({
imports: [CommonModule],
exports: [ButtonDirective],
declarations: [ButtonDirective],
exports: [ButtonComponent],
declarations: [ButtonComponent],
})
export class ButtonModule {}

View File

@@ -1,12 +1,14 @@
import { Meta, Story } from "@storybook/angular";
import { ButtonDirective } from "./button.directive";
import { ButtonComponent } from "./button.component";
export default {
title: "Component Library/Button",
component: ButtonDirective,
component: ButtonComponent,
args: {
buttonType: "primary",
disabled: false,
loading: false,
},
parameters: {
design: {
@@ -16,11 +18,11 @@ export default {
},
} as Meta;
const Template: Story<ButtonDirective> = (args: ButtonDirective) => ({
const Template: Story<ButtonComponent> = (args: ButtonComponent) => ({
props: args,
template: `
<button bitButton [buttonType]="buttonType" [block]="block">Button</button>
<a bitButton [buttonType]="buttonType" [block]="block" href="#" class="tw-ml-2">Link</a>
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block">Button</button>
<a bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" href="#" class="tw-ml-2">Link</a>
`,
});
@@ -39,21 +41,50 @@ Danger.args = {
buttonType: "danger",
};
const DisabledTemplate: Story = (args) => ({
const AllStylesTemplate: Story = (args) => ({
props: args,
template: `
<button bitButton disabled buttonType="primary" class="tw-mr-2">Primary</button>
<button bitButton disabled buttonType="secondary" class="tw-mr-2">Secondary</button>
<button bitButton disabled buttonType="danger" class="tw-mr-2">Danger</button>
<button bitButton [disabled]="disabled" [loading]="loading" [block]="block" buttonType="primary" class="tw-mr-2">Primary</button>
<button bitButton [disabled]="disabled" [loading]="loading" [block]="block" buttonType="secondary" class="tw-mr-2">Secondary</button>
<button bitButton [disabled]="disabled" [loading]="loading" [block]="block" buttonType="danger" class="tw-mr-2">Danger</button>
`,
});
export const Disabled = DisabledTemplate.bind({});
Disabled.args = {
size: "small",
export const Loading = AllStylesTemplate.bind({});
Loading.args = {
disabled: false,
loading: true,
};
const BlockTemplate: Story<ButtonDirective> = (args: ButtonDirective) => ({
export const Disabled = AllStylesTemplate.bind({});
Disabled.args = {
disabled: true,
loading: false,
};
const DisabledWithAttributeTemplate: Story = (args) => ({
props: args,
template: `
<ng-container *ngIf="disabled">
<button bitButton disabled [loading]="loading" [block]="block" buttonType="primary" class="tw-mr-2">Primary</button>
<button bitButton disabled [loading]="loading" [block]="block" buttonType="secondary" class="tw-mr-2">Secondary</button>
<button bitButton disabled [loading]="loading" [block]="block" buttonType="danger" class="tw-mr-2">Danger</button>
</ng-container>
<ng-container *ngIf="!disabled">
<button bitButton [loading]="loading" [block]="block" buttonType="primary" class="tw-mr-2">Primary</button>
<button bitButton [loading]="loading" [block]="block" buttonType="secondary" class="tw-mr-2">Secondary</button>
<button bitButton [loading]="loading" [block]="block" buttonType="danger" class="tw-mr-2">Danger</button>
</ng-container>
`,
});
export const DisabledWithAttribute = DisabledWithAttributeTemplate.bind({});
DisabledWithAttribute.args = {
disabled: true,
loading: false,
};
const BlockTemplate: Story<ButtonComponent> = (args: ButtonComponent) => ({
props: args,
template: `
<span class="tw-flex">

View File

@@ -1,2 +1,2 @@
export * from "./button.directive";
export * from "./button.component";
export * from "./button.module";

View File

@@ -19,7 +19,7 @@
></button>
</div>
<div class="tw-overflow-y-auto tw-p-4 tw-pb-8">
<div class="tw-overflow-y-auto tw-pb-8" [ngClass]="{ 'tw-p-4': !disablePadding }">
<ng-content select="[bitDialogContent]"></ng-content>
</div>

View File

@@ -1,3 +1,4 @@
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { Component, Input } from "@angular/core";
@Component({
@@ -7,6 +8,14 @@ import { Component, Input } from "@angular/core";
export class DialogComponent {
@Input() dialogSize: "small" | "default" | "large" = "default";
private _disablePadding: boolean;
@Input() set disablePadding(value: boolean) {
this._disablePadding = coerceBooleanProperty(value);
}
get disablePadding() {
return this._disablePadding;
}
get width() {
switch (this.dialogSize) {
case "small": {

View File

@@ -5,6 +5,7 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { ButtonModule } from "../../button";
import { IconButtonModule } from "../../icon-button";
import { SharedModule } from "../../shared";
import { TabsModule } from "../../tabs";
import { I18nMockService } from "../../utils/i18n-mock.service";
import { DialogCloseDirective } from "../directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
@@ -16,7 +17,7 @@ export default {
component: DialogComponent,
decorators: [
moduleMetadata({
imports: [ButtonModule, SharedModule, IconButtonModule],
imports: [ButtonModule, SharedModule, IconButtonModule, TabsModule],
declarations: [DialogTitleContainerDirective, DialogCloseDirective],
providers: [
{
@@ -33,6 +34,13 @@ export default {
args: {
dialogSize: "small",
},
argTypes: {
_disablePadding: {
table: {
disable: true,
},
},
},
parameters: {
design: {
type: "figma",
@@ -44,7 +52,7 @@ export default {
const Template: Story<DialogComponent> = (args: DialogComponent) => ({
props: args,
template: `
<bit-dialog [dialogSize]="dialogSize">
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
<span bitDialogTitle>{{title}}</span>
<span bitDialogContent>Dialog body text goes here.</span>
<div bitDialogFooter class="tw-flex tw-items-center tw-flex-row tw-gap-2">
@@ -83,7 +91,7 @@ Large.args = {
const TemplateScrolling: Story<DialogComponent> = (args: DialogComponent) => ({
props: args,
template: `
<bit-dialog [dialogSize]="dialogSize">
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
<span bitDialogTitle>Scrolling Example</span>
<span bitDialogContent>
Dialog body text goes here.<br>
@@ -104,3 +112,37 @@ export const ScrollingContent = TemplateScrolling.bind({});
ScrollingContent.args = {
dialogSize: "small",
};
const TemplateTabbed: Story<DialogComponent> = (args: DialogComponent) => ({
props: args,
template: `
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
<span bitDialogTitle>Tab Content Example</span>
<span bitDialogContent>
<bit-tab-group>
<bit-tab label="First Tab">First Tab Content</bit-tab>
<bit-tab label="Second Tab">Second Tab Content</bit-tab>
<bit-tab label="Third Tab">Third Tab Content</bit-tab>
</bit-tab-group>
</span>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary">Save</button>
<button bitButton buttonType="secondary">Cancel</button>
</div>
</bit-dialog>
`,
});
export const TabContent = TemplateTabbed.bind({});
TabContent.args = {
dialogSize: "large",
disablePadding: true,
};
TabContent.story = {
parameters: {
docs: {
storyDescription: `An example of using the \`bitTabGroup\` component within the Dialog. The content padding should be
disabled (via \`disablePadding\`) so that the tabs are flush against the dialog title.`,
},
},
};

View File

@@ -72,7 +72,7 @@ const sizes: Record<IconButtonSize, string[]> = {
@Component({
selector: "button[bitIconButton]",
template: `<i class="bwi" [ngClass]="icon" aria-hidden="true"></i>`,
template: `<i class="bwi" [ngClass]="iconClass" aria-hidden="true"></i>`,
})
export class BitIconButtonComponent {
@Input("bitIconButton") icon: string;
@@ -106,10 +106,15 @@ export class BitIconButtonComponent {
"before:tw-rounded-md",
"before:tw-transition",
"before:tw-ring",
"before:tw-ring-transparent",
"focus-visible:before:tw-ring-text-contrast",
"focus-visible:tw-z-10",
]
.concat(styles[this.buttonType])
.concat(sizes[this.size]);
}
get iconClass() {
return [this.icon, "!tw-m-0"];
}
}

View File

@@ -7,7 +7,6 @@ export * from "./icon";
export * from "./icon-button";
export * from "./menu";
export * from "./dialog";
export * from "./submit-button";
export * from "./link";
export * from "./tabs";
export * from "./toggle-group";

View File

@@ -1 +0,0 @@
export * from "./submit-button.module";

View File

@@ -1,16 +0,0 @@
<button
bitButton
type="submit"
[block]="block"
[buttonType]="buttonType"
[disabled]="loading || disabled"
>
<span class="tw-relative">
<span [ngClass]="{ 'tw-invisible': loading }">
<ng-content></ng-content>
</span>
<span class="tw-absolute tw-inset-0" [ngClass]="{ 'tw-invisible': !loading }">
<i class="bwi bwi-spinner bwi-lg bwi-spin tw-align-baseline" aria-hidden="true"></i>
</span>
</span>
</button>

View File

@@ -1,19 +0,0 @@
import { Component, HostBinding, Input } from "@angular/core";
import { ButtonTypes } from "../button";
@Component({
selector: "bit-submit-button",
templateUrl: "./submit-button.component.html",
})
export class SubmitButtonComponent {
@Input() buttonType: ButtonTypes = "primary";
@Input() disabled = false;
@Input() loading: boolean;
@Input() block?: boolean;
@HostBinding("class") get classList() {
return this.block == null || this.block === false ? [] : ["tw-w-full", "tw-block"];
}
}

View File

@@ -1,13 +0,0 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { ButtonModule } from "../button";
import { SubmitButtonComponent } from "./submit-button.component";
@NgModule({
imports: [CommonModule, ButtonModule],
exports: [SubmitButtonComponent],
declarations: [SubmitButtonComponent],
})
export class SubmitButtonModule {}

View File

@@ -1,45 +0,0 @@
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { SubmitButtonComponent } from "./submit-button.component";
import { SubmitButtonModule } from "./submit-button.module";
export default {
title: "Component Library/Submit Button",
component: SubmitButtonComponent,
decorators: [
moduleMetadata({
imports: [SubmitButtonModule],
}),
],
args: {
buttonType: "primary",
loading: false,
block: false,
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16733",
},
},
} as Meta;
const Template: Story<SubmitButtonComponent> = (args: SubmitButtonComponent) => ({
props: args,
template: `<bit-submit-button [buttonType]="buttonType" [loading]="loading" [disabled]="disabled" [block]="block">
Submit
</bit-submit-button>`,
});
export const Primary = Template.bind({});
Primary.args = {};
export const Loading = Template.bind({});
Loading.args = {
loading: true,
};
export const Disabled = Template.bind({});
Disabled.args = {
disabled: true,
};