[CL-146] Add kitchen sink story (#8310)

This commit is contained in:
Victoria League 2024-03-22 10:17:00 -04:00 committed by GitHub
parent f70639d792
commit 0f375c3a0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 678 additions and 0 deletions

View File

@ -0,0 +1,176 @@
import { Component } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "../../../dialog";
import { I18nMockService } from "../../../utils/i18n-mock.service";
import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module";
@Component({
standalone: true,
selector: "bit-kitchen-sink-form",
imports: [KitchenSinkSharedModule],
providers: [
DialogService,
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
close: "Close",
checkboxRequired: "Option is required",
fieldsNeedAttention: "__$1__ field(s) above need your attention.",
inputEmail: "Input is not an email-address.",
inputMaxValue: (max) => `Input value must not exceed ${max}.`,
inputMinValue: (min) => `Input value must be at least ${min}.`,
inputRequired: "Input is required.",
multiSelectClearAll: "Clear all",
multiSelectLoading: "Retrieving options...",
multiSelectNotFound: "No items found",
multiSelectPlaceholder: "-- Type to Filter --",
required: "required",
selectPlaceholder: "-- Select --",
toggleVisibility: "Toggle visibility",
});
},
},
],
template: `
<form [formGroup]="formObj" [bitSubmit]="submit">
<div class="tw-mb-6">
<bit-progress [barWidth]="50"></bit-progress>
</div>
<bit-form-field>
<bit-label>Your favorite feature</bit-label>
<input bitInput formControlName="favFeature" />
</bit-form-field>
<bit-form-field>
<bit-label>Your favorite color</bit-label>
<bit-select formControlName="favColor">
<bit-option
*ngFor="let color of colors"
[value]="color.value"
[label]="color.name"
></bit-option>
</bit-select>
</bit-form-field>
<bit-form-field>
<bit-label>Your top 3 worst passwords</bit-label>
<bit-multi-select formControlName="topWorstPasswords" [baseItems]="worstPasswords">
</bit-multi-select>
</bit-form-field>
<bit-form-field>
<bit-label>How many passwords do you have?</bit-label>
<input bitInput type="number" formControlName="numPasswords" min="0" max="150" />
</bit-form-field>
<bit-form-field>
<bit-label>
A random password
<button
bitLink
linkType="primary"
[bitPopoverTriggerFor]="myPopover"
#triggerRef="popoverTrigger"
>
<i class="bwi bwi-question-circle"></i>
</button>
</bit-label>
<input bitInput type="password" formControlName="password" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
<div class="tw-mb-6">
<span bitTypography="body1" class="tw-text-main">
An example of a strong password: &nbsp;
</span>
<bit-color-password
class="tw-text-base"
[password]="'Wq$Jk😀7j DX#rS5Sdi!z'"
[showCount]="true"
></bit-color-password>
</div>
<bit-form-control>
<bit-label>Check if you love security</bit-label>
<input type="checkbox" bitCheckbox formControlName="loveSecurity" />
<bit-hint>Hint: the correct answer is yes!</bit-hint>
</bit-form-control>
<bit-radio-group formControlName="current">
<bit-label>Do you currently use Bitwarden?</bit-label>
<bit-radio-button value="yes">
<bit-label>Yes</bit-label>
</bit-radio-button>
<bit-radio-button value="no">
<bit-label>No</bit-label>
</bit-radio-button>
</bit-radio-group>
<button bitButton bitFormButton buttonType="primary" type="submit">Submit</button>
<bit-error-summary [formGroup]="formObj"></bit-error-summary>
<bit-popover [title]="'Password help'" #myPopover>
<div>A strong password has the following:</div>
<ul class="tw-mt-2 tw-mb-0 tw-pl-4">
<li>Letters</li>
<li>Numbers</li>
<li>Special characters</li>
</ul>
</bit-popover>
</form>
`,
})
export class KitchenSinkForm {
constructor(
public dialogService: DialogService,
public formBuilder: FormBuilder,
) {}
formObj = this.formBuilder.group({
favFeature: ["", [Validators.required]],
favColor: [undefined as string | undefined, [Validators.required]],
topWorstPasswords: [undefined as string | undefined],
loveSecurity: [false, [Validators.requiredTrue]],
current: ["yes"],
numPasswords: [null, [Validators.min(0), Validators.max(150)]],
password: ["", [Validators.required]],
});
submit = async () => {
await this.dialogService.openSimpleDialog({
title: "Confirm",
content: "Are you sure you want to submit?",
type: "primary",
acceptButtonText: "Yes",
cancelButtonText: "No",
acceptAction: async () => this.acceptDialog(),
});
};
acceptDialog() {
this.formObj.markAllAsTouched();
this.dialogService.closeAll();
}
colors = [
{ value: "blue", name: "Blue" },
{ value: "white", name: "White" },
{ value: "gray", name: "Gray" },
];
worstPasswords = [
{ id: "1", listName: "1234", labelName: "1234" },
{ id: "2", listName: "admin", labelName: "admin" },
{ id: "3", listName: "password", labelName: "password" },
{ id: "4", listName: "querty", labelName: "querty" },
{ id: "5", listName: "letmein", labelName: "letmein" },
{ id: "6", listName: "trustno1", labelName: "trustno1" },
{ id: "7", listName: "1qaz2wsx", labelName: "1qaz2wsx" },
];
}

