1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-12-05 09:14:28 +01:00
This commit is contained in:
Mark Youssef 2025-12-04 18:38:43 -06:00 committed by GitHub
commit 9bd6041957
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 356 additions and 59 deletions

View File

@ -3,7 +3,8 @@ import { RouterModule } from "@angular/router";
import { mock } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { NavigationModule } from "@bitwarden/components";
import { NavigationModule, StorybookGlobalStateProvider } from "@bitwarden/components";
import { GlobalStateProvider } from "@bitwarden/state";
import { DesktopLayoutComponent } from "./desktop-layout.component";
@ -33,6 +34,10 @@ describe("DesktopLayoutComponent", () => {
provide: I18nService,
useValue: mock<I18nService>(),
},
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}).compileComponents();

View File

@ -2,7 +2,8 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
import { mock } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { NavigationModule } from "@bitwarden/components";
import { NavigationModule, StorybookGlobalStateProvider } from "@bitwarden/components";
import { GlobalStateProvider } from "@bitwarden/state";
import { DesktopSideNavComponent } from "./desktop-side-nav.component";
@ -32,6 +33,10 @@ describe("DesktopSideNavComponent", () => {
provide: I18nService,
useValue: mock<I18nService>(),
},
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}).compileComponents();

View File

@ -4255,5 +4255,8 @@
},
"sessionTimeoutHeader": {
"message": "Session timeout"
},
"resizeSideNavigation": {
"message": "Resize side navigation"
}
}

View File

@ -7,10 +7,15 @@ import { BehaviorSubject } from "rxjs";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { IconButtonModule, NavigationModule } from "@bitwarden/components";
import {
IconButtonModule,
NavigationModule,
StorybookGlobalStateProvider,
} from "@bitwarden/components";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { NavItemComponent } from "@bitwarden/components/src/navigation/nav-item.component";
import { GlobalStateProvider } from "@bitwarden/state";
import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service";
@ -72,6 +77,10 @@ describe("NavigationProductSwitcherComponent", () => {
provide: ActivatedRoute,
useValue: mock<ActivatedRoute>(),
},
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}).compileComponents();
});

View File

@ -16,10 +16,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
import { LayoutComponent, NavigationModule } from "@bitwarden/components";
import {
LayoutComponent,
NavigationModule,
StorybookGlobalStateProvider,
} from "@bitwarden/components";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service";
import { GlobalStateProvider } from "@bitwarden/state";
import { I18nPipe } from "@bitwarden/ui-common";
import { ProductSwitcherService } from "../shared/product-switcher.service";
@ -180,6 +185,10 @@ export default {
},
]),
),
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}),
],

View File

@ -39,7 +39,8 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { LayoutComponent } from "@bitwarden/components";
import { LayoutComponent, StorybookGlobalStateProvider } from "@bitwarden/components";
import { GlobalStateProvider } from "@bitwarden/state";
import { GroupView } from "../../../admin-console/organizations/core";
import { PreloadedEnglishI18nModule } from "../../../core/tests";
@ -156,6 +157,10 @@ export default {
providers: [
importProvidersFrom(RouterModule.forRoot([], { useHash: true })),
importProvidersFrom(PreloadedEnglishI18nModule),
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}),
],

View File

@ -12214,5 +12214,8 @@
},
"userVerificationFailed": {
"message": "User verification failed."
},
"resizeSideNavigation": {
"message": "Resize side navigation"
}
}

View File

@ -6,13 +6,14 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an
import { getAllByRole, userEvent } from "storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { GlobalStateProvider } from "@bitwarden/state";
import { ButtonModule } from "../button";
import { IconButtonModule } from "../icon-button";
import { LayoutComponent } from "../layout";
import { SharedModule } from "../shared";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { I18nMockService } from "../utils/i18n-mock.service";
import { I18nMockService, StorybookGlobalStateProvider } from "../utils";
import { DialogModule } from "./dialog.module";
import { DialogService } from "./dialog.service";
@ -161,6 +162,10 @@ export default {
});
},
},
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}),
],

View File

@ -1,7 +1,8 @@
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { GlobalStateProvider } from "@bitwarden/state";
import { ButtonModule } from "../button";
import { CalloutModule } from "../callout";
@ -9,7 +10,7 @@ import { LayoutComponent } from "../layout";
import { mockLayoutI18n } from "../layout/mocks";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { TypographyModule } from "../typography";
import { I18nMockService } from "../utils";
import { I18nMockService, StorybookGlobalStateProvider } from "../utils";
import { DrawerBodyComponent } from "./drawer-body.component";
import { DrawerHeaderComponent } from "./drawer-header.component";
@ -47,6 +48,14 @@ export default {
},
],
}),
applicationConfig({
providers: [
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}),
],
} as Meta<DrawerComponent>;

