1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-23 02:31:26 +01:00

[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 <vleague@bitwarden.com>

* update docs

* Update libs/components/src/drawer/drawer.mdx

Co-authored-by: Victoria League <vleague@bitwarden.com>

* update docs / stories

* add non text element to drawer

---------

Co-authored-by: Victoria League <vleague@bitwarden.com>
This commit is contained in:
Will Martin 2025-01-16 15:43:04 -05:00 committed by GitHub
parent 3917f50fdd
commit ea052b9e07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 634 additions and 55 deletions

View File

@ -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"

View File

@ -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: ` <ng-content></ng-content> `,
})
export class DrawerBodyComponent {
private scrollable = inject(CdkScrollable);
/** TODO: share this utility with browser popup header? */
protected isScrolled: Signal<boolean> = toSignal(
this.scrollable
.elementScrolled()
.pipe(map(() => this.scrollable.measureScrollOffset("top") > 0)),
{ initialValue: false },
);
}

View File

@ -0,0 +1,29 @@
import { Directive, inject } from "@angular/core";
import { DrawerComponent } from "./drawer.component";
/**
* Closes the ancestor drawer
*
* @example
*
* ```html
* <bit-drawer>
* <button type="button" bitButton bitDrawerClose>Close</button>
* </bit-drawer>
* ```
**/
@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);
}
}

View File

@ -0,0 +1,15 @@
<header class="tw-flex tw-justify-between tw-items-center">
<div class="tw-flex tw-items-center tw-gap-1 tw-overflow-auto">
<ng-content select="[slot=start]"></ng-content>
<h2 bitTypography="h3" noMargin class="tw-text-main tw-mb-0 tw-truncate" [attr.title]="title()">
{{ title() }}
</h2>
</div>
<button
bitIconButton="bwi-close"
type="button"
bitDrawerClose
[attr.title]="'close' | i18n"
[attr.aria-label]="'close' | i18n"
></button>
</header>

View File

@ -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<string>();
/** We don't want to set the HTML title attribute with `this.title` */
@HostBinding("attr.title")
protected get getTitle(): null {
return null;
}
}

View File

@ -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<Portal<unknown> | undefined>(undefined);
/** The portal to display */
portal = this._portal.asReadonly();
open(portal: Portal<unknown>) {
this._portal.set(portal);
}
close(portal: Portal<unknown>) {
if (portal === this.portal()) {
this._portal.set(undefined);
}
}
}

View File

@ -0,0 +1,8 @@
<ng-container *cdkPortal>
<section
[attr.role]="role()"
class="tw-w-[23rem] tw-sticky tw-top-0 tw-h-screen tw-flex tw-flex-col tw-overflow-auto tw-border-0 tw-border-l tw-border-solid tw-border-secondary-300 tw-bg-background"
>
<ng-content></ng-content>
</section>
</ng-container>

View File

@ -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<boolean>(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);
}
}

View File