View File

@ -0,0 +1,117 @@
import { DialogRef } from "@angular/cdk/dialog";
import { Component } from "@angular/core";
import { DialogService } from "../../../dialog";
import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module";
import { KitchenSinkForm } from "./kitchen-sink-form.component";
import { KitchenSinkTable } from "./kitchen-sink-table.component";
import { KitchenSinkToggleList } from "./kitchen-sink-toggle-list.component";
@Component({
standalone: true,
imports: [KitchenSinkSharedModule],
template: `
<bit-dialog title="Dialog Title" dialogSize="large">
<span bitDialogContent> Dialog body text goes here. </span>
<ng-container bitDialogFooter>
<button bitButton buttonType="primary" (click)="dialogRef.close()">OK</button>
<button bitButton buttonType="secondary" bitDialogClose>Cancel</button>
</ng-container>
</bit-dialog>
`,
})
class KitchenSinkDialog {
constructor(public dialogRef: DialogRef) {}
}
@Component({
standalone: true,
selector: "bit-tab-main",
imports: [
KitchenSinkSharedModule,
KitchenSinkTable,
KitchenSinkToggleList,
KitchenSinkForm,
KitchenSinkDialog,
],
template: `
<bit-banner bannerType="info" class="-tw-m-6 tw-flex tw-flex-col tw-pb-6">
Kitchen Sink test zone
</bit-banner>
<p class="tw-mt-4">
<bit-breadcrumbs>
<bit-breadcrumb *ngFor="let item of navItems" [icon]="item.icon" [route]="[item.route]">
{{ item.name }}
</bit-breadcrumb>
</bit-breadcrumbs>
</p>
<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
run, we'll be able to spot-check visual changes in a more app-like environment than just the
isolated stories. The stories for the Kitchen Sink exist to be tested by the Chromatic UI
tests.
</p>
<p bitTypography="body1">
NOTE: These stories will treat "Light & Dark" mode as "Light" mode. This is done to avoid a
bug with the way that we render the same component twice in the same iframe and how that
interacts with the <code>router-outlet</code>.
</p>
</bit-callout>
<div class="tw-mb-6 tw-mt-6">
<h1 bitTypography="h1" class="tw-text-main">
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-text-main tw-mb-6">About</h2>
<bit-kitchen-sink-table></bit-kitchen-sink-table>
<button bitButton (click)="openDefaultDialog()">Open Dialog</button>
</bit-section>
<bit-section>
<h2 bitTypography="h2" class="tw-text-main tw-mb-6">Companies using Bitwarden</h2>
<bit-kitchen-sink-toggle-list></bit-kitchen-sink-toggle-list>
</bit-section>
<bit-section>
<h2 bitTypography="h2" class="tw-text-main tw-mb-6">Survey</h2>
<bit-kitchen-sink-form></bit-kitchen-sink-form>
</bit-section>
</bit-tab>
<bit-tab label="Empty tab" data-testid="empty-tab">
<bit-section>
<bit-no-items class="tw-text-main">
<ng-container slot="title">This tab is empty</ng-container>
<ng-container slot="description">
<p bitTypography="body2">Try searching for what you are looking for:</p>
<bit-search></bit-search>
<p bitTypography="helper">Note that the search bar is not functional</p>
</ng-container>
</bit-no-items>
</bit-section>
</bit-tab>
</bit-tab-group>
`,
})
export class KitchenSinkMainComponent {
constructor(public dialogService: DialogService) {}
openDefaultDialog() {
this.dialogService.open(KitchenSinkDialog);
}
navItems = [
{ icon: "bwi-collection", name: "Password Managers", route: "/" },
{ icon: "bwi-collection", name: "Favorites", route: "/" },
];
}

View File