View File

@ -1,16 +1,23 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular";
import {
Meta,
StoryObj,
applicationConfig,
componentWrapperDecorator,
moduleMetadata,
} from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { GlobalStateProvider } from "@bitwarden/state";
import { AvatarModule } from "../avatar";
import { BadgeModule } from "../badge";
import { IconButtonModule } from "../icon-button";
import { LayoutComponent } from "../layout";
import { TypographyModule } from "../typography";
import { I18nMockService } from "../utils/i18n-mock.service";
import { I18nMockService, StorybookGlobalStateProvider } from "../utils";
import { ItemActionComponent } from "./item-action.component";
import { ItemContentComponent } from "./item-content.component";
@ -50,6 +57,14 @@ export default {
},
],
}),
applicationConfig({
providers: [
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}),
componentWrapperDecorator((story) => `<div class="tw-bg-background-alt tw-p-2">${story}</div>`),
],
parameters: {

View File

@ -1,13 +1,15 @@
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { userEvent } from "storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { GlobalStateProvider } from "@bitwarden/state";
import { CalloutModule } from "../callout";
import { NavigationModule } from "../navigation";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { I18nMockService } from "../utils/i18n-mock.service";
import { StorybookGlobalStateProvider } from "../utils/state-mock";
import { LayoutComponent } from "./layout.component";
import { mockLayoutI18n } from "./mocks";
@ -28,6 +30,14 @@ export default {
},
],
}),
applicationConfig({
providers: [
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}),
],
parameters: {
chromatic: { viewports: [640, 1280] },

View File

@ -5,4 +5,5 @@ export const mockLayoutI18n = {
submenu: "submenu",
toggleCollapse: "toggle collapse",
loading: "Loading",
resizeSideNavigation: "Resize side navigation",
};

View File

@ -3,11 +3,13 @@ import { RouterModule } from "@angular/router";
import { StoryObj, Meta, moduleMetadata, applicationConfig } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { GlobalStateProvider } from "@bitwarden/state";
import { LayoutComponent } from "../layout";
import { SharedModule } from "../shared/shared.module";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { I18nMockService } from "../utils/i18n-mock.service";
import { StorybookGlobalStateProvider } from "../utils/state-mock";
import { NavGroupComponent } from "./nav-group.component";
import { NavigationModule } from "./navigation.module";
@ -42,6 +44,7 @@ export default {
toggleSideNavigation: "Toggle side navigation",
skipToContent: "Skip to content",
loading: "Loading",
resizeSideNavigation: "Resize side navigation",
});
},
},
@ -58,6 +61,10 @@ export default {
{ useHash: true },
),
),
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}),
],

View File

