1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-25 09:03:28 +00:00

[CL-971] update responsive behavior of three panel layout (#19086) (#19149)

* update responsive behavior of three panel layout; give sidenav extra top padding on electron; add stories that show mix of drawer and sidenav states

---------

Co-authored-by: Will Martin <contact@willmartian.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Robyn MacCallum
2026-02-23 12:57:58 -05:00
committed by GitHub
parent b9b23fca40
commit d5478ee8d2
22 changed files with 601 additions and 221 deletions

View File

@@ -25,7 +25,7 @@ const render: Story["render"] = (args) => ({
...args,
},
template: `
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding" disableAnimations>
<bit-dialog disableAnimations>
<span bitDialogTitle>Access selector</span>
<span bitDialogContent>
<bit-access-selector

View File

@@ -1,68 +1,66 @@
<div class="tw-mt-auto">
@let accessibleProducts = accessibleProducts$ | async;
@if (accessibleProducts && accessibleProducts.length > 1) {
<!-- [attr.icon] is used to keep the icon attribute on the bit-nav-item after prod mode is enabled. Matches other navigation items and assists in automated testing. -->
<bit-nav-item
*ngFor="let product of accessibleProducts"
[icon]="product.icon"
[text]="product.name"
[route]="product.appRoute"
[attr.icon]="product.icon"
[forceActiveStyles]="product.isActive"
focusAfterNavTarget="body"
>
</bit-nav-item>
}
@let accessibleProducts = accessibleProducts$ | async;
@if (accessibleProducts && accessibleProducts.length > 1) {
<!-- [attr.icon] is used to keep the icon attribute on the bit-nav-item after prod mode is enabled. Matches other navigation items and assists in automated testing. -->
<bit-nav-item
*ngFor="let product of accessibleProducts"
[icon]="product.icon"
[text]="product.name"
[route]="product.appRoute"
[attr.icon]="product.icon"
[forceActiveStyles]="product.isActive"
focusAfterNavTarget="body"
>
</bit-nav-item>
}
@if (shouldShowPremiumUpgradeButton$ | async) {
<app-upgrade-nav-button></app-upgrade-nav-button>
}
@if (shouldShowPremiumUpgradeButton$ | async) {
<app-upgrade-nav-button></app-upgrade-nav-button>
}
@let moreProducts = moreProducts$ | async;
@if (moreProducts && moreProducts.length > 0) {
<section class="tw-mt-2 tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-0">
<span class="tw-text-xs !tw-text-alt2 tw-p-2 tw-pb-0">{{ "moreFromBitwarden" | i18n }}</span>
<ng-container *ngFor="let more of moreProducts">
<div class="tw-ps-2 tw-pe-2">
<!-- <a> for when the marketing route is external -->
<a
*ngIf="more.marketingRoute.external"
[href]="more.marketingRoute.route"
target="_blank"
rel="noreferrer"
class="tw-flex tw-px-3 tw-py-2 tw-rounded-md tw-font-medium !tw-text-alt2 !tw-no-underline hover:tw-bg-hover-contrast [&>:not(.bwi)]:hover:tw-underline"
>
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1 tw-mx-1"></i>
<div>
{{ more.otherProductOverrides?.name ?? more.name }}
<div
*ngIf="more.otherProductOverrides?.supportingText"
class="tw-text-xs tw-font-normal"
>
{{ more.otherProductOverrides.supportingText }}
</div>
@let moreProducts = moreProducts$ | async;
@if (moreProducts && moreProducts.length > 0) {
<section class="tw-mt-2 tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-0">
<span class="tw-text-xs !tw-text-alt2 tw-p-2 tw-pb-0">{{ "moreFromBitwarden" | i18n }}</span>
<ng-container *ngFor="let more of moreProducts">
<div class="tw-ps-2 tw-pe-2">
<!-- <a> for when the marketing route is external -->
<a
*ngIf="more.marketingRoute.external"
[href]="more.marketingRoute.route"
target="_blank"
rel="noreferrer"
class="tw-flex tw-px-3 tw-py-2 tw-rounded-md tw-font-medium !tw-text-alt2 !tw-no-underline hover:tw-bg-hover-contrast [&>:not(.bwi)]:hover:tw-underline"
>
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1 tw-mx-1"></i>
<div>
{{ more.otherProductOverrides?.name ?? more.name }}
<div
*ngIf="more.otherProductOverrides?.supportingText"
class="tw-text-xs tw-font-normal"
>
{{ more.otherProductOverrides.supportingText }}
</div>
</a>
<!-- <a> for when the marketing route is internal, it needs to use [routerLink] instead of [href] like the external <a> uses. -->
<a
*ngIf="!more.marketingRoute.external"
[routerLink]="more.marketingRoute.route"
rel="noreferrer"
class="tw-flex tw-px-3 tw-py-2 tw-rounded-md tw-font-medium !tw-text-alt2 !tw-no-underline hover:tw-bg-hover-contrast [&>:not(.bwi)]:hover:tw-underline"
>
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1 tw-mx-1"></i>
<div>
{{ more.otherProductOverrides?.name ?? more.name }}
<div
*ngIf="more.otherProductOverrides?.supportingText"
class="tw-text-xs tw-font-normal"
>
{{ more.otherProductOverrides.supportingText }}
</div>
</div>
</a>
<!-- <a> for when the marketing route is internal, it needs to use [routerLink] instead of [href] like the external <a> uses. -->
<a
*ngIf="!more.marketingRoute.external"
[routerLink]="more.marketingRoute.route"
rel="noreferrer"
class="tw-flex tw-px-3 tw-py-2 tw-rounded-md tw-font-medium !tw-text-alt2 !tw-no-underline hover:tw-bg-hover-contrast [&>:not(.bwi)]:hover:tw-underline"
>
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1 tw-mx-1"></i>
<div>
{{ more.otherProductOverrides?.name ?? more.name }}
<div
*ngIf="more.otherProductOverrides?.supportingText"
class="tw-text-xs tw-font-normal"
>
{{ more.otherProductOverrides.supportingText }}
</div>
</a>
</div>
</ng-container>
</section>
}
</div>
</div>
</a>
</div>
</ng-container>
</section>
}

View File

@@ -8,7 +8,7 @@ import { BehaviorSubject } from "rxjs";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
import { IconButtonModule, NavigationModule } from "@bitwarden/components";
import { IconButtonModule, NavigationModule, SideNavService } 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";
@@ -86,6 +86,9 @@ describe("NavigationProductSwitcherComponent", () => {
beforeEach(() => {
fixture = TestBed.createComponent(NavigationProductSwitcherComponent);
// SideNavService.open starts false (managed by LayoutComponent's ResizeObserver in a real
// app). Set it to true so NavItemComponent renders text labels (used in text-content checks).
TestBed.inject(SideNavService).open.set(true);
fixture.detectChanges();
});

View File

@@ -1,8 +1,10 @@
import { Component, Directive, importProvidersFrom, Input } from "@angular/core";
import { provideNoopAnimations } from "@angular/platform-browser/animations";
import { RouterModule } from "@angular/router";
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { BehaviorSubject, Observable, of } from "rxjs";
import { PasswordManagerLogo } from "@bitwarden/assets/svg";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
@@ -17,13 +19,13 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
import {
I18nMockService,
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 { positionFixedWrapperDecorator } from "@bitwarden/components/src/stories/storybook-decorators";
import { GlobalStateProvider } from "@bitwarden/state";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -109,15 +111,6 @@ class MockConfigService implements Partial<ConfigService> {
}
}
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "story-layout",
template: `<ng-content></ng-content>`,
standalone: false,
})
class StoryLayoutComponent {}
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
@@ -132,17 +125,23 @@ const translations: Record<string, string> = {
secureYourInfrastructure: "Secure your infrastructure",
protectYourFamilyOrBusiness: "Protect your family or business",
skipToContent: "Skip to content",
toggleSideNavigation: "Toggle side navigation",
resizeSideNavigation: "Resize side navigation",
submenu: "submenu",
toggleCollapse: "toggle collapse",
close: "Close",
loading: "Loading",
};
export default {
title: "Web/Navigation Product Switcher",
decorators: [
positionFixedWrapperDecorator(),
moduleMetadata({
declarations: [
NavigationProductSwitcherComponent,
MockOrganizationService,
MockProviderService,
StoryLayoutComponent,
StoryContentComponent,
],
imports: [NavigationModule, RouterModule, LayoutComponent, I18nPipe],
@@ -174,19 +173,11 @@ export default {
}),
applicationConfig({
providers: [
provideNoopAnimations(),
importProvidersFrom(
RouterModule.forRoot([
{
path: "",
component: StoryLayoutComponent,
children: [
{
path: "**",
component: StoryContentComponent,
},
],
},
]),
RouterModule.forRoot([{ path: "**", component: StoryContentComponent }], {
useHash: true,
}),
),
{
provide: GlobalStateProvider,
@@ -203,12 +194,47 @@ type Story = StoryObj<
const Template: Story = {
render: (args) => ({
props: args,
props: { ...args, logo: PasswordManagerLogo },
template: `
<router-outlet [mockOrgs]="mockOrgs" [mockProviders]="mockProviders"></router-outlet>
<div class="tw-bg-background-alt3 tw-w-60">
<navigation-product-switcher></navigation-product-switcher>
</div>
<bit-layout>
<bit-side-nav>
<bit-nav-logo [openIcon]="logo" route="." label="Bitwarden"></bit-nav-logo>
<bit-nav-item text="Vault" icon="bwi-lock"></bit-nav-item>
<bit-nav-item text="Send" icon="bwi-send"></bit-nav-item>
<bit-nav-group text="Tools" icon="bwi-key" [open]="true">
<bit-nav-item text="Generator"></bit-nav-item>
<bit-nav-item text="Import"></bit-nav-item>
<bit-nav-item text="Export"></bit-nav-item>
</bit-nav-group>
<bit-nav-group text="Organizations" icon="bwi-business" [open]="true">
<bit-nav-item text="Acme Corp" icon="bwi-collection-shared"></bit-nav-item>
<bit-nav-item text="Acme Corp — Vault" variant="tree"></bit-nav-item>
<bit-nav-item text="Acme Corp — Members" variant="tree"></bit-nav-item>
<bit-nav-item text="Acme Corp — Settings" variant="tree"></bit-nav-item>
<bit-nav-item text="My Family" icon="bwi-collection-shared"></bit-nav-item>
<bit-nav-item text="My Family — Vault" variant="tree"></bit-nav-item>
<bit-nav-item text="My Family — Members" variant="tree"></bit-nav-item>
<bit-nav-item text="Initech" icon="bwi-collection-shared"></bit-nav-item>
<bit-nav-item text="Initech — Vault" variant="tree"></bit-nav-item>
<bit-nav-item text="Initech — Members" variant="tree"></bit-nav-item>
<bit-nav-item text="Initech — Settings" variant="tree"></bit-nav-item>
<bit-nav-item text="Umbrella Corp" icon="bwi-collection-shared"></bit-nav-item>
<bit-nav-item text="Umbrella Corp — Vault" variant="tree"></bit-nav-item>
<bit-nav-item text="Umbrella Corp — Members" variant="tree"></bit-nav-item>
<bit-nav-item text="Umbrella Corp — Settings" variant="tree"></bit-nav-item>
<bit-nav-item text="Stark Industries" icon="bwi-collection-shared"></bit-nav-item>
<bit-nav-item text="Stark Industries — Vault" variant="tree"></bit-nav-item>
<bit-nav-item text="Stark Industries — Members" variant="tree"></bit-nav-item>
<bit-nav-item text="Stark Industries — Settings" variant="tree"></bit-nav-item>
</bit-nav-group>
<bit-nav-item text="Settings" icon="bwi-cog"></bit-nav-item>
<ng-container slot="product-switcher">
<bit-nav-divider></bit-nav-divider>
<navigation-product-switcher [mockOrgs]="mockOrgs" [mockProviders]="mockProviders"></navigation-product-switcher>
</ng-container>
</bit-side-nav>
<router-outlet></router-outlet>
</bit-layout>
`,
}),
};

View File

@@ -1,8 +1,12 @@
<bit-side-nav [variant]="variant">
<ng-content></ng-content>
<ng-container slot="footer">
<ng-container slot="product-switcher">
<bit-nav-divider></bit-nav-divider>
<navigation-product-switcher></navigation-product-switcher>
</ng-container>
<ng-container slot="footer">
<app-toggle-width></app-toggle-width>
</ng-container>
</bit-side-nav>