@ -0,0 +1,49 @@
import { Component } from "@angular/core";
import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module";
@Component({
standalone: true,
selector: "bit-kitchen-sink-table",
imports: [KitchenSinkSharedModule],
template: `
<bit-table>
<ng-container header>
<tr>
<th bitCell>Product</th>
<th bitCell>User</th>
<th bitCell>Options</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow>
<td bitCell>Password Manager</td>
<td bitCell>Everyone</td>
<td bitCell>
<button bitIconButton="bwi-ellipsis-v" [bitMenuTriggerFor]="menu1"></button>
<bit-menu #menu1>
<a href="#" bitMenuItem>Anchor link</a>
<a href="#" bitMenuItem>Another link</a>
<bit-menu-divider></bit-menu-divider>
<button type="button" bitMenuItem>Button after divider</button>
</bit-menu>
</td>
</tr>
<tr bitRow>
<td bitCell>Secrets Manager</td>
<td bitCell>Developers</td>
<td bitCell>
<button bitIconButton="bwi-ellipsis-v" [bitMenuTriggerFor]="menu2"></button>
<bit-menu #menu2>
<a href="#" bitMenuItem>Anchor link</a>
<a href="#" bitMenuItem>Another link</a>
<bit-menu-divider></bit-menu-divider>
<button type="button" bitMenuItem>Button after divider</button>
</bit-menu>
</td>
</tr>
</ng-template>
</bit-table>
`,
})
export class KitchenSinkTable {}

View File

@ -0,0 +1,34 @@
import { Component } from "@angular/core";
import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module";
@Component({
standalone: true,
selector: "bit-kitchen-sink-toggle-list",
imports: [KitchenSinkSharedModule],
template: `
<div class="tw-mt-6 tw-mb-6">
<bit-toggle-group [(selected)]="selectedToggle" aria-label="Company list filter">
<bit-toggle value="all"> All <span bitBadge variant="info">3</span> </bit-toggle>
<bit-toggle value="large"> Enterprise <span bitBadge variant="info">2</span> </bit-toggle>
<bit-toggle value="small"> Mid-sized <span bitBadge variant="info">1</span> </bit-toggle>
</bit-toggle-group>
</div>
<ul *ngFor="let company of companyList">
<li *ngIf="company.size === selectedToggle || selectedToggle === 'all'">
{{ company.name }}
</li>
</ul>
`,
})
export class KitchenSinkToggleList {
selectedToggle: "all" | "large" | "small" = "all";
companyList = [
{ name: "A large enterprise company", size: "large" },
{ name: "Another enterprise company", size: "large" },
{ name: "A smaller company", size: "small" },
];
}

View File

@ -0,0 +1 @@
export * from "./kitchen-sink.stories";

View File

@ -0,0 +1,114 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { AsyncActionsModule } from "../../async-actions";
import { AvatarModule } from "../../avatar";
import { BadgeModule } from "../../badge";
import { BannerModule } from "../../banner";
import { BreadcrumbsModule } from "../../breadcrumbs";
import { ButtonModule } from "../../button";
import { CalloutModule } from "../../callout";
import { CheckboxModule } from "../../checkbox";
import { ColorPasswordModule } from "../../color-password";
import { DialogModule } from "../../dialog";
import { FormControlModule } from "../../form-control";
import { FormFieldModule } from "../../form-field";
import { IconModule } from "../../icon";
import { IconButtonModule } from "../../icon-button";
import { InputModule } from "../../input";
import { LayoutComponent } from "../../layout";
import { LinkModule } from "../../link";
import { MenuModule } from "../../menu";
import { NavigationModule } from "../../navigation";
import { NoItemsModule } from "../../no-items";
import { PopoverModule } from "../../popover";
import { ProgressModule } from "../../progress";
import { RadioButtonModule } from "../../radio-button";
import { SearchModule } from "../../search";
import { SectionComponent } from "../../section";
import { SelectModule } from "../../select";
import { SharedModule } from "../../shared";
import { TableModule } from "../../table";
import { TabsModule } from "../../tabs";
import { ToggleGroupModule } from "../../toggle-group";
import { TypographyModule } from "../../typography";
@NgModule({
imports: [
AsyncActionsModule,
AvatarModule,
BadgeModule,
BannerModule,
BreadcrumbsModule,
ButtonModule,
CalloutModule,
CheckboxModule,
ColorPasswordModule,
CommonModule,
DialogModule,
FormControlModule,
FormFieldModule,
FormsModule,
IconButtonModule,
IconModule,
InputModule,
LayoutComponent,
LinkModule,
MenuModule,
NavigationModule,
NoItemsModule,
PopoverModule,
ProgressModule,
RadioButtonModule,
ReactiveFormsModule,
RouterModule,
SearchModule,
SectionComponent,
SelectModule,
SharedModule,
TableModule,
TabsModule,
ToggleGroupModule,
TypographyModule,
],
exports: [
AsyncActionsModule,
AvatarModule,
BadgeModule,
BannerModule,
BreadcrumbsModule,
ButtonModule,
CalloutModule,
CheckboxModule,
ColorPasswordModule,
CommonModule,
DialogModule,
FormControlModule,
FormFieldModule,
FormsModule,
IconButtonModule,
IconModule,
InputModule,
LayoutComponent,
LinkModule,
MenuModule,
NavigationModule,
NoItemsModule,
PopoverModule,
ProgressModule,
RadioButtonModule,
ReactiveFormsModule,
RouterModule,
SearchModule,
SectionComponent,
SelectModule,
SharedModule,
TableModule,
TabsModule,
ToggleGroupModule,
TypographyModule,
],
})
export class KitchenSinkSharedModule {}