@ -1,12 +1,14 @@
import { RouterTestingModule } from "@angular/router/testing";
import { StoryObj, Meta, moduleMetadata } from "@storybook/angular";
import { StoryObj, Meta, moduleMetadata, applicationConfig } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { GlobalStateProvider } from "@bitwarden/state";
import { IconButtonModule } from "../icon-button";
import { LayoutComponent } from "../layout";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { I18nMockService } from "../utils/i18n-mock.service";
import { StorybookGlobalStateProvider } from "../utils/state-mock";
import { NavItemComponent } from "./nav-item.component";
import { NavigationModule } from "./navigation.module";
@ -31,11 +33,20 @@ export default {
toggleSideNavigation: "Toggle side navigation",
skipToContent: "Skip to content",
loading: "Loading",
resizeSideNavigation: "Resize side navigation",
});
},
},
],
}),
applicationConfig({
providers: [
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}),
],
parameters: {
design: {

View File

@ -5,47 +5,64 @@
};
as data
) {
<nav
id="bit-side-nav"
class="tw-sticky tw-inset-y-0 tw-left-0 tw-z-30 tw-flex tw-h-screen tw-flex-col tw-overscroll-none tw-overflow-auto tw-bg-background-alt3 tw-outline-none"
[ngClass]="{ 'tw-w-60': data.open }"
[ngStyle]="
variant() === 'secondary' && {
'--color-text-alt2': 'var(--color-text-main)',
'--color-background-alt3': 'var(--color-secondary-100)',
'--color-background-alt4': 'var(--color-secondary-300)',
'--color-hover-contrast': 'var(--color-hover-default)',
}
"
[cdkTrapFocus]="data.isOverlay"
[attr.role]="data.isOverlay ? 'dialog' : null"
[attr.aria-modal]="data.isOverlay ? 'true' : null"
(keydown)="handleKeyDown($event)"
>
<ng-content></ng-content>
<!-- 53rem = ~850px -->
<!-- This is a magic number. This number was selected by going to the UI and finding the number that felt the best to me and design. No real rhyme or reason :) -->
<div
class="[@media(min-height:53rem)]:tw-sticky tw-bottom-0 tw-left-0 tw-z-20 tw-mt-auto tw-w-full tw-bg-background-alt3"
<div class="tw-relative">
<nav
id="bit-side-nav"
class="tw-sticky tw-inset-y-0 tw-left-0 tw-z-30 tw-flex tw-h-screen tw-flex-col tw-overscroll-none tw-overflow-auto tw-bg-background-alt3 tw-outline-none"
[style.width.px]="data.open ? (sideNavService.width$ | async) : undefined"
[ngStyle]="
variant() === 'secondary' && {
'--color-text-alt2': 'var(--color-text-main)',
'--color-background-alt3': 'var(--color-secondary-100)',
'--color-background-alt4': 'var(--color-secondary-300)',
'--color-hover-contrast': 'var(--color-hover-default)',
}
"
[cdkTrapFocus]="data.isOverlay"
[attr.role]="data.isOverlay ? 'dialog' : null"
[attr.aria-modal]="data.isOverlay ? 'true' : null"
(keydown)="handleKeyDown($event)"
>
<bit-nav-divider></bit-nav-divider>
@if (data.open) {
<ng-content select="[slot=footer]"></ng-content>
}
<div class="tw-mx-0.5 tw-my-4 tw-w-[3.75rem]">
<button
#toggleButton
type="button"
class="tw-mx-auto tw-block tw-max-w-fit"
[bitIconButton]="data.open ? 'bwi-angle-left' : 'bwi-angle-right'"
buttonType="nav-contrast"
size="small"
(click)="sideNavService.toggle()"
[label]="'toggleSideNavigation' | i18n"
[attr.aria-expanded]="data.open"
aria-controls="bit-side-nav"
></button>
<ng-content></ng-content>
<!-- 53rem = ~850px -->
<!-- This is a magic number. This number was selected by going to the UI and finding the number that felt the best to me and design. No real rhyme or reason :) -->
<div
class="[@media(min-height:53rem)]:tw-sticky tw-bottom-0 tw-left-0 tw-z-20 tw-mt-auto tw-w-full tw-bg-background-alt3"
>
<bit-nav-divider></bit-nav-divider>
@if (data.open) {
<ng-content select="[slot=footer]"></ng-content>
}
<div class="tw-mx-0.5 tw-my-4 tw-w-[3.75rem]">
<button
#toggleButton
type="button"
class="tw-mx-auto tw-block tw-max-w-fit"
[bitIconButton]="data.open ? 'bwi-angle-left' : 'bwi-angle-right'"
buttonType="nav-contrast"
size="small"
(click)="sideNavService.toggle()"
[label]="'toggleSideNavigation' | i18n"
[attr.aria-expanded]="data.open"
aria-controls="bit-side-nav"
></button>
</div>
</div>
</div>
</nav>
</nav>
<div
cdkDrag
(cdkDragMoved)="onDragMoved($event)"
class="tw-absolute tw-top-0 -tw-right-0.5 tw-z-30 tw-h-full tw-w-1 tw-cursor-col-resize tw-transition-colors tw-duration-200 hover:tw-ease-in-out hover:tw-delay-500 hover:tw-bg-primary-300 focus:tw-outline-none focus-visible:tw-bg-primary-300 before:tw-content-[''] before:tw-absolute before:tw-block before:tw-inset-y-0 before:-tw-left-0.5 before:-tw-right-1"
[ngClass]="{ 'tw-hidden': !data.open }"
tabindex="0"
(keydown)="onKeydown($event)"
role="separator"
[attr.aria-valuenow]="sideNavService.width$ | async"
[attr.aria-valuemax]="sideNavService.MAX_OPEN_WIDTH"
[attr.aria-valuemin]="sideNavService.MIN_OPEN_WIDTH"
aria-orientation="vertical"
aria-controls="bit-side-nav"
[attr.aria-label]="'resizeSideNavigation' | i18n"
></div>
</div>
}

View File