@ -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";
<Meta of={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.
<Primary />
<Controls />
## Usage
A `bit-drawer` in a template will not render inline, but rather will render adjacent to the main
page content.
```html
<bit-drawer [open]="true">
<bit-drawer-header title="Hello Bitwaaaaaaaaaaaaaaaaaaaaaaaaarden!"></bit-drawer-header>
<bit-drawer-body>
<p>Lorem ipsum dolor...</p>
</bit-drawer-body>
</bit-drawer>
```
`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:
`<bit-drawer-header title="Foobar"></bit-drawer-header>`
Custom content can be rendered before the title with the header's `start` slot:
```html
<bit-drawer-header title="Foobar">
<i slot="start" class="bwi bwi-key" aria-hidden="true"></i>
</bit-drawer-header>
```
## Opening and closing
`bit-drawer` opens when its `open` input is `true`:
```html
<bit-drawer [open]="true">...</bit-drawer>
```
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 `<bit-drawer [open]="true">` instead of just `<bit-drawer open>`.
Buttons can be made to open/toggle drawers by referencing a template variable, or by manipulating
state that is bound to `open`:
```html
<button (click)="myDrawer.toggle()"></button> <bit-drawer #myDrawer>...</bit-drawer>
```
For convenience, close buttons can be created _inside_ the drawer with the `bitDrawerClose`
directive:
```html
<bit-drawer>
<button type="button" bitDrawerClose>Close</button>
</bit-drawer>
```
## 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.
<Story of={stories.MultipleDrawers} />
## Headless
Omitting `bit-drawer-header` and `bit-drawer-body` allows for fully customizable content.
<Story of={stories.Headless} />
## 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
<bit-drawer>
<h2 bitTypography="h2">Hello world!</h2>
</bit-drawer>
<!-- or -->
<bit-drawer>
<bit-drawer-header title="Hello world!"></bit-drawer-header>
</bit-drawer>
```
- 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
<Story of={KitchenSink} autoplay />

View File

@ -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 {}

View File

@ -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<DrawerComponent>;
type Story = StoryObj<DrawerComponent>;
export const Default: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-layout class="tw-text-main">
<p>The drawer is {{ open ? "open" : "closed" }}.<p>
<button type="button" bitButton (click)="drawer.toggle()">Toggle</button>
<!-- Note: bit-drawer does *not* need to be a direct descendant of bit-layout. -->
<bit-drawer [(open)]="open" #drawer>
<bit-drawer-header title="Hello Bitwaaaaaaaaaaaaaaaaaaaaaaaaarden!">
<i slot="start" class="bwi bwi-key" aria-hidden="true"></i>
</bit-drawer-header>
<bit-drawer-body>
<p bitTypography="body1">
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.
</p>
</bit-drawer-body>
</bit-drawer>
</bit-layout>
`,
}),
args: {
open: true,
},
};
export const Headless: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-layout class="tw-text-main">
<p>The drawer is {{ open ? "open" : "closed" }}.<p>
<button type="button" bitButton (click)="drawer.toggle()">Toggle</button>
<bit-drawer [(open)]="open" #drawer>
<h2 bitTypography="h2"></h2>
Hello world!
</bit-drawer>
</bit-layout>
`,
}),
args: {
open: true,
},
};
export const MultipleDrawers: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-layout class="tw-text-main">
<button type="button" bitButton (click)="foo.toggle()">{{ !foo.open() ? "Open" : "Close" }} Foo</button>
<button type="button" bitButton (click)="bar.toggle()">{{ !bar.open() ? "Open" : "Close" }} Bar</button>
<bit-drawer #foo>
Foo
</bit-drawer>
<bit-drawer #bar [open]="true">
Bar
</bit-drawer>
</bit-layout>
`,
}),
};

View File

@ -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";

View File

@ -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";

View File

@ -37,4 +37,5 @@
></div>
</div>
</main>
<ng-template [cdkPortalOutlet]="drawerPortal()"></ng-template>
</div>

View File

@ -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();

View File

@ -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);
},
},
],

View File

@ -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",
};

View File

@ -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";

View File

@ -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";

View File

@ -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: `
<bit-banner bannerType="info" class="-tw-m-6 tw-flex tw-flex-col tw-pb-6">
Kitchen Sink test zone
@ -48,6 +42,11 @@ class KitchenSinkDialog {
</bit-breadcrumbs>
</p>
<div class="tw-mb-6 tw-mt-6">
<h1 bitTypography="h1">Bitwarden Kitchen Sink<bit-avatar text="Bit Warden"></bit-avatar></h1>
<a bitLink linkType="primary" href="#">Learn more</a>
</div>
<bit-callout type="info" title="About the Kitchen Sink">
<p bitTypography="body1">
The purpose of this story is to compose together all of our components. When snapshot tests
@ -63,18 +62,14 @@ class KitchenSinkDialog {
</p>
</bit-callout>
<div class="tw-mb-6 tw-mt-6">
<h1 bitTypography="h1">Bitwarden <bit-avatar text="Bit Warden"></bit-avatar></h1>
<a bitLink linkType="primary" href="#">Learn more</a>
</div>
<bit-tab-group label="Main content tabs" class="tw-text-main">
<bit-tab label="Evaluation">
<bit-section>
<h2 bitTypography="h2" class="tw-mb-6">About</h2>
<bit-kitchen-sink-table></bit-kitchen-sink-table>
<button bitButton (click)="openDefaultDialog()">Open Dialog</button>
<button bitButton (click)="openDialog()">Open Dialog</button>
<button bitButton (click)="openDrawer()">Open Drawer</button>
</bit-section>
<bit-section>
<h2 bitTypography="h2" class="tw-mb-6">Companies using Bitwarden</h2>
@ -99,15 +94,87 @@ class KitchenSinkDialog {
</bit-section>
</bit-tab>
</bit-tab-group>
<bit-drawer [(open)]="drawerOpen">
<bit-drawer-header title="Foo ipsum"></bit-drawer-header>
<bit-drawer-body>
<p bitTypography="body1">
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.
</p>
<bit-form-field>
<bit-label>What did foo say to bar?</bit-label>
<input bitInput value="Baz" />
</bit-form-field>
<p bitTypography="body1">
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.
</p>
<p bitTypography="body1">
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.
</p>
<p bitTypography="body1">
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.
</p>
<p bitTypography="body1">
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.
</p>
<p bitTypography="body1">
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.
</p>
<p bitTypography="body1">
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.
</p>
</bit-drawer-body>
</bit-drawer>
`,
})
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: "/" },

View File

@ -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,

View File

@ -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 */ `<div class="tw-scale-100 tw-border-2 tw-border-solid tw-border-[red]">
${story}
</div>`;
},
({ 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) => {

View File

@ -11,7 +11,21 @@ export const positionFixedWrapperDecorator = (wrapper?: (story: string) => strin
* https://github.com/storybookjs/storybook/issues/8011#issue-490251969
*/
(story) =>
/* HTML */ `<div class="tw-scale-100 tw-h-screen tw-border-2 tw-border-solid tw-border-[red]">
/* HTML */ `<div
class="tw-scale-100 tw-h-screen tw-border-2 tw-border-solid tw-border-secondary-300 tw-overflow-auto"
>
${wrapper ? wrapper(story) : story}
</div>`,
);
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 };
},
);