View File

@ -0,0 +1,15 @@
import { Meta, Story } from "@storybook/addon-docs";
import * as stories from "./kitchen-sink.stories";
<Meta of={stories} />
# Kitchen Sink
The purpose of this story is to compose together all of our components. When snapshot tests run,
we'll be able to spot-check visual changes in a more app-like environment than just the isolated
stories. The stories for the Kitchen Sink exist to be tested by the Chromatic UI tests.
NOTE: These stories will treat "Light & Dark" mode as "Light" mode. This is done to 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`.

View File

@ -0,0 +1,172 @@
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 {
userEvent,
getAllByRole,
getByRole,
getByLabelText,
fireEvent,
} from "@storybook/testing-library";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "../../dialog";
import { LayoutComponent } from "../../layout";
import { I18nMockService } from "../../utils/i18n-mock.service";
import { KitchenSinkForm } from "./components/kitchen-sink-form.component";
import { KitchenSinkMainComponent } from "./components/kitchen-sink-main.component";
import { KitchenSinkTable } from "./components/kitchen-sink-table.component";
import { KitchenSinkToggleList } from "./components/kitchen-sink-toggle-list.component";
import { KitchenSinkSharedModule } from "./kitchen-sink-shared.module";
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 };
},
),
moduleMetadata({
imports: [
KitchenSinkSharedModule,
KitchenSinkForm,
KitchenSinkMainComponent,
KitchenSinkTable,
KitchenSinkToggleList,
],
providers: [
DialogService,
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
close: "Close",
search: "Search",
skipToContent: "Skip to content",
submenu: "submenu",
toggleCollapse: "toggle collapse",
});
},
},
],
}),
applicationConfig({
providers: [
provideNoopAnimations(),
importProvidersFrom(
RouterModule.forRoot(
[
{ path: "", redirectTo: "bitwarden", pathMatch: "full" },
{ path: "bitwarden", component: KitchenSinkMainComponent },
],
{ useHash: true },
),
),
],
}),
],
} as Meta;
type Story = StoryObj<LayoutComponent>;
export const Default: Story = {
render: (args) => {
return {
props: args,
template: /* HTML */ `<bit-layout>
<nav slot="sidebar">
<bit-nav-group text="Password Managers" icon="bwi-collection" [open]="true">
<bit-nav-group text="Favorites" icon="bwi-collection" variant="tree" [open]="true">
<bit-nav-item text="Bitwarden" route="bitwarden"></bit-nav-item>
<bit-nav-divider></bit-nav-divider>
</bit-nav-group>
</bit-nav-group>
</nav>
<router-outlet></router-outlet>
</bit-layout>`,
};
},
};
export const MenuOpen: Story = {
...Default,
play: async (context) => {
const canvas = context.canvasElement;
const table = getByRole(canvas, "table");
const menuButton = getAllByRole(table, "button")[0];
await userEvent.click(menuButton);
},
};
export const DefaultDialogOpen: Story = {
...Default,
play: (context) => {
const canvas = context.canvasElement;
const dialogButton = getByRole(canvas, "button", {
name: "Open Dialog",
});
// workaround for userEvent not firing in FF https://github.com/testing-library/user-event/issues/1075
fireEvent.click(dialogButton);
},
};
export const PopoverOpen: Story = {
...Default,
play: async (context) => {
const canvas = context.canvasElement;
const passwordLabelIcon = getByLabelText(canvas, "A random password (required)", {
selector: "button",
});
await userEvent.click(passwordLabelIcon);
},
};
export const SimpleDialogOpen: Story = {
...Default,
play: (context) => {
const canvas = context.canvasElement;
const submitButton = getByRole(canvas, "button", {
name: "Submit",
});
// workaround for userEvent not firing in FF https://github.com/testing-library/user-event/issues/1075
fireEvent.click(submitButton);
},
};
export const EmptyTab: Story = {
...Default,
play: async (context) => {
const canvas = context.canvasElement;
const emptyTab = getByRole(canvas, "tab", { name: "Empty tab" });
await userEvent.click(emptyTab);
},
};