@ -1,4 +1,5 @@
import { CdkTrapFocus } from "@angular/cdk/a11y";
import { DragDropModule, CdkDragMove } from "@angular/cdk/drag-drop";
import { CommonModule } from "@angular/common";
import { Component, ElementRef, inject, input, viewChild } from "@angular/core";
@ -16,13 +17,23 @@ export type SideNavVariant = "primary" | "secondary";
@Component({
selector: "bit-side-nav",
templateUrl: "side-nav.component.html",
imports: [CommonModule, CdkTrapFocus, NavDividerComponent, BitIconButtonComponent, I18nPipe],
imports: [
CommonModule,
CdkTrapFocus,
NavDividerComponent,
BitIconButtonComponent,
I18nPipe,
DragDropModule,
],
})
export class SideNavComponent {
protected sideNavService = inject(SideNavService);
readonly variant = input<SideNavVariant>("primary");
private readonly toggleButton = viewChild("toggleButton", { read: ElementRef });
protected sideNavService = inject(SideNavService);
private elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
protected handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
@ -33,4 +44,23 @@ export class SideNavComponent {
return true;
};
protected onDragMoved(event: CdkDragMove) {
const rectX = this.elementRef.nativeElement.getBoundingClientRect().x;
const eventXPointer = event.pointerPosition.x;
this.sideNavService.setWidthFromDrag(eventXPointer, rectX);
// Fix for CDK applying a transform that can cause visual drifting
const element = event.source.element.nativeElement;
element.style.transform = "none";
}
protected onKeydown(event: KeyboardEvent) {
if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
this.sideNavService.setWidthFromKeys(event.key);
}
return;
}
}

View File

@ -1,15 +1,34 @@
import { Injectable } from "@angular/core";
import { inject, Injectable } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { BehaviorSubject, Observable, combineLatest, fromEvent, map, startWith } from "rxjs";
import {
BehaviorSubject,
Observable,
combineLatest,
fromEvent,
map,
startWith,
debounceTime,
first,
} from "rxjs";
import { BIT_SIDE_NAV_DISK, GlobalStateProvider, KeyDefinition } from "@bitwarden/state";
import { BREAKPOINTS, isAtOrLargerThanBreakpoint } from "../utils/responsive-utils";
type CollapsePreference = "open" | "closed" | null;
const BIT_SIDE_NAV_WIDTH_KEY_DEF = new KeyDefinition<number>(BIT_SIDE_NAV_DISK, "side-nav-width", {
deserializer: (s) => s,
});
@Injectable({
providedIn: "root",
})
export class SideNavService {
readonly DEFAULT_OPEN_WIDTH = 288;
readonly MIN_OPEN_WIDTH = 240;
readonly MAX_OPEN_WIDTH = 384;
private _open$ = new BehaviorSubject<boolean>(isAtOrLargerThanBreakpoint("md"));
open$ = this._open$.asObservable();
@ -21,7 +40,16 @@ export class SideNavService {
map(([open, isLargeScreen]) => open && !isLargeScreen),
);
private readonly _width$ = new BehaviorSubject<number>(this.DEFAULT_OPEN_WIDTH);
readonly width$ = this._width$.asObservable();
private readonly widthState = inject(GlobalStateProvider).get(BIT_SIDE_NAV_WIDTH_KEY_DEF);
readonly widthState$ = this.widthState.state$.pipe(
map((width) => width ?? this.DEFAULT_OPEN_WIDTH),
);
constructor() {
// Handle open/close state
combineLatest([this.isLargeScreen$, this.userCollapsePreference$])
.pipe(takeUntilDestroyed())
.subscribe(([isLargeScreen, userCollapsePreference]) => {
@ -32,6 +60,16 @@ export class SideNavService {
this.setOpen();
}
});
// Initialize the resizable width from state provider
this.widthState$.pipe(first()).subscribe((width: number) => {
this._width$.next(width);
});
// Handle width resize events
this.width$.pipe(debounceTime(200), takeUntilDestroyed()).subscribe((width) => {
void this.widthState.update(() => width);
});
}
get open() {
@ -46,6 +84,9 @@ export class SideNavService {
this._open$.next(false);
}
/**
* Toggle the open/close state of the side nav
*/
toggle() {
const curr = this._open$.getValue();
// Store user's preference based on what state they're toggling TO
@ -57,8 +98,54 @@ export class SideNavService {
this.setOpen();
}
}
/**
* Set new side nav width from drag event coordinates
*
* @param eventXCoordinate x coordinate of the pointer's bounding client rect
* @param dragElementXCoordinate x coordinate of the drag element's bounding client rect
*/
setWidthFromDrag(eventXPointer: number, dragElementXCoordinate: number) {
const newWidth = eventXPointer - dragElementXCoordinate;
this._setWidthWithinMinMax(newWidth);
}
/**
* Set new side nav width from arrow key events
*
* @param key event key, must be either ArrowRight or ArrowLeft
*/
setWidthFromKeys(key: "ArrowRight" | "ArrowLeft") {
const currentWidth = this._width$.getValue();
if (key === "ArrowLeft") {
const newWidth = currentWidth - 10;
this._setWidthWithinMinMax(newWidth);
}
if (key === "ArrowRight") {
const newWidth = currentWidth + 10;
this._setWidthWithinMinMax(newWidth);
}
}
/**
* Calculate and set the new width, not going out of the min/max bounds
* @param newWidth desired new width: number
*/
private _setWidthWithinMinMax(newWidth: number) {
const width = Math.min(Math.max(newWidth, this.MIN_OPEN_WIDTH), this.MAX_OPEN_WIDTH);
this._width$.next(width);
}
}
/**
* Helper function for subscribing to media query events
* @param query media query to validate against
* @returns Observable<boolean>
*/
export const media = (query: string): Observable<boolean> => {
const mediaQuery = window.matchMedia(query);
return fromEvent<MediaQueryList>(mediaQuery, "change").pipe(

View File

@ -13,9 +13,11 @@ import {
import { PasswordManagerLogo } from "@bitwarden/assets/svg";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { GlobalStateProvider } from "@bitwarden/state";
import { LayoutComponent } from "../../layout";
import { I18nMockService } from "../../utils/i18n-mock.service";
import { StorybookGlobalStateProvider } from "../../utils/state-mock";
import { positionFixedWrapperDecorator } from "../storybook-decorators";
import { DialogVirtualScrollBlockComponent } from "./components/dialog-virtual-scroll-block.component";
@ -65,9 +67,14 @@ export default {
yes: "Yes",
no: "No",
loading: "Loading",
resizeSideNavigation: "Resize side navigation",
});
},
},
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}),
],

View File

@ -1,13 +1,14 @@
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { GlobalStateProvider } from "@bitwarden/state";
import { countries } from "../form/countries";
import { LayoutComponent } from "../layout";
import { mockLayoutI18n } from "../layout/mocks";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { I18nMockService } from "../utils";
import { I18nMockService, StorybookGlobalStateProvider } from "../utils";
import { TableDataSource } from "./table-data-source";
import { TableModule } from "./table.module";
@ -27,6 +28,14 @@ export default {
},
],
}),
applicationConfig({
providers: [
{
provide: GlobalStateProvider,
useClass: StorybookGlobalStateProvider,
},
],
}),
],
argTypes: {
alignRowContent: {

View File

@ -2,3 +2,4 @@ export * from "./aria-disable-element";
export * from "./function-to-observable";
export * from "./has-scrollable-content";
export * from "./i18n-mock.service";
export * from "./state-mock";

View File

@ -0,0 +1,48 @@
import { BehaviorSubject, Observable } from "rxjs";
import {
GlobalState,
StateUpdateOptions,
GlobalStateProvider,
KeyDefinition,
} from "@bitwarden/state";
export class StorybookGlobalState<T> implements GlobalState<T> {
private _state$ = new BehaviorSubject<T | null>(null);
constructor(initialValue?: T | null) {
this._state$.next(initialValue ?? null);
}
async update<TCombine>(
configureState: (state: T | null, dependency: TCombine) => T | null,
options?: Partial<StateUpdateOptions<T, TCombine>>,
): Promise<T | null> {
const currentState = this._state$.value;
const newState = configureState(currentState, null as TCombine);
this._state$.next(newState);
return newState;
}
get state$(): Observable<T | null> {
return this._state$.asObservable();
}
setValue(value: T | null): void {
this._state$.next(value);
}
}
export class StorybookGlobalStateProvider implements GlobalStateProvider {
private states = new Map<string, StorybookGlobalState<any>>();
get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
const key = `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`;
if (!this.states.has(key)) {
this.states.set(key, new StorybookGlobalState<T>());
}
return this.states.get(key)!;
}
}

View File

@ -103,6 +103,7 @@ export const AUTOTYPE_SETTINGS_DISK = new StateDefinition("autotypeSettings", "d
export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanner", "disk", {
web: "disk-local",
});
export const BIT_SIDE_NAV_DISK = new StateDefinition("bitSideNav", "disk");
// DIRT