mirror of
https://github.com/bitwarden/browser.git
synced 2025-12-05 09:14:28 +01:00
Merge 5b97a273f5 into d32365fbba
This commit is contained in:
commit
9bd6041957
@ -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();
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -4255,5 +4255,8 @@
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@ -12214,5 +12214,8 @@
|
||||
},
|
||||
"userVerificationFailed": {
|
||||
"message": "User verification failed."
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@ -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>;
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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] },
|
||||
|
||||
@ -5,4 +5,5 @@ export const mockLayoutI18n = {
|
||||
submenu: "submenu",
|
||||
toggleCollapse: "toggle collapse",
|
||||
loading: "Loading",
|
||||
resizeSideNavigation: "Resize side navigation",
|
||||
};
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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";
|
||||
|
||||
48
libs/components/src/utils/state-mock.ts
Normal file
48
libs/components/src/utils/state-mock.ts
Normal 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)!;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user