diff --git a/libs/components/src/banner/banner.component.html b/libs/components/src/banner/banner.component.html index 531682b640..8ce7da97ed 100644 --- a/libs/components/src/banner/banner.component.html +++ b/libs/components/src/banner/banner.component.html @@ -1,5 +1,5 @@
- +
diff --git a/libs/components/src/banner/banner.component.spec.ts b/libs/components/src/banner/banner.component.spec.ts index 2cb7fdc15b..0fe2391a5f 100644 --- a/libs/components/src/banner/banner.component.spec.ts +++ b/libs/components/src/banner/banner.component.spec.ts @@ -1,5 +1,10 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; + +import { SharedModule } from "../shared/shared.module"; +import { I18nMockService } from "../utils/i18n-mock.service"; + import { BannerComponent } from "./banner.component"; describe("BannerComponent", () => { @@ -8,7 +13,17 @@ describe("BannerComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ + imports: [SharedModule], declarations: [BannerComponent], + providers: [ + { + provide: I18nService, + useFactory: () => + new I18nMockService({ + close: "Close", + }), + }, + ], }).compileComponents(); fixture = TestBed.createComponent(BannerComponent); diff --git a/libs/components/src/banner/banner.module.ts b/libs/components/src/banner/banner.module.ts index df8c8f29b3..2c819fbc5b 100644 --- a/libs/components/src/banner/banner.module.ts +++ b/libs/components/src/banner/banner.module.ts @@ -1,10 +1,13 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; +import { IconButtonModule } from "../icon-button"; +import { SharedModule } from "../shared/shared.module"; + import { BannerComponent } from "./banner.component"; @NgModule({ - imports: [CommonModule], + imports: [CommonModule, SharedModule, IconButtonModule], exports: [BannerComponent], declarations: [BannerComponent], }) diff --git a/libs/components/src/banner/banner.stories.ts b/libs/components/src/banner/banner.stories.ts index 871b069a7a..693731e372 100644 --- a/libs/components/src/banner/banner.stories.ts +++ b/libs/components/src/banner/banner.stories.ts @@ -1,19 +1,43 @@ -import { Meta, Story } from "@storybook/angular"; +import { Meta, moduleMetadata, Story } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; + +import { IconButtonModule } from "../icon-button"; +import { SharedModule } from "../shared/shared.module"; +import { I18nMockService } from "../utils/i18n-mock.service"; import { BannerComponent } from "./banner.component"; export default { title: "Component Library/Banner", component: BannerComponent, + decorators: [ + moduleMetadata({ + imports: [SharedModule, IconButtonModule], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + close: "Close", + }); + }, + }, + ], + }), + ], args: { bannerType: "warning", }, + argTypes: { + onClose: { action: "onClose" }, + }, } as Meta; const Template: Story = (args: BannerComponent) => ({ props: args, template: ` - Content Really Long Text Lorem Ipsum Ipsum Ipsum + Content Really Long Text Lorem Ipsum Ipsum Ipsum `, }); diff --git a/libs/components/src/button/button.directive.ts b/libs/components/src/button/button.directive.ts index b36da8620e..59e7b4269f 100644 --- a/libs/components/src/button/button.directive.ts +++ b/libs/components/src/button/button.directive.ts @@ -9,8 +9,6 @@ const buttonStyles: Record = { "!tw-text-contrast", "hover:tw-bg-primary-700", "hover:tw-border-primary-700", - "focus:tw-bg-primary-700", - "focus:tw-border-primary-700", "disabled:tw-bg-primary-500/60", "disabled:tw-border-primary-500/60", "disabled:!tw-text-contrast/60", @@ -23,9 +21,6 @@ const buttonStyles: Record = { "hover:tw-bg-secondary-500", "hover:tw-border-secondary-500", "hover:!tw-text-contrast", - "focus:tw-bg-secondary-500", - "focus:tw-border-secondary-500", - "focus:!tw-text-contrast", "disabled:tw-bg-transparent", "disabled:tw-border-text-muted/60", "disabled:!tw-text-muted/60", @@ -37,9 +32,6 @@ const buttonStyles: Record = { "hover:tw-bg-danger-500", "hover:tw-border-danger-500", "hover:!tw-text-contrast", - "focus:tw-bg-danger-500", - "focus:tw-border-danger-500", - "focus:!tw-text-contrast", "disabled:tw-bg-transparent", "disabled:tw-border-danger-500/60", "disabled:!tw-text-danger/60", @@ -62,10 +54,10 @@ export class ButtonDirective { "tw-text-center", "hover:tw-no-underline", "focus:tw-outline-none", - "focus:tw-ring", - "focus:tw-ring-offset-2", - "focus:tw-ring-primary-700", - "focus:tw-z-10", + "focus-visible:tw-ring", + "focus-visible:tw-ring-offset-2", + "focus-visible:tw-ring-primary-700", + "focus-visible:tw-z-10", ] .concat(this.block ? ["tw-w-full", "tw-block"] : ["tw-inline-block"]) .concat(buttonStyles[this.buttonType ?? "secondary"]); diff --git a/libs/components/src/dialog/dialog.module.ts b/libs/components/src/dialog/dialog.module.ts index 535972417c..3492a38f10 100644 --- a/libs/components/src/dialog/dialog.module.ts +++ b/libs/components/src/dialog/dialog.module.ts @@ -1,6 +1,7 @@ import { DialogModule as CdkDialogModule } from "@angular/cdk/dialog"; import { NgModule } from "@angular/core"; +import { IconButtonModule } from "../icon-button"; import { SharedModule } from "../shared"; import { DialogService } from "./dialog.service"; @@ -10,11 +11,11 @@ import { DialogTitleContainerDirective } from "./directives/dialog-title-contain import { SimpleDialogComponent } from "./simple-dialog/simple-dialog.component"; @NgModule({ - imports: [SharedModule, CdkDialogModule], + imports: [SharedModule, IconButtonModule, CdkDialogModule], declarations: [ DialogCloseDirective, - DialogComponent, DialogTitleContainerDirective, + DialogComponent, SimpleDialogComponent, ], exports: [CdkDialogModule, DialogComponent, SimpleDialogComponent], diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index 1bc95d5208..40e1fe7505 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -3,19 +3,19 @@ class="tw-my-4 tw-flex tw-max-h-screen tw-flex-col tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-text-contrast tw-text-main" >

+ [attr.title]="'close' | i18n" + [attr.aria-label]="'close' | i18n" + >
diff --git a/libs/components/src/dialog/dialog/dialog.stories.ts b/libs/components/src/dialog/dialog/dialog.stories.ts index 3bf8457d12..8e2826111f 100644 --- a/libs/components/src/dialog/dialog/dialog.stories.ts +++ b/libs/components/src/dialog/dialog/dialog.stories.ts @@ -3,6 +3,7 @@ import { Meta, moduleMetadata, Story } from "@storybook/angular"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { ButtonModule } from "../../button"; +import { IconButtonModule } from "../../icon-button"; import { SharedModule } from "../../shared"; import { I18nMockService } from "../../utils/i18n-mock.service"; import { DialogCloseDirective } from "../directives/dialog-close.directive"; @@ -15,7 +16,7 @@ export default { component: DialogComponent, decorators: [ moduleMetadata({ - imports: [SharedModule, ButtonModule], + imports: [ButtonModule, SharedModule, IconButtonModule], declarations: [DialogTitleContainerDirective, DialogCloseDirective], providers: [ { @@ -46,9 +47,16 @@ const Template: Story = (args: DialogComponent) => ({ {{title}} Dialog body text goes here. -
+
+
`, diff --git a/libs/components/src/dialog/simple-dialog.service.stories.ts b/libs/components/src/dialog/simple-dialog.service.stories.ts index 90963515aa..ab973484c8 100644 --- a/libs/components/src/dialog/simple-dialog.service.stories.ts +++ b/libs/components/src/dialog/simple-dialog.service.stories.ts @@ -2,7 +2,12 @@ import { DialogModule, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; import { Component, Inject } from "@angular/core"; import { Meta, moduleMetadata, Story } from "@storybook/angular"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; + import { ButtonModule } from "../button"; +import { IconButtonModule } from "../icon-button"; +import { SharedModule } from "../shared/shared.module"; +import { I18nMockService } from "../utils/i18n-mock.service"; import { DialogService } from "./dialog.service"; import { DialogCloseDirective } from "./directives/dialog-close.directive"; @@ -60,13 +65,23 @@ export default { decorators: [ moduleMetadata({ declarations: [ - DialogCloseDirective, - SimpleDialogComponent, - DialogTitleContainerDirective, StoryDialogContentComponent, + DialogCloseDirective, + DialogTitleContainerDirective, + SimpleDialogComponent, + ], + imports: [SharedModule, IconButtonModule, ButtonModule, DialogModule], + providers: [ + DialogService, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + close: "Close", + }); + }, + }, ], - imports: [ButtonModule, DialogModule], - providers: [DialogService], }), ], parameters: { diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts new file mode 100644 index 0000000000..cd6f1be402 --- /dev/null +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -0,0 +1,115 @@ +import { Component, HostBinding, Input } from "@angular/core"; + +export type IconButtonStyle = "contrast" | "main" | "muted" | "primary" | "secondary" | "danger"; + +const styles: Record = { + contrast: [ + "tw-bg-transparent", + "!tw-text-contrast", + "tw-border-transparent", + "hover:tw-bg-transparent-hover", + "hover:tw-border-text-contrast", + "focus-visible:before:tw-ring-text-contrast", + "disabled:hover:tw-bg-transparent", + ], + main: [ + "tw-bg-transparent", + "!tw-text-main", + "tw-border-transparent", + "hover:tw-bg-transparent-hover", + "hover:tw-border-text-main", + "focus-visible:before:tw-ring-text-main", + "disabled:hover:tw-bg-transparent", + ], + muted: [ + "tw-bg-transparent", + "!tw-text-muted", + "tw-border-transparent", + "hover:tw-bg-transparent-hover", + "hover:tw-border-primary-700", + "focus-visible:before:tw-ring-primary-700", + "disabled:hover:tw-bg-transparent", + ], + primary: [ + "tw-bg-primary-500", + "!tw-text-contrast", + "tw-border-primary-500", + "hover:tw-bg-primary-700", + "hover:tw-border-primary-700", + "focus-visible:before:tw-ring-primary-700", + "disabled:hover:tw-bg-primary-500", + ], + secondary: [ + "tw-bg-transparent", + "!tw-text-muted", + "tw-border-text-muted", + "hover:!tw-text-contrast", + "hover:tw-bg-text-muted", + "focus-visible:before:tw-ring-primary-700", + "disabled:hover:tw-bg-transparent", + "disabled:hover:!tw-text-muted", + "disabled:hover:tw-border-text-muted", + ], + danger: [ + "tw-bg-transparent", + "!tw-text-danger", + "tw-border-danger-500", + "hover:!tw-text-contrast", + "hover:tw-bg-danger-500", + "focus-visible:before:tw-ring-primary-700", + "disabled:hover:tw-bg-transparent", + "disabled:hover:!tw-text-danger", + "disabled:hover:tw-border-danger-500", + ], +}; + +export type IconButtonSize = "default" | "small"; + +const sizes: Record = { + default: ["tw-px-2.5", "tw-py-1.5"], + small: ["tw-leading-none", "tw-text-base", "tw-p-1"], +}; + +@Component({ + selector: "button[bitIconButton]", + template: ``, +}) +export class BitIconButtonComponent { + @Input("bitIconButton") icon: string; + + @Input() buttonType: IconButtonStyle = "main"; + + @Input() size: IconButtonSize = "default"; + + @HostBinding("class") get classList() { + return [ + "tw-font-semibold", + "tw-border", + "tw-border-solid", + "tw-rounded", + "tw-transition", + "hover:tw-no-underline", + "disabled:tw-opacity-60", + "disabled:hover:tw-border-transparent", + "focus:tw-outline-none", + + // Workaround for box-shadow with transparent offset issue: + // https://github.com/tailwindlabs/tailwindcss/issues/3595 + // Remove `before:` and use regular `tw-ring` when browser no longer has bug, or better: + // switch to `outline` with `outline-offset` when Safari supports border radius on outline. + // Using `box-shadow` to create outlines is a hack and as such `outline` should be preferred. + "tw-relative", + "before:tw-content-['']", + "before:tw-block", + "before:tw-absolute", + "before:-tw-inset-[3px]", + "before:tw-rounded-md", + "before:tw-transition", + "before:tw-ring", + "focus-visible:before:tw-ring-text-contrast", + "focus-visible:tw-z-10", + ] + .concat(styles[this.buttonType]) + .concat(sizes[this.size]); + } +} diff --git a/libs/components/src/icon-button/icon-button.module.ts b/libs/components/src/icon-button/icon-button.module.ts new file mode 100644 index 0000000000..fb4e858971 --- /dev/null +++ b/libs/components/src/icon-button/icon-button.module.ts @@ -0,0 +1,11 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; + +import { BitIconButtonComponent } from "./icon-button.component"; + +@NgModule({ + imports: [CommonModule], + declarations: [BitIconButtonComponent], + exports: [BitIconButtonComponent], +}) +export class IconButtonModule {} diff --git a/libs/components/src/icon-button/icon-button.stories.ts b/libs/components/src/icon-button/icon-button.stories.ts new file mode 100644 index 0000000000..7be45d3f71 --- /dev/null +++ b/libs/components/src/icon-button/icon-button.stories.ts @@ -0,0 +1,59 @@ +import { Meta, Story } from "@storybook/angular"; + +import { BitIconButtonComponent } from "./icon-button.component"; + +export default { + title: "Component Library/Icon Button", + component: BitIconButtonComponent, + args: { + bitIconButton: "bwi-plus", + buttonType: "primary", + size: "default", + disabled: false, + }, +} as Meta; + +const Template: Story = (args: BitIconButtonComponent) => ({ + props: args, + template: ` +
+ +
+ `, +}); + +export const Contrast = Template.bind({}); +Contrast.args = { + buttonType: "contrast", +}; + +export const Main = Template.bind({}); +Main.args = { + buttonType: "main", +}; + +export const Muted = Template.bind({}); +Muted.args = { + buttonType: "muted", +}; + +export const Primary = Template.bind({}); +Primary.args = { + buttonType: "primary", +}; + +export const Secondary = Template.bind({}); +Secondary.args = { + buttonType: "secondary", +}; + +export const Danger = Template.bind({}); +Danger.args = { + buttonType: "danger", +}; diff --git a/libs/components/src/icon-button/index.ts b/libs/components/src/icon-button/index.ts new file mode 100644 index 0000000000..9da4a3162b --- /dev/null +++ b/libs/components/src/icon-button/index.ts @@ -0,0 +1 @@ +export * from "./icon-button.module"; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 2acd60765f..1f1ae6a8d2 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -4,6 +4,7 @@ export * from "./button"; export * from "./callout"; export * from "./form-field"; export * from "./icon"; +export * from "./icon-button"; export * from "./menu"; export * from "./dialog"; export * from "./submit-button"; diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 3801dd2e9e..f76c838585 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -1,4 +1,6 @@ :root { + --color-transparent-hover: rgb(0 0 0 / 0.03); + --color-background: 255 255 255; --color-background-alt: 251 251 251; --color-background-alt2: 23 92 219; @@ -37,6 +39,8 @@ } .theme_dark { + --color-transparent-hover: rgb(255 255 255 / 0.12); + --color-background: 31 36 46; --color-background-alt: 22 28 38; --color-background-alt2: 47 52 61; diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index 67eb96a6c3..082abe7372 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -12,7 +12,10 @@ module.exports = { corePlugins: { preflight: false }, theme: { colors: { - transparent: colors.transparent, + transparent: { + DEFAULT: colors.transparent, + hover: "var(--color-transparent-hover)", + }, current: colors.current, black: colors.black, primary: {