From ea052b9e079dc95daa799dc87c7394c0473649d8 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Thu, 16 Jan 2025 15:43:04 -0500 Subject: [PATCH] [CL-428] create drawer component (#12812) * remove private/protected/lifecycle fields from Storybook docs table * move theme override decorator into util method * implement base drawer component * update bit-layout to be drawer container * create drawer helper components * expose new APIs to DS barrel file * write docs * update docs; add role input * use host directive instead of service * clean up logic a tad * add start slot to story * update docs * Apply suggestions from code review Co-authored-by: Victoria League * update docs * Update libs/components/src/drawer/drawer.mdx Co-authored-by: Victoria League * update docs / stories * add non text element to drawer --------- Co-authored-by: Victoria League --- angular.json | 8 ++ .../src/drawer/drawer-body.component.ts | 36 +++++ .../src/drawer/drawer-close.directive.ts | 29 ++++ .../src/drawer/drawer-header.component.html | 15 +++ .../src/drawer/drawer-header.component.ts | 34 +++++ .../src/drawer/drawer-host.directive.ts | 28 ++++ .../src/drawer/drawer.component.html | 8 ++ .../components/src/drawer/drawer.component.ts | 76 +++++++++++ libs/components/src/drawer/drawer.mdx | 120 +++++++++++++++++ libs/components/src/drawer/drawer.module.ts | 12 ++ libs/components/src/drawer/drawer.stories.ts | 124 ++++++++++++++++++ libs/components/src/drawer/index.ts | 5 + libs/components/src/index.ts | 1 + .../src/layout/layout.component.html | 1 + .../components/src/layout/layout.component.ts | 10 +- libs/components/src/layout/layout.stories.ts | 10 +- libs/components/src/layout/mocks.ts | 7 + .../src/navigation/nav-group.stories.ts | 2 +- .../src/navigation/nav-item.stories.ts | 2 +- .../components/kitchen-sink-main.component.ts | 97 +++++++++++--- .../kitchen-sink-shared.module.ts | 3 + .../kitchen-sink/kitchen-sink.stories.ts | 45 +++---- .../storybook-decorators.ts} | 16 ++- 23 files changed, 634 insertions(+), 55 deletions(-) create mode 100644 libs/components/src/drawer/drawer-body.component.ts create mode 100644 libs/components/src/drawer/drawer-close.directive.ts create mode 100644 libs/components/src/drawer/drawer-header.component.html create mode 100644 libs/components/src/drawer/drawer-header.component.ts create mode 100644 libs/components/src/drawer/drawer-host.directive.ts create mode 100644 libs/components/src/drawer/drawer.component.html create mode 100644 libs/components/src/drawer/drawer.component.ts create mode 100644 libs/components/src/drawer/drawer.mdx create mode 100644 libs/components/src/drawer/drawer.module.ts create mode 100644 libs/components/src/drawer/drawer.stories.ts create mode 100644 libs/components/src/drawer/index.ts create mode 100644 libs/components/src/layout/mocks.ts rename libs/components/src/{utils/position-fixed-wrapper-decorator.ts => stories/storybook-decorators.ts} (50%) diff --git a/angular.json b/angular.json index 7053050262..665d810cf4 100644 --- a/angular.json +++ b/angular.json @@ -147,6 +147,10 @@ "./tsconfig.json", "-e", "json", + "--disableInternal", + "--disableLifeCycleHooks", + "--disablePrivate", + "--disableProtected", "-d", ".", "--disableRoutesGraph" @@ -165,6 +169,10 @@ "./tsconfig.json", "-e", "json", + "--disableInternal", + "--disableLifeCycleHooks", + "--disablePrivate", + "--disableProtected", "-d", ".", "--disableRoutesGraph" diff --git a/libs/components/src/drawer/drawer-body.component.ts b/libs/components/src/drawer/drawer-body.component.ts new file mode 100644 index 0000000000..9bd2adcffb --- /dev/null +++ b/libs/components/src/drawer/drawer-body.component.ts @@ -0,0 +1,36 @@ +import { CdkScrollable } from "@angular/cdk/scrolling"; +import { ChangeDetectionStrategy, Component, Signal, inject } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { map } from "rxjs"; + +/** + * Body container for `bit-drawer` + */ +@Component({ + selector: "bit-drawer-body", + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [], + host: { + class: + "tw-p-4 tw-pt-0 tw-block tw-overflow-auto tw-border-solid tw-border tw-border-transparent tw-transition-colors tw-duration-200", + "[class.tw-border-t-secondary-300]": "isScrolled()", + }, + hostDirectives: [ + { + directive: CdkScrollable, + }, + ], + template: ` `, +}) +export class DrawerBodyComponent { + private scrollable = inject(CdkScrollable); + + /** TODO: share this utility with browser popup header? */ + protected isScrolled: Signal = toSignal( + this.scrollable + .elementScrolled() + .pipe(map(() => this.scrollable.measureScrollOffset("top") > 0)), + { initialValue: false }, + ); +} diff --git a/libs/components/src/drawer/drawer-close.directive.ts b/libs/components/src/drawer/drawer-close.directive.ts new file mode 100644 index 0000000000..bf56dd8b71 --- /dev/null +++ b/libs/components/src/drawer/drawer-close.directive.ts @@ -0,0 +1,29 @@ +import { Directive, inject } from "@angular/core"; + +import { DrawerComponent } from "./drawer.component"; + +/** + * Closes the ancestor drawer + * + * @example + * + * ```html + * + * + * + * ``` + **/ +@Directive({ + selector: "button[bitDrawerClose]", + standalone: true, + host: { + "(click)": "onClick()", + }, +}) +export class DrawerCloseDirective { + private drawer = inject(DrawerComponent, { optional: true }); + + protected onClick() { + this.drawer?.open.set(false); + } +} diff --git a/libs/components/src/drawer/drawer-header.component.html b/libs/components/src/drawer/drawer-header.component.html new file mode 100644 index 0000000000..4652e5537e --- /dev/null +++ b/libs/components/src/drawer/drawer-header.component.html @@ -0,0 +1,15 @@ +
+
+ +

+ {{ title() }} +

+
+ +
diff --git a/libs/components/src/drawer/drawer-header.component.ts b/libs/components/src/drawer/drawer-header.component.ts new file mode 100644 index 0000000000..73834b8487 --- /dev/null +++ b/libs/components/src/drawer/drawer-header.component.ts @@ -0,0 +1,34 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, HostBinding, input } from "@angular/core"; + +import { IconButtonModule } from "../icon-button"; +import { I18nPipe } from "../shared/i18n.pipe"; +import { TypographyModule } from "../typography"; + +import { DrawerCloseDirective } from "./drawer-close.directive"; + +/** + * Header container for `bit-drawer` + **/ +@Component({ + selector: "bit-drawer-header", + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, DrawerCloseDirective, TypographyModule, IconButtonModule, I18nPipe], + templateUrl: "drawer-header.component.html", + host: { + class: "tw-block tw-pl-4 tw-pr-2 tw-py-2", + }, +}) +export class DrawerHeaderComponent { + /** + * The title to display + */ + title = input.required(); + + /** We don't want to set the HTML title attribute with `this.title` */ + @HostBinding("attr.title") + protected get getTitle(): null { + return null; + } +} diff --git a/libs/components/src/drawer/drawer-host.directive.ts b/libs/components/src/drawer/drawer-host.directive.ts new file mode 100644 index 0000000000..f5e3e56b09 --- /dev/null +++ b/libs/components/src/drawer/drawer-host.directive.ts @@ -0,0 +1,28 @@ +import { Portal } from "@angular/cdk/portal"; +import { Directive, signal } from "@angular/core"; + +/** + * Host that renders a drawer + * + * @internal + */ +@Directive({ + selector: "[bitDrawerHost]", + standalone: true, +}) +export class DrawerHostDirective { + private _portal = signal | undefined>(undefined); + + /** The portal to display */ + portal = this._portal.asReadonly(); + + open(portal: Portal) { + this._portal.set(portal); + } + + close(portal: Portal) { + if (portal === this.portal()) { + this._portal.set(undefined); + } + } +} diff --git a/libs/components/src/drawer/drawer.component.html b/libs/components/src/drawer/drawer.component.html new file mode 100644 index 0000000000..fce6b3c57e --- /dev/null +++ b/libs/components/src/drawer/drawer.component.html @@ -0,0 +1,8 @@ + +
+ +
+
diff --git a/libs/components/src/drawer/drawer.component.ts b/libs/components/src/drawer/drawer.component.ts new file mode 100644 index 0000000000..ccabb6f0b6 --- /dev/null +++ b/libs/components/src/drawer/drawer.component.ts @@ -0,0 +1,76 @@ +import { CdkPortal, PortalModule } from "@angular/cdk/portal"; +import { CommonModule } from "@angular/common"; +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + input, + model, + viewChild, +} from "@angular/core"; + +import { DrawerHostDirective } from "./drawer-host.directive"; + +/** + * A drawer is a panel of supplementary content that is adjacent to the page's main content. + * + * Drawers render in `bit-layout`. Drawers must be a descendant of `bit-layout`, but they do not need to be a direct descendant. + */ +@Component({ + selector: "bit-drawer", + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, PortalModule], + templateUrl: "drawer.component.html", +}) +export class DrawerComponent { + private drawerHost = inject(DrawerHostDirective); + private portal = viewChild.required(CdkPortal); + + /** + * Whether or not the drawer is open. + * + * Note: Does not support implicit boolean transform due to Angular limitation. Must be bound explicitly `[open]="true"` instead of just `open`. + * https://github.com/angular/angular/issues/55166#issuecomment-2032150999 + **/ + open = model(false); + + /** + * The ARIA role of the drawer. + * + * - [complementary](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/complementary_role) + * - For drawers that contain content that is complementary to the page's main content. (default) + * - [navigation](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/navigation_role) + * - For drawers that primary contain links to other content. + */ + role = input<"complementary" | "navigation">("complementary"); + + constructor() { + effect( + () => { + this.open() ? this.drawerHost.open(this.portal()) : this.drawerHost.close(this.portal()); + }, + { + allowSignalWrites: true, + }, + ); + + // Set `open` to `false` when another drawer is opened. + effect( + () => { + if (this.drawerHost.portal() !== this.portal()) { + this.open.set(false); + } + }, + { + allowSignalWrites: true, + }, + ); + } + + /** Toggle the drawer between open & closed */ + toggle() { + this.open.update((prev) => !prev); + } +} diff --git a/libs/components/src/drawer/drawer.mdx b/libs/components/src/drawer/drawer.mdx new file mode 100644 index 0000000000..0098ce64ea --- /dev/null +++ b/libs/components/src/drawer/drawer.mdx @@ -0,0 +1,120 @@ +import { Meta, Story, Primary, Controls } from "@storybook/addon-docs"; + +import * as stories from "./drawer.stories"; + +import { DrawerOpen as KitchenSink } from "../stories/kitchen-sink/kitchen-sink.stories"; + + + +```ts +import { DrawerComponent } from "@bitwarden/components"; +``` + +# Drawer + +A drawer is a panel of supplementary content that is adjacent to the page's main content. + + + + + +## Usage + +A `bit-drawer` in a template will not render inline, but rather will render adjacent to the main +page content. + +```html + + + +

Lorem ipsum dolor...

+
+
+``` + +`bit-drawer` must be a descendant of `bit-layout`, but it does not need to be a direct descendant. + +## Header and body + +Header and body content can be provided with the `bit-drawer-header` and `bit-drawer-body` +components, respectively. + +A title can be passed to the header by input: +`` + +Custom content can be rendered before the title with the header's `start` slot: + +```html + + + +``` + +## Opening and closing + +`bit-drawer` opens when its `open` input is `true`: + +```html +... +``` + +Note: Model inputs do not support implicit boolean transformation (see Angular reasoning +[here](https://github.com/angular/angular/issues/55166#issuecomment-2032150999)). `open` must be +bound explicitly `` instead of just ``. + +Buttons can be made to open/toggle drawers by referencing a template variable, or by manipulating +state that is bound to `open`: + +```html + ... +``` + +For convenience, close buttons can be created _inside_ the drawer with the `bitDrawerClose` +directive: + +```html + + + +``` + +## Multiple Drawers + +Only one drawer can be open at a time, and they do not stack. If a drawer is already open, opening +another will close and replace the one already open. + + + +## Headless + +Omitting `bit-drawer-header` and `bit-drawer-body` allows for fully customizable content. + + + +## Accessibility + +- The drawer should contain an h2 element. If you are using `bit-drawer-header`, this is created for + you via the `title` input: + +```html + +

Hello world!

+
+ + + + + + +``` + +- The ARIA role of the drawer can be set with the `role` attribute: + - [complementary](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/complementary_role) + (default) + - For drawers that contain content that is complementary to the page's main content. + - [navigation](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/navigation_role) + - For drawers that primary contain links to other content. + +## Kitchen Sink + + diff --git a/libs/components/src/drawer/drawer.module.ts b/libs/components/src/drawer/drawer.module.ts new file mode 100644 index 0000000000..9f51ba06b4 --- /dev/null +++ b/libs/components/src/drawer/drawer.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from "@angular/core"; + +import { DrawerBodyComponent } from "./drawer-body.component"; +import { DrawerCloseDirective } from "./drawer-close.directive"; +import { DrawerHeaderComponent } from "./drawer-header.component"; +import { DrawerComponent } from "./drawer.component"; + +@NgModule({ + imports: [DrawerComponent, DrawerHeaderComponent, DrawerBodyComponent, DrawerCloseDirective], + exports: [DrawerComponent, DrawerHeaderComponent, DrawerBodyComponent, DrawerCloseDirective], +}) +export class DrawerModule {} diff --git a/libs/components/src/drawer/drawer.stories.ts b/libs/components/src/drawer/drawer.stories.ts new file mode 100644 index 0000000000..54b4c89f4c --- /dev/null +++ b/libs/components/src/drawer/drawer.stories.ts @@ -0,0 +1,124 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { RouterTestingModule } from "@angular/router/testing"; +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { ButtonModule } from "../button"; +import { CalloutModule } from "../callout"; +import { LayoutComponent } from "../layout"; +import { mockLayoutI18n } from "../layout/mocks"; +import { + disableBothThemeDecorator, + positionFixedWrapperDecorator, +} from "../stories/storybook-decorators"; +import { TypographyModule } from "../typography"; +import { I18nMockService } from "../utils"; + +import { DrawerBodyComponent } from "./drawer-body.component"; +import { DrawerHeaderComponent } from "./drawer-header.component"; +import { DrawerComponent } from "./drawer.component"; +import { DrawerModule } from "./drawer.module"; + +export default { + title: "Component Library/Drawer", + component: DrawerComponent, + subcomponents: { + DrawerHeaderComponent, + DrawerBodyComponent, + }, + decorators: [ + positionFixedWrapperDecorator(), + disableBothThemeDecorator, + moduleMetadata({ + imports: [ + RouterTestingModule, + LayoutComponent, + DrawerModule, + ButtonModule, + CalloutModule, + TypographyModule, + ], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + ...mockLayoutI18n, + close: "Close", + }); + }, + }, + ], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + +

The drawer is {{ open ? "open" : "closed" }}.

+ + + + + + + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +

+ +
+ + `, + }), + args: { + open: true, + }, +}; + +export const Headless: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + +

The drawer is {{ open ? "open" : "closed" }}.

+ + +

+ Hello world! +
+ + `, + }), + args: { + open: true, + }, +}; + +export const MultipleDrawers: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + + + Foo + + + + Bar + + + `, + }), +}; diff --git a/libs/components/src/drawer/index.ts b/libs/components/src/drawer/index.ts new file mode 100644 index 0000000000..abf5b8d34f --- /dev/null +++ b/libs/components/src/drawer/index.ts @@ -0,0 +1,5 @@ +export * from "./drawer.module"; +export * from "./drawer.component"; +export * from "./drawer-body.component"; +export * from "./drawer-close.directive"; +export * from "./drawer-header.component"; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index a48750a99f..ed84452044 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -14,6 +14,7 @@ export * from "./color-password"; export * from "./container"; export * from "./dialog"; export * from "./disclosure"; +export * from "./drawer"; export * from "./form-field"; export * from "./icon-button"; export * from "./icon"; diff --git a/libs/components/src/layout/layout.component.html b/libs/components/src/layout/layout.component.html index ccbb40c2b5..7c1c5b2501 100644 --- a/libs/components/src/layout/layout.component.html +++ b/libs/components/src/layout/layout.component.html @@ -37,4 +37,5 @@ > + diff --git a/libs/components/src/layout/layout.component.ts b/libs/components/src/layout/layout.component.ts index d55ad8493e..7bf8a6ad17 100644 --- a/libs/components/src/layout/layout.component.ts +++ b/libs/components/src/layout/layout.component.ts @@ -1,7 +1,9 @@ +import { PortalModule } from "@angular/cdk/portal"; import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { RouterModule } from "@angular/router"; +import { DrawerHostDirective } from "../drawer/drawer-host.directive"; import { LinkModule } from "../link"; import { SideNavService } from "../navigation/side-nav.service"; import { SharedModule } from "../shared"; @@ -10,12 +12,14 @@ import { SharedModule } from "../shared"; selector: "bit-layout", templateUrl: "layout.component.html", standalone: true, - imports: [CommonModule, SharedModule, LinkModule, RouterModule], + imports: [CommonModule, SharedModule, LinkModule, RouterModule, PortalModule], + hostDirectives: [DrawerHostDirective], }) export class LayoutComponent { protected mainContentId = "main-content"; - constructor(protected sideNavService: SideNavService) {} + protected sideNavService = inject(SideNavService); + protected drawerPortal = inject(DrawerHostDirective).portal; focusMainContent() { document.getElementById(this.mainContentId)?.focus(); diff --git a/libs/components/src/layout/layout.stories.ts b/libs/components/src/layout/layout.stories.ts index a0eadebe7f..7fdad65554 100644 --- a/libs/components/src/layout/layout.stories.ts +++ b/libs/components/src/layout/layout.stories.ts @@ -6,10 +6,11 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { CalloutModule } from "../callout"; import { NavigationModule } from "../navigation"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; import { I18nMockService } from "../utils/i18n-mock.service"; -import { positionFixedWrapperDecorator } from "../utils/position-fixed-wrapper-decorator"; import { LayoutComponent } from "./layout.component"; +import { mockLayoutI18n } from "./mocks"; export default { title: "Component Library/Layout", @@ -22,12 +23,7 @@ export default { { provide: I18nService, useFactory: () => { - return new I18nMockService({ - toggleSideNavigation: "Toggle side navigation", - skipToContent: "Skip to content", - submenu: "submenu", - toggleCollapse: "toggle collapse", - }); + return new I18nMockService(mockLayoutI18n); }, }, ], diff --git a/libs/components/src/layout/mocks.ts b/libs/components/src/layout/mocks.ts new file mode 100644 index 0000000000..50c2bd9afb --- /dev/null +++ b/libs/components/src/layout/mocks.ts @@ -0,0 +1,7 @@ +/** TODO: create mock messages.json file for all of CL in favor of sharing per-Story mocks */ +export const mockLayoutI18n = { + toggleSideNavigation: "Toggle side navigation", + skipToContent: "Skip to content", + submenu: "submenu", + toggleCollapse: "toggle collapse", +}; diff --git a/libs/components/src/navigation/nav-group.stories.ts b/libs/components/src/navigation/nav-group.stories.ts index a6fa53ff18..f412dbc20b 100644 --- a/libs/components/src/navigation/nav-group.stories.ts +++ b/libs/components/src/navigation/nav-group.stories.ts @@ -6,8 +6,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LayoutComponent } from "../layout"; import { SharedModule } from "../shared/shared.module"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; import { I18nMockService } from "../utils/i18n-mock.service"; -import { positionFixedWrapperDecorator } from "../utils/position-fixed-wrapper-decorator"; import { NavGroupComponent } from "./nav-group.component"; import { NavigationModule } from "./navigation.module"; diff --git a/libs/components/src/navigation/nav-item.stories.ts b/libs/components/src/navigation/nav-item.stories.ts index 20d6ebd1d7..376f121eb0 100644 --- a/libs/components/src/navigation/nav-item.stories.ts +++ b/libs/components/src/navigation/nav-item.stories.ts @@ -5,8 +5,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { IconButtonModule } from "../icon-button"; import { LayoutComponent } from "../layout"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; import { I18nMockService } from "../utils/i18n-mock.service"; -import { positionFixedWrapperDecorator } from "../utils/position-fixed-wrapper-decorator"; import { NavItemComponent } from "./nav-item.component"; import { NavigationModule } from "./navigation.module"; diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts index 687b891738..13f0a16a4d 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts @@ -1,5 +1,5 @@ import { DialogRef } from "@angular/cdk/dialog"; -import { Component } from "@angular/core"; +import { Component, signal } from "@angular/core"; import { DialogService } from "../../../dialog"; import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; @@ -28,13 +28,7 @@ class KitchenSinkDialog { @Component({ standalone: true, selector: "bit-tab-main", - imports: [ - KitchenSinkSharedModule, - KitchenSinkTable, - KitchenSinkToggleList, - KitchenSinkForm, - KitchenSinkDialog, - ], + imports: [KitchenSinkSharedModule, KitchenSinkTable, KitchenSinkToggleList, KitchenSinkForm], template: ` Kitchen Sink test zone @@ -48,6 +42,11 @@ class KitchenSinkDialog {

+
+

Bitwarden Kitchen Sink

+ Learn more +
+

The purpose of this story is to compose together all of our components. When snapshot tests @@ -63,18 +62,14 @@ class KitchenSinkDialog {

-
-

Bitwarden

- Learn more -
-

About

- + +

Companies using Bitwarden

@@ -99,15 +94,87 @@ class KitchenSinkDialog {
+ + + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+ + What did foo say to bar? + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+
+
`, }) export class KitchenSinkMainComponent { constructor(public dialogService: DialogService) {} - openDefaultDialog() { + protected drawerOpen = signal(false); + + openDialog() { this.dialogService.open(KitchenSinkDialog); } + openDrawer() { + this.drawerOpen.set(true); + } + navItems = [ { icon: "bwi-collection", name: "Password Managers", route: "/" }, { icon: "bwi-collection", name: "Favorites", route: "/" }, diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts index 56e3a92e2a..c4fe2f9b2a 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts @@ -13,6 +13,7 @@ import { CalloutModule } from "../../callout"; import { CheckboxModule } from "../../checkbox"; import { ColorPasswordModule } from "../../color-password"; import { DialogModule } from "../../dialog"; +import { DrawerModule } from "../../drawer"; import { FormControlModule } from "../../form-control"; import { FormFieldModule } from "../../form-field"; import { IconModule } from "../../icon"; @@ -48,6 +49,7 @@ import { TypographyModule } from "../../typography"; ColorPasswordModule, CommonModule, DialogModule, + DrawerModule, FormControlModule, FormFieldModule, FormsModule, @@ -85,6 +87,7 @@ import { TypographyModule } from "../../typography"; ColorPasswordModule, CommonModule, DialogModule, + DrawerModule, FormControlModule, FormFieldModule, FormsModule, diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts index a90597c171..62b9398438 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -1,13 +1,7 @@ import { importProvidersFrom } from "@angular/core"; import { provideNoopAnimations } from "@angular/platform-browser/animations"; import { RouterModule } from "@angular/router"; -import { - Meta, - StoryObj, - applicationConfig, - componentWrapperDecorator, - moduleMetadata, -} from "@storybook/angular"; +import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; import { userEvent, getAllByRole, @@ -23,6 +17,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { DialogService } from "../../dialog"; import { LayoutComponent } from "../../layout"; import { I18nMockService } from "../../utils/i18n-mock.service"; +import { disableBothThemeDecorator, positionFixedWrapperDecorator } from "../storybook-decorators"; import { DialogVirtualScrollBlockComponent } from "./components/dialog-virtual-scroll-block.component"; import { KitchenSinkForm } from "./components/kitchen-sink-form.component"; @@ -35,25 +30,8 @@ export default { title: "Documentation / Kitchen Sink", component: LayoutComponent, decorators: [ - componentWrapperDecorator( - /** - * Applying a CSS transform makes a `position: fixed` element act like it is `position: relative` - * https://github.com/storybookjs/storybook/issues/8011#issue-490251969 - */ - (story) => { - return /* HTML */ `
- ${story} -
`; - }, - ({ globals }) => { - /** - * avoid a bug with the way that we render the same component twice in the same iframe and how - * that interacts with the router-outlet - */ - const themeOverride = globals["theme"] === "both" ? "light" : globals["theme"]; - return { theme: themeOverride }; - }, - ), + positionFixedWrapperDecorator(), + disableBothThemeDecorator, moduleMetadata({ imports: [ KitchenSinkSharedModule, @@ -135,7 +113,7 @@ export const MenuOpen: Story = { }, }; -export const DefaultDialogOpen: Story = { +export const DialogOpen: Story = { ...Default, play: async (context) => { const canvas = context.canvasElement; @@ -148,6 +126,19 @@ export const DefaultDialogOpen: Story = { }, }; +export const DrawerOpen: Story = { + ...Default, + play: async (context) => { + const canvas = context.canvasElement; + const drawerButton = getByRole(canvas, "button", { + name: "Open Drawer", + }); + + // workaround for userEvent not firing in FF https://github.com/testing-library/user-event/issues/1075 + await fireEvent.click(drawerButton); + }, +}; + export const PopoverOpen: Story = { ...Default, play: async (context) => { diff --git a/libs/components/src/utils/position-fixed-wrapper-decorator.ts b/libs/components/src/stories/storybook-decorators.ts similarity index 50% rename from libs/components/src/utils/position-fixed-wrapper-decorator.ts rename to libs/components/src/stories/storybook-decorators.ts index a3298e6ad0..d59f2dd1f3 100644 --- a/libs/components/src/utils/position-fixed-wrapper-decorator.ts +++ b/libs/components/src/stories/storybook-decorators.ts @@ -11,7 +11,21 @@ export const positionFixedWrapperDecorator = (wrapper?: (story: string) => strin * https://github.com/storybookjs/storybook/issues/8011#issue-490251969 */ (story) => - /* HTML */ `
+ /* HTML */ `
${wrapper ? wrapper(story) : story}
`, ); + +export const disableBothThemeDecorator = componentWrapperDecorator( + (story) => story, + ({ globals }) => { + /** + * avoid a bug with the way that we render the same component twice in the same iframe and how + * that interacts with the router-outlet + */ + const themeOverride = globals["theme"] === "both" ? "light" : globals["theme"]; + return { theme: themeOverride }; + }, +);