From 0f375c3a0e848a555ae39ae8cabf38b6d49c6f53 Mon Sep 17 00:00:00 2001 From: Victoria League Date: Fri, 22 Mar 2024 10:17:00 -0400 Subject: [PATCH] [CL-146] Add kitchen sink story (#8310) --- .../components/kitchen-sink-form.component.ts | 176 ++++++++++++++++++ .../components/kitchen-sink-main.component.ts | 117 ++++++++++++ .../kitchen-sink-table.component.ts | 49 +++++ .../kitchen-sink-toggle-list.component.ts | 34 ++++ .../src/stories/kitchen-sink/index.ts | 1 + .../kitchen-sink-shared.module.ts | 114 ++++++++++++ .../src/stories/kitchen-sink/kitchen-sink.mdx | 15 ++ .../kitchen-sink/kitchen-sink.stories.ts | 172 +++++++++++++++++ 8 files changed, 678 insertions(+) create mode 100644 libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts create mode 100644 libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts create mode 100644 libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts create mode 100644 libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts create mode 100644 libs/components/src/stories/kitchen-sink/index.ts create mode 100644 libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts create mode 100644 libs/components/src/stories/kitchen-sink/kitchen-sink.mdx create mode 100644 libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts new file mode 100644 index 0000000000..fa3a915f22 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts @@ -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: ` +
+
+ +
+ + + Your favorite feature + + + + + Your favorite color + + + + + + + Your top 3 worst passwords + + + + + + How many passwords do you have? + + + + + + A random password + + + + + + +
+ + An example of a strong password:   + + + +
+ + + Check if you love security + + Hint: the correct answer is yes! + + + + Do you currently use Bitwarden? + + Yes + + + No + + + + + + + +
A strong password has the following:
+
    +
  • Letters
  • +
  • Numbers
  • +
  • Special characters
  • +
+
+
+ `, +}) +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" }, + ]; +} 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 new file mode 100644 index 0000000000..ae4448eb76 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts @@ -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: ` + + Dialog body text goes here. + + + + + + `, +}) +class KitchenSinkDialog { + constructor(public dialogRef: DialogRef) {} +} + +@Component({ + standalone: true, + selector: "bit-tab-main", + imports: [ + KitchenSinkSharedModule, + KitchenSinkTable, + KitchenSinkToggleList, + KitchenSinkForm, + KitchenSinkDialog, + ], + template: ` + + Kitchen Sink test zone + + +

+ + + {{ item.name }} + + +

+ + +

+ 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. +

+
+ +
+

+ Bitwarden +

+ Learn more +
+ + + + +

About

+ + + +
+ +

Companies using Bitwarden

+ +
+ +

Survey

+ +
+
+ + + + + This tab is empty + +

Try searching for what you are looking for:

+ +

Note that the search bar is not functional

+
+
+
+
+
+ `, +}) +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: "/" }, + ]; +} diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts new file mode 100644 index 0000000000..3c6d6f1144 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts @@ -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: ` + + + + Product + User + Options + + + + + Password Manager + Everyone + + + + Anchor link + Another link + + + + + + + Secrets Manager + Developers + + + + Anchor link + Another link + + + + + + + + `, +}) +export class KitchenSinkTable {} diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts new file mode 100644 index 0000000000..2804c9e835 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts @@ -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: ` +
+ + All 3 + + Enterprise 2 + + Mid-sized 1 + +
+ + `, +}) +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" }, + ]; +} diff --git a/libs/components/src/stories/kitchen-sink/index.ts b/libs/components/src/stories/kitchen-sink/index.ts new file mode 100644 index 0000000000..1d13b23136 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/index.ts @@ -0,0 +1 @@ +export * from "./kitchen-sink.stories"; 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 new file mode 100644 index 0000000000..56e3a92e2a --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts @@ -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 {} diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.mdx b/libs/components/src/stories/kitchen-sink/kitchen-sink.mdx new file mode 100644 index 0000000000..49493f749e --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.mdx @@ -0,0 +1,15 @@ +import { Meta, Story } from "@storybook/addon-docs"; + +import * as stories from "./kitchen-sink.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`. diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts new file mode 100644 index 0000000000..70adb21191 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -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 */ `
+ ${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 }; + }, + ), + 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; + +export const Default: Story = { + render: (args) => { + return { + props: args, + template: /* HTML */ ` + + + `, + }; + }, +}; + +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); + }, +};