diff --git a/libs/components/src/navigation/nav-group.component.ts b/libs/components/src/navigation/nav-group.component.ts index 868f3e1dd8..3e62823e21 100644 --- a/libs/components/src/navigation/nav-group.component.ts +++ b/libs/components/src/navigation/nav-group.component.ts @@ -3,29 +3,25 @@ import { Component, ContentChildren, EventEmitter, - forwardRef, Input, + Optional, Output, QueryList, + SkipSelf, } from "@angular/core"; import { NavBaseComponent } from "./nav-base.component"; -import { NavItemComponent } from "./nav-item.component"; @Component({ selector: "bit-nav-group", templateUrl: "./nav-group.component.html", + providers: [{ provide: NavBaseComponent, useExisting: NavGroupComponent }], }) export class NavGroupComponent extends NavBaseComponent implements AfterContentInit { - @ContentChildren(forwardRef(() => NavGroupComponent), { + @ContentChildren(NavBaseComponent, { descendants: true, }) - nestedGroups!: QueryList; - - @ContentChildren(NavItemComponent, { - descendants: true, - }) - nestedItems!: QueryList; + nestedNavComponents!: QueryList; /** The parent nav item should not show active styles when open. */ protected get parentHideActiveStyles(): boolean { @@ -51,10 +47,19 @@ export class NavGroupComponent extends NavBaseComponent implements AfterContentI @Output() openChange = new EventEmitter(); + constructor(@Optional() @SkipSelf() private parentNavGroup: NavGroupComponent) { + super(); + } + + setOpen(isOpen: boolean) { + this.open = isOpen; + this.openChange.emit(this.open); + this.open && this.parentNavGroup?.setOpen(this.open); + } + protected toggle(event?: MouseEvent) { event?.stopPropagation(); - this.open = !this.open; - this.openChange.emit(this.open); + this.setOpen(!this.open); } /** @@ -64,7 +69,7 @@ export class NavGroupComponent extends NavBaseComponent implements AfterContentI if (this.variant !== "tree") { return; } - [...this.nestedGroups, ...this.nestedItems].forEach((navGroupOrItem) => { + [...this.nestedNavComponents].forEach((navGroupOrItem) => { navGroupOrItem.treeDepth += 1; }); } diff --git a/libs/components/src/navigation/nav-group.stories.ts b/libs/components/src/navigation/nav-group.stories.ts index 9e99ccbd06..15bee43d55 100644 --- a/libs/components/src/navigation/nav-group.stories.ts +++ b/libs/components/src/navigation/nav-group.stories.ts @@ -1,5 +1,6 @@ -import { RouterTestingModule } from "@angular/router/testing"; -import { StoryObj, Meta, moduleMetadata } from "@storybook/angular"; +import { Component, importProvidersFrom } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { StoryObj, Meta, moduleMetadata, applicationConfig } from "@storybook/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -9,12 +10,18 @@ import { I18nMockService } from "../utils/i18n-mock.service"; import { NavGroupComponent } from "./nav-group.component"; import { NavigationModule } from "./navigation.module"; +@Component({ + standalone: true, + template: "", +}) +class DummyContentComponent {} + export default { title: "Component Library/Nav/Nav Group", component: NavGroupComponent, decorators: [ moduleMetadata({ - imports: [SharedModule, RouterTestingModule, NavigationModule], + imports: [SharedModule, RouterModule, NavigationModule, DummyContentComponent], providers: [ { provide: I18nService, @@ -27,6 +34,19 @@ export default { }, ], }), + applicationConfig({ + providers: [ + importProvidersFrom( + RouterModule.forRoot( + [ + { path: "", redirectTo: "a", pathMatch: "full" }, + { path: "**", component: DummyContentComponent }, + ], + { useHash: true }, + ), + ), + ], + }), ], parameters: { design: { @@ -40,10 +60,10 @@ export const Default: StoryObj = { render: (args) => ({ props: args, template: ` - - - - + + + + @@ -59,19 +79,19 @@ export const Tree: StoryObj = { props: args, template: ` - - - - - - - + + + + + + + - - + + - + `, }), diff --git a/libs/components/src/navigation/nav-item.component.html b/libs/components/src/navigation/nav-item.component.html index 9b66d11d5e..5c2b3718e2 100644 --- a/libs/components/src/navigation/nav-item.component.html +++ b/libs/components/src/navigation/nav-item.component.html @@ -55,7 +55,7 @@ routerLinkActive [routerLinkActiveOptions]="rlaOptions" [ariaCurrentWhenActive]="'page'" - (isActiveChange)="setActive($event)" + (isActiveChange)="setIsActive($event)" (click)="mainContentClicked.emit()" > diff --git a/libs/components/src/navigation/nav-item.component.ts b/libs/components/src/navigation/nav-item.component.ts index bce13e097d..83ef1a7b3b 100644 --- a/libs/components/src/navigation/nav-item.component.ts +++ b/libs/components/src/navigation/nav-item.component.ts @@ -1,23 +1,28 @@ -import { Component, HostListener, Input } from "@angular/core"; +import { Component, HostListener, Input, Optional } from "@angular/core"; import { IsActiveMatchOptions } from "@angular/router"; import { BehaviorSubject, map } from "rxjs"; import { NavBaseComponent } from "./nav-base.component"; +import { NavGroupComponent } from "./nav-group.component"; @Component({ selector: "bit-nav-item", templateUrl: "./nav-item.component.html", + providers: [{ provide: NavBaseComponent, useExisting: NavItemComponent }], }) export class NavItemComponent extends NavBaseComponent { /** * Is `true` if `to` matches the current route */ - private _active = false; - protected setActive(isActive: boolean) { - this._active = isActive; + private _isActive = false; + protected setIsActive(isActive: boolean) { + this._isActive = isActive; + if (this._isActive && this.parentNavGroup) { + this.parentNavGroup.setOpen(true); + } } protected get showActiveStyles() { - return this._active && !this.hideActiveStyles; + return this._isActive && !this.hideActiveStyles; } protected rlaOptions: IsActiveMatchOptions = { paths: "subset", @@ -54,4 +59,8 @@ export class NavItemComponent extends NavBaseComponent { onFocusOut() { this.focusVisibleWithin$.next(false); } + + constructor(@Optional() private parentNavGroup: NavGroupComponent) { + super(); + } }