1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 00:03:56 +00:00

[CL-718] nav updates (#15226)

* add basic new nav item styling

* update alt-3 bg color

* add x padding to item

* remove copy left in error

* style app switcher to match nav items

* adding new button hover colors

* add new logo lockups

* use new logos in web vault

* fix color and svg fills

* use set height for nav items

* optimize SVGs

* move if logic

* use rem for icon size

* move shield logo

* use shield svg for collapsed icon

* remove unused eslint disable directive

* run prettier

* remove variables

* update logo hover styles

* use more standard flow control syntax

* update admin console logo svg

* add new hover variables

* use class instead of fill

* use variable for logo hover

* remove unnecessary container

* use hover variable for nav switcher

* use correct variables for fill colors

* update hover state to use variable left in error

* give icon width to preserve text alignment

* remove tree story as functionality no longer supported

* remove nested styles helper

* remove obsolete afterContentInit

* remove tree example from layout story

* remove tree example from secondary layout story

* remove tree example from kitchen sink story

* Fix interaction test

* remove remaining references to tree variant
This commit is contained in:
Bryan Cunningham
2025-07-03 16:12:56 -04:00
committed by GitHub
parent 3e4b82d725
commit b4d87007ba
27 changed files with 213 additions and 531 deletions

View File

@@ -55,16 +55,6 @@ export abstract class NavBaseComponent {
matrixParams: "ignored",
};
/**
* If this item is used within a tree, set `variant` to `"tree"`
*/
@Input() variant: "default" | "tree" = "default";
/**
* Depth level when nested inside of a `'tree'` variant
*/
@Input() treeDepth = 0;
/**
* If `true`, do not change styles when nav item is active.
*/

View File

@@ -1,3 +1,3 @@
@if (sideNavService.open$ | async) {
<div class="tw-h-px tw-w-full tw-bg-secondary-300"></div>
<div class="tw-h-px tw-w-full tw-my-2 tw-bg-secondary-300"></div>
}

View File

@@ -6,8 +6,6 @@
[route]="route"
[relativeTo]="relativeTo"
[routerLinkActiveOptions]="routerLinkActiveOptions"
[variant]="variant"
[treeDepth]="treeDepth"
(mainContentClicked)="handleMainContentClicked()"
[ariaLabel]="ariaLabel"
[hideActiveStyles]="parentHideActiveStyles"
@@ -16,9 +14,7 @@
<button
type="button"
class="tw-ms-auto"
[bitIconButton]="
open ? 'bwi-angle-up' : variant === 'tree' ? 'bwi-angle-right' : 'bwi-angle-down'
"
[bitIconButton]="open ? 'bwi-angle-up' : 'bwi-angle-down'"
[buttonType]="'light'"
(click)="toggle($event)"
size="small"
@@ -29,17 +25,9 @@
[attr.aria-label]="['toggleCollapse' | i18n, text].join(' ')"
></button>
</ng-template>
<!-- Show toggle to the left for trees otherwise to the right -->
@if (variant === "tree") {
<ng-container slot="start">
<ng-container *ngTemplateOutlet="button"></ng-container>
</ng-container>
}
<ng-container slot="end">
<ng-content select="[slot=end]"></ng-content>
@if (variant !== "tree") {
<ng-container *ngTemplateOutlet="button"></ng-container>
}
<ng-container *ngTemplateOutlet="button"></ng-container>
</ng-container>
</bit-nav-item>
<!-- [attr.aria-controls] of the above button expects a unique ID on the controlled element -->

View File

@@ -1,6 +1,5 @@
import { CommonModule } from "@angular/common";
import {
AfterContentInit,
booleanAttribute,
Component,
ContentChildren,
@@ -29,7 +28,7 @@ import { SideNavService } from "./side-nav.service";
],
imports: [CommonModule, NavItemComponent, IconButtonModule, I18nPipe],
})
export class NavGroupComponent extends NavBaseComponent implements AfterContentInit {
export class NavGroupComponent extends NavBaseComponent {
@ContentChildren(NavBaseComponent, {
descendants: true,
})
@@ -80,18 +79,6 @@ export class NavGroupComponent extends NavBaseComponent implements AfterContentI
this.setOpen(!this.open);
}
/**
* - For any nested NavGroupComponents or NavItemComponents, increment the `treeDepth` by 1.
*/
private initNestedStyles() {
if (this.variant !== "tree") {
return;
}
[...this.nestedNavComponents].forEach((navGroupOrItem) => {
navGroupOrItem.treeDepth += 1;
});
}
protected handleMainContentClicked() {
if (!this.sideNavService.open) {
if (!this.route) {
@@ -103,8 +90,4 @@ export class NavGroupComponent extends NavBaseComponent implements AfterContentI
}
this.mainContentClicked.emit();
}
ngAfterContentInit(): void {
this.initNestedStyles();
}
}

View File

@@ -111,31 +111,6 @@ export const HideEmptyGroups: StoryObj<NavGroupComponent & { renderChildren: boo
}),
};
export const Tree: StoryObj<NavGroupComponent> = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-side-nav>
<bit-nav-group text="Tree example" icon="bwi-collection-shared" [open]="true">
<bit-nav-group text="Level 1 - with children (empty)" route="t1" icon="bwi-collection-shared" variant="tree"></bit-nav-group>
<bit-nav-item text="Level 1 - no children" route="t2" icon="bwi-collection-shared" variant="tree"></bit-nav-item>
<bit-nav-group text="Level 1 - with children" route="t3" icon="bwi-collection-shared" variant="tree" [open]="true">
<bit-nav-group text="Level 2 - with children" route="t4" icon="bwi-collection-shared" variant="tree" [open]="true">
<bit-nav-item text="Level 3 - no children, no icon" route="t5" variant="tree"></bit-nav-item>
<bit-nav-group text="Level 3 - with children" route="t6" icon="bwi-collection-shared" variant="tree" [open]="true">
<bit-nav-item text="Level 4 - no children, no icon" route="t7" variant="tree"></bit-nav-item>
</bit-nav-group>
</bit-nav-group>
<bit-nav-group text="Level 2 - with children (empty)" route="t8" icon="bwi-collection-shared" variant="tree" [open]="true"></bit-nav-group>
<bit-nav-item text="Level 2 - no children" route="t9" icon="bwi-collection-shared" variant="tree"></bit-nav-item>
</bit-nav-group>
<bit-nav-item text="Level 1 - no children" route="t10" icon="bwi-collection-shared" variant="tree"></bit-nav-item>
</bit-nav-group>
</bit-side-nav>
`,
}),
};
export const Secondary: StoryObj<NavGroupComponent> = {
render: (args) => ({
props: args,

View File

@@ -1,113 +1,77 @@
<ng-container
*ngIf="{
open: sideNavService.open$ | async,
} as data"
>
<div
*ngIf="data.open || icon"
class="tw-relative"
[ngClass]="[
showActiveStyles
? 'tw-bg-background-alt4'
: 'tw-bg-background-alt3 hover:tw-bg-primary-300/60',
fvwStyles$ | async,
]"
>
<div class="tw-ps-2 tw-pe-2">
@let open = sideNavService.open$ | async;
@if (open || icon) {
<div
[ngStyle]="{
'padding-left': data.open ? (variant === 'tree' ? 2.5 : 1) + treeDepth * 1.5 + 'rem' : '0',
}"
class="tw-relative tw-flex"
class="tw-relative tw-rounded-md tw-h-10"
[ngClass]="[
showActiveStyles
? 'tw-bg-background-alt4'
: 'tw-bg-background-alt3 hover:tw-bg-hover-contrast',
fvwStyles$ | async,
]"
>
<div [ngClass]="[variant === 'tree' ? 'tw-py-1' : 'tw-py-2']">
<div
#slotStart
class="[&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:!tw-text-alt2"
>
<ng-content select="[slot=start]"></ng-content>
</div>
<!-- Default content for #slotStart (for consistent sizing) -->
<div
*ngIf="slotStart.childElementCount === 0"
[ngClass]="{
'tw-w-0': variant !== 'tree',
}"
>
<div class="tw-relative tw-flex tw-items-center tw-h-full">
<ng-container *ngIf="route; then isAnchor; else isButton"></ng-container>
<!-- Main content of `NavItem` -->
<ng-template #anchorAndButtonContent>
<div
[title]="text"
class="tw-truncate tw-gap-2 tw-items-center tw-font-bold"
[ngClass]="{ 'tw-text-center': !open, 'tw-flex': open }"
>
<i
class="!tw-m-0 tw-w-4 bwi bwi-fw tw-text-alt2 {{ icon }}"
[attr.aria-hidden]="open"
[attr.aria-label]="text"
></i>
@if (open) {
<span>{{ text }}</span>
}
</div>
</ng-template>
<!-- Show if a value was passed to `this.to` -->
<ng-template #isAnchor>
<!-- The `data-fvw` attribute passes focus to `this.focusVisibleWithin$` -->
<!-- The following `class` field should match the `#isButton` class field below -->
<a
class="tw-w-full tw-px-3 tw-block tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none"
data-fvw
[routerLink]="route"
[relativeTo]="relativeTo"
[attr.aria-label]="ariaLabel || text"
routerLinkActive
[routerLinkActiveOptions]="routerLinkActiveOptions"
[ariaCurrentWhenActive]="'page'"
(isActiveChange)="setIsActive($event)"
(click)="mainContentClicked.emit()"
>
<ng-container *ngTemplateOutlet="anchorAndButtonContent"></ng-container>
</a>
</ng-template>
<!-- Show if `this.to` is falsy -->
<ng-template #isButton>
<!-- Class field should match `#isAnchor` class field above -->
<button
type="button"
class="tw-invisible"
[bitIconButton]="'bwi-angle-down'"
size="small"
aria-hidden="true"
></button>
</div>
</div>
<ng-container *ngIf="route; then isAnchor; else isButton"></ng-container>
<!-- Main content of `NavItem` -->
<ng-template #anchorAndButtonContent>
<div
[title]="text"
class="tw-truncate"
[ngClass]="[
variant === 'tree' ? 'tw-py-1' : 'tw-py-2',
data.open ? 'tw-pe-4' : 'tw-text-center',
]"
>
<i
class="bwi bwi-fw tw-text-alt2 tw-mx-1 {{ icon }}"
[attr.aria-hidden]="data.open"
[attr.aria-label]="text"
></i
><span
*ngIf="data.open"
[ngClass]="showActiveStyles ? 'tw-font-bold' : 'tw-font-semibold'"
>{{ text }}</span
class="tw-w-full tw-px-3 tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none"
data-fvw
(click)="mainContentClicked.emit()"
>
</div>
</ng-template>
<ng-container *ngTemplateOutlet="anchorAndButtonContent"></ng-container>
</button>
</ng-template>
<!-- Show if a value was passed to `this.to` -->
<ng-template #isAnchor>
<!-- The `data-fvw` attribute passes focus to `this.focusVisibleWithin$` -->
<!-- The following `class` field should match the `#isButton` class field below -->
<a
class="tw-w-full tw-truncate tw-border-none tw-bg-transparent tw-p-0 tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none"
data-fvw
[routerLink]="route"
[relativeTo]="relativeTo"
[attr.aria-label]="ariaLabel || text"
routerLinkActive
[routerLinkActiveOptions]="routerLinkActiveOptions"
[ariaCurrentWhenActive]="'page'"
(isActiveChange)="setIsActive($event)"
(click)="mainContentClicked.emit()"
>
<ng-container *ngTemplateOutlet="anchorAndButtonContent"></ng-container>
</a>
</ng-template>
<!-- Show if `this.to` is falsy -->
<ng-template #isButton>
<!-- Class field should match `#isAnchor` class field above -->
<button
type="button"
class="tw-w-full tw-truncate tw-border-none tw-bg-transparent tw-p-0 tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none"
data-fvw
(click)="mainContentClicked.emit()"
>
<ng-container *ngTemplateOutlet="anchorAndButtonContent"></ng-container>
</button>
</ng-template>
<div
*ngIf="data.open"
class="tw-flex -tw-ms-3 tw-pe-4 tw-gap-1 [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2 empty:tw-hidden"
[ngClass]="[variant === 'tree' ? 'tw-py-1' : 'tw-py-2']"
>
<ng-content select="[slot=end]"></ng-content>
@if (open) {
<div
class="tw-flex tw-items-center tw-pe-1.5 tw-gap-1 [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2 empty:tw-hidden"
>
<ng-content select="[slot=end]"></ng-content>
</div>
}
</div>
</div>
</div>
</ng-container>
}
</div>

View File

@@ -1,23 +1,19 @@
@if (sideNavService.open) {
<div class="tw-sticky tw-top-0 tw-z-50">
<a
[routerLink]="route"
class="tw-px-5 tw-pb-5 tw-pt-7 tw-block tw-bg-background-alt3 tw-outline-none focus-visible:tw-ring focus-visible:tw-ring-inset focus-visible:tw-ring-text-alt2"
[attr.aria-label]="label"
[title]="label"
routerLinkActive
[ariaCurrentWhenActive]="'page'"
>
<bit-icon [icon]="openIcon"></bit-icon>
</a>
</div>
}
@if (!sideNavService.open) {
<bit-nav-item
class="tw-block tw-pt-7"
[hideActiveStyles]="true"
[route]="route"
[icon]="closedIcon"
[text]="label"
></bit-nav-item>
}
<div
[ngClass]="{
'tw-sticky tw-top-0 tw-z-50 tw-pb-2': sideNavService.open,
'tw-pb-5': !sideNavService.open,
}"
class="tw-px-2 tw-pt-5"
>
<a
[routerLink]="route"
class="tw-p-3 tw-block tw-rounded-md tw-bg-background-alt3 tw-outline-none focus-visible:tw-ring focus-visible:tw-ring-inset focus-visible:tw-ring-text-alt2 hover:tw-bg-hover-contrast [&_path]:tw-fill-text-alt2"
[ngClass]="{ '[&_svg]:tw-w-[1.687rem]': !sideNavService.open }"
[attr.aria-label]="label"
[title]="label"
routerLinkActive
[ariaCurrentWhenActive]="'page'"
>
<bit-icon [icon]="sideNavService.open ? openIcon : closedIcon"></bit-icon>
</a>
</div>

View File

@@ -1,23 +1,23 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { RouterLinkActive, RouterLink } from "@angular/router";
import { Icon } from "../icon";
import { BitIconComponent } from "../icon/icon.component";
import { BitwardenShield } from "../icon/logos";
import { NavItemComponent } from "./nav-item.component";
import { SideNavService } from "./side-nav.service";
@Component({
selector: "bit-nav-logo",
templateUrl: "./nav-logo.component.html",
imports: [RouterLinkActive, RouterLink, BitIconComponent, NavItemComponent],
imports: [CommonModule, RouterLinkActive, RouterLink, BitIconComponent],
})
export class NavLogoComponent {
/** Icon that is displayed when the side nav is closed */
@Input() closedIcon = "bwi-shield";
@Input() closedIcon = BitwardenShield;
/** Icon that is displayed when the side nav is open */
@Input({ required: true }) openIcon: Icon;

View File

@@ -14,6 +14,7 @@
'--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"