From 9d82fc7dfca575996dd17ea3034c8c40c167f837 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Tue, 23 Sep 2025 15:36:18 -0400 Subject: [PATCH] [CL-95] loading spinner (#16363) * add spiner from previous branch * add loading spinner to button * Add spinner to dialog * Add spinner to icon button * add spinner to multi select component * fix spinner positioning * Add mock i18n in stories where needed * round stroke caps. Update classes * fix ts error * fix broken tests * add missing translation keys to stories * Add mising key for layout * Add mising key for nav group * Add mising key for spotlight * Add mising key for product switcher * Add mising key for dialog service * add translation to copy click story --- .../product-switcher.stories.ts | 1 + .../components/spotlight/spotlight.stories.ts | 1 + .../src/async-actions/in-forms.stories.ts | 1 + .../src/async-actions/standalone.stories.ts | 10 ++++ .../src/banner/banner.component.spec.ts | 1 + libs/components/src/banner/banner.stories.ts | 1 + .../src/breadcrumbs/breadcrumbs.stories.ts | 1 + .../src/button/button.component.html | 11 ++-- .../components/src/button/button.component.ts | 3 +- libs/components/src/button/button.stories.ts | 18 ++++++- .../src/copy-click/copy-click.stories.ts | 1 + .../src/dialog/dialog.service.stories.ts | 1 + .../src/dialog/dialog/dialog.component.html | 2 +- .../src/dialog/dialog/dialog.component.ts | 2 + .../simple-dialog/simple-dialog.stories.ts | 13 +++++ .../src/disclosure/disclosure.stories.ts | 13 +++++ libs/components/src/drawer/drawer.stories.ts | 1 + .../src/form-field/form-field.stories.ts | 1 + .../form-field/password-input-toggle.spec.ts | 1 + .../password-input-toggle.stories.ts | 5 +- .../icon-button/icon-button.component.html | 6 +-- .../src/icon-button/icon-button.component.ts | 3 +- .../src/icon-button/icon-button.stories.ts | 19 ++++++- libs/components/src/item/item.stories.ts | 1 + libs/components/src/layout/mocks.ts | 1 + libs/components/src/menu/menu.stories.ts | 9 ++++ .../multi-select/multi-select.component.html | 2 +- .../multi-select/multi-select.component.ts | 10 +++- .../src/navigation/nav-group.stories.ts | 1 + .../src/navigation/nav-item.stories.ts | 1 + .../src/no-items/no-items.stories.ts | 8 +++ .../components/src/section/section.stories.ts | 9 ++++ libs/components/src/spinner/index.ts | 1 + .../src/spinner/spinner.component.html | 27 ++++++++++ .../src/spinner/spinner.component.ts | 51 ++++++++++++++++++ .../components/src/spinner/spinner.stories.ts | 53 +++++++++++++++++++ .../components/kitchen-sink-form.component.ts | 1 + .../kitchen-sink/kitchen-sink.stories.ts | 1 + libs/components/src/tabs/tabs.stories.ts | 9 ++++ libs/components/src/toast/toast.stories.ts | 1 + 40 files changed, 283 insertions(+), 19 deletions(-) create mode 100644 libs/components/src/spinner/index.ts create mode 100644 libs/components/src/spinner/spinner.component.html create mode 100644 libs/components/src/spinner/spinner.component.ts create mode 100644 libs/components/src/spinner/spinner.stories.ts diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index 1a44df4dd00..18cb8e26c70 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -124,6 +124,7 @@ export default { switchProducts: "Switch Products", secureYourInfrastructure: "Secure your infrastructure", protectYourFamilyOrBusiness: "Protect your family or business", + loading: "Loading", }); }, }, diff --git a/libs/angular/src/vault/components/spotlight/spotlight.stories.ts b/libs/angular/src/vault/components/spotlight/spotlight.stories.ts index 8e660aacbad..2988344e23d 100644 --- a/libs/angular/src/vault/components/spotlight/spotlight.stories.ts +++ b/libs/angular/src/vault/components/spotlight/spotlight.stories.ts @@ -22,6 +22,7 @@ const meta: Meta = { useFactory: () => { return new I18nMockService({ close: "Close", + loading: "Loading", }); }, }, diff --git a/libs/components/src/async-actions/in-forms.stories.ts b/libs/components/src/async-actions/in-forms.stories.ts index 88383fe85a3..af5034c49d5 100644 --- a/libs/components/src/async-actions/in-forms.stories.ts +++ b/libs/components/src/async-actions/in-forms.stories.ts @@ -148,6 +148,7 @@ export default { required: "required", inputRequired: "Input is required.", inputEmail: "Input is not an email-address.", + loading: "Loading", }); }, }, diff --git a/libs/components/src/async-actions/standalone.stories.ts b/libs/components/src/async-actions/standalone.stories.ts index 99cde70566b..150bdb8813b 100644 --- a/libs/components/src/async-actions/standalone.stories.ts +++ b/libs/components/src/async-actions/standalone.stories.ts @@ -3,11 +3,13 @@ import { action } from "@storybook/addon-actions"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { delay, of } from "rxjs"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ButtonModule } from "../button"; import { IconButtonModule } from "../icon-button"; +import { I18nMockService } from "../utils"; import { AsyncActionsModule } from "./async-actions.module"; import { BitActionDirective } from "./bit-action.directive"; @@ -103,6 +105,14 @@ export default { error: action("LogService.error"), } as Partial, }, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + loading: "Loading", + }); + }, + }, ], }), ], diff --git a/libs/components/src/banner/banner.component.spec.ts b/libs/components/src/banner/banner.component.spec.ts index edfbe8c8be9..1c4378a5aee 100644 --- a/libs/components/src/banner/banner.component.spec.ts +++ b/libs/components/src/banner/banner.component.spec.ts @@ -19,6 +19,7 @@ describe("BannerComponent", () => { useFactory: () => new I18nMockService({ close: "Close", + loading: "Loading", }), }, ], diff --git a/libs/components/src/banner/banner.stories.ts b/libs/components/src/banner/banner.stories.ts index a7649a28228..458f453bf1a 100644 --- a/libs/components/src/banner/banner.stories.ts +++ b/libs/components/src/banner/banner.stories.ts @@ -22,6 +22,7 @@ export default { useFactory: () => { return new I18nMockService({ close: "Close", + loading: "Loading", }); }, }, diff --git a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts index 893f645a913..989b72adbd5 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts @@ -35,6 +35,7 @@ export default { useFactory: () => { return new I18nMockService({ moreBreadcrumbs: "More breadcrumbs", + loading: "Loading", }); }, }, diff --git a/libs/components/src/button/button.component.html b/libs/components/src/button/button.component.html index a07ab9fb99b..26e0c3b4d3d 100644 --- a/libs/components/src/button/button.component.html +++ b/libs/components/src/button/button.component.html @@ -2,10 +2,9 @@ - - - + @if (showLoadingStyle()) { + + + + } diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 1dce792c963..47612685c16 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -14,6 +14,7 @@ import { debounce, interval } from "rxjs"; import { AriaDisableDirective } from "../a11y"; import { ButtonLikeAbstraction, ButtonType, ButtonSize } from "../shared/button-like.abstraction"; +import { SpinnerComponent } from "../spinner"; import { ariaDisableElement } from "../utils"; const focusRing = [ @@ -60,7 +61,7 @@ const buttonStyles: Record = { selector: "button[bitButton], a[bitButton]", templateUrl: "button.component.html", providers: [{ provide: ButtonLikeAbstraction, useExisting: ButtonComponent }], - imports: [NgClass], + imports: [NgClass, SpinnerComponent], hostDirectives: [AriaDisableDirective], }) export class ButtonComponent implements ButtonLikeAbstraction { diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index 180f5cb4aa9..7319b47bce5 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -1,12 +1,28 @@ -import { Meta, StoryObj } from "@storybook/angular"; +import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { formatArgsForCodeSnippet } from "../../../../.storybook/format-args-for-code-snippet"; +import { I18nMockService } from "../utils/i18n-mock.service"; import { ButtonComponent } from "./button.component"; export default { title: "Component Library/Button", component: ButtonComponent, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: I18nService, + useFactory: () => + new I18nMockService({ + loading: "Loading", + }), + }, + ], + }), + ], args: { disabled: false, loading: false, diff --git a/libs/components/src/copy-click/copy-click.stories.ts b/libs/components/src/copy-click/copy-click.stories.ts index 50b9549dedd..208d42b44c4 100644 --- a/libs/components/src/copy-click/copy-click.stories.ts +++ b/libs/components/src/copy-click/copy-click.stories.ts @@ -38,6 +38,7 @@ export default { success: "Success", close: "Close", info: "Info", + loading: "Loading", }); }, }, diff --git a/libs/components/src/dialog/dialog.service.stories.ts b/libs/components/src/dialog/dialog.service.stories.ts index 5a002f7ee7e..caa7a86a2a8 100644 --- a/libs/components/src/dialog/dialog.service.stories.ts +++ b/libs/components/src/dialog/dialog.service.stories.ts @@ -155,6 +155,7 @@ export default { toggleSideNavigation: "Toggle side navigation", yes: "Yes", no: "No", + loading: "Loading", }); }, }, diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index 159944dcc1f..1b701a50584 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -52,7 +52,7 @@ > @if (loading()) {
- +
}
{ + return new I18nMockService({ + loading: "Loading", + }); + }, + }, + ], }), ], parameters: { diff --git a/libs/components/src/disclosure/disclosure.stories.ts b/libs/components/src/disclosure/disclosure.stories.ts index 2e45964ccaa..3ed6903060c 100644 --- a/libs/components/src/disclosure/disclosure.stories.ts +++ b/libs/components/src/disclosure/disclosure.stories.ts @@ -1,6 +1,9 @@ import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + import { IconButtonModule } from "../icon-button"; +import { I18nMockService } from "../utils"; import { DisclosureTriggerForDirective } from "./disclosure-trigger-for.directive"; import { DisclosureComponent } from "./disclosure.component"; @@ -11,6 +14,16 @@ export default { decorators: [ moduleMetadata({ imports: [DisclosureTriggerForDirective, DisclosureComponent, IconButtonModule], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + loading: "Loading", + }); + }, + }, + ], }), ], parameters: { diff --git a/libs/components/src/drawer/drawer.stories.ts b/libs/components/src/drawer/drawer.stories.ts index 1d7aa79137d..727d16b5481 100644 --- a/libs/components/src/drawer/drawer.stories.ts +++ b/libs/components/src/drawer/drawer.stories.ts @@ -41,6 +41,7 @@ export default { return new I18nMockService({ ...mockLayoutI18n, close: "Close", + loading: "Loading", }); }, }, diff --git a/libs/components/src/form-field/form-field.stories.ts b/libs/components/src/form-field/form-field.stories.ts index 7aeb2f63040..01aa56f896c 100644 --- a/libs/components/src/form-field/form-field.stories.ts +++ b/libs/components/src/form-field/form-field.stories.ts @@ -63,6 +63,7 @@ export default { inputRequired: "Input is required.", inputEmail: "Input is not an email-address.", toggleVisibility: "Toggle visibility", + loading: "Loading", }); }, }, diff --git a/libs/components/src/form-field/password-input-toggle.spec.ts b/libs/components/src/form-field/password-input-toggle.spec.ts index 72f2481d789..2b82fad9876 100644 --- a/libs/components/src/form-field/password-input-toggle.spec.ts +++ b/libs/components/src/form-field/password-input-toggle.spec.ts @@ -48,6 +48,7 @@ describe("PasswordInputToggle", () => { provide: I18nService, useValue: new I18nMockService({ toggleVisibility: "Toggle visibility", + loading: "Loading", }), }, ], diff --git a/libs/components/src/form-field/password-input-toggle.stories.ts b/libs/components/src/form-field/password-input-toggle.stories.ts index 3d50a4eb75a..1fcc899ddba 100644 --- a/libs/components/src/form-field/password-input-toggle.stories.ts +++ b/libs/components/src/form-field/password-input-toggle.stories.ts @@ -19,7 +19,10 @@ export default { providers: [ { provide: I18nService, - useValue: new I18nMockService({ toggleVisibility: "Toggle visibility" }), + useValue: new I18nMockService({ + toggleVisibility: "Toggle visibility", + loading: "Loading", + }), }, ], }), diff --git a/libs/components/src/icon-button/icon-button.component.html b/libs/components/src/icon-button/icon-button.component.html index e775a868871..b5826ce9928 100644 --- a/libs/components/src/icon-button/icon-button.component.html +++ b/libs/components/src/icon-button/icon-button.component.html @@ -6,10 +6,6 @@ class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center" [ngClass]="{ 'tw-invisible': !showLoadingStyle() }" > - + diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index 4130cf27170..6bb6ccf10bd 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -16,6 +16,7 @@ import { AriaDisableDirective } from "../a11y"; import { setA11yTitleAndAriaLabel } from "../a11y/set-a11y-title-and-aria-label"; import { ButtonLikeAbstraction } from "../shared/button-like.abstraction"; import { FocusableElement } from "../shared/focusable-element"; +import { SpinnerComponent } from "../spinner"; import { ariaDisableElement } from "../utils"; export type IconButtonType = "primary" | "danger" | "contrast" | "main" | "muted" | "nav-contrast"; @@ -87,7 +88,7 @@ const sizes: Record = { { provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }, { provide: FocusableElement, useExisting: BitIconButtonComponent }, ], - imports: [NgClass], + imports: [NgClass, SpinnerComponent], host: { /** * When the `bitIconButton` input is dynamic from a consumer, Angular doesn't put the diff --git a/libs/components/src/icon-button/icon-button.stories.ts b/libs/components/src/icon-button/icon-button.stories.ts index d716b9697f2..c93beb167bd 100644 --- a/libs/components/src/icon-button/icon-button.stories.ts +++ b/libs/components/src/icon-button/icon-button.stories.ts @@ -1,12 +1,29 @@ -import { Meta, StoryObj } from "@storybook/angular"; +import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { formatArgsForCodeSnippet } from "../../../../.storybook/format-args-for-code-snippet"; +import { I18nMockService } from "../utils"; import { BitIconButtonComponent } from "./icon-button.component"; export default { title: "Component Library/Icon Button", component: BitIconButtonComponent, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + loading: "Loading", + }); + }, + }, + ], + }), + ], args: { bitIconButton: "bwi-plus", label: "Your button label here", diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts index bc2ced7fd11..36bbebee940 100644 --- a/libs/components/src/item/item.stories.ts +++ b/libs/components/src/item/item.stories.ts @@ -44,6 +44,7 @@ export default { skipToContent: "Skip to content", submenu: "submenu", toggleCollapse: "toggle collapse", + loading: "Loading", }); }, }, diff --git a/libs/components/src/layout/mocks.ts b/libs/components/src/layout/mocks.ts index 50c2bd9afb2..8b001eb8fd1 100644 --- a/libs/components/src/layout/mocks.ts +++ b/libs/components/src/layout/mocks.ts @@ -4,4 +4,5 @@ export const mockLayoutI18n = { skipToContent: "Skip to content", submenu: "submenu", toggleCollapse: "toggle collapse", + loading: "Loading", }; diff --git a/libs/components/src/menu/menu.stories.ts b/libs/components/src/menu/menu.stories.ts index b29613061b8..7a4f06232ef 100644 --- a/libs/components/src/menu/menu.stories.ts +++ b/libs/components/src/menu/menu.stories.ts @@ -1,7 +1,10 @@ import { OverlayModule } from "@angular/cdk/overlay"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + import { ButtonModule } from "../button/button.module"; +import { I18nMockService } from "../utils"; import { MenuTriggerForDirective } from "./menu-trigger-for.directive"; import { MenuModule } from "./menu.module"; @@ -12,6 +15,12 @@ export default { decorators: [ moduleMetadata({ imports: [MenuModule, OverlayModule, ButtonModule], + providers: [ + { + provide: I18nService, + useValue: new I18nMockService({ loading: "Loading" }), + }, + ], }), ], parameters: { diff --git a/libs/components/src/multi-select/multi-select.component.html b/libs/components/src/multi-select/multi-select.component.html index 8aa45b7dae8..7f6a22779ef 100644 --- a/libs/components/src/multi-select/multi-select.component.html +++ b/libs/components/src/multi-select/multi-select.component.html @@ -20,7 +20,7 @@ appendTo="body" > - +