mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
When a sidebar item links to a page in a different category (e.g., Route
transition animations under Animations links to a Routing page), clicking
back navigates to the main menu instead of the originating category.
Store the originating category in NavigationState when clicking a
cross-referenced item, so the back button returns to the correct section.
(cherry picked from commit 8132a96884)
130 lines
4.4 KiB
TypeScript
130 lines
4.4 KiB
TypeScript
/*!
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import {Injectable, inject, linkedSignal, signal} from '@angular/core';
|
|
import {NavigationItem} from '../interfaces/index';
|
|
import {Router} from '@angular/router';
|
|
|
|
@Injectable({
|
|
providedIn: 'root',
|
|
})
|
|
export class NavigationState {
|
|
private readonly router = inject(Router);
|
|
|
|
private readonly _activeNavigationItem = signal<NavigationItem | null>(null);
|
|
private readonly _expandedItems = signal<NavigationItem[]>([]);
|
|
private readonly _isMobileNavVisible = signal<boolean>(false);
|
|
private readonly _level = linkedSignal(() => this._expandedItems().length);
|
|
|
|
readonly crossCategoryOrigin = signal<NavigationItem | undefined>(undefined);
|
|
readonly primaryActiveRouteItem = signal<string | null>(null);
|
|
activeNavigationItem = this._activeNavigationItem.asReadonly();
|
|
expandedItems = this._expandedItems.asReadonly();
|
|
isMobileNavVisible = this._isMobileNavVisible.asReadonly();
|
|
level = this._level.asReadonly();
|
|
|
|
async toggleItem(item: NavigationItem): Promise<void> {
|
|
if (!item.children) {
|
|
return;
|
|
}
|
|
|
|
if (item.isExpanded) {
|
|
this.collapse(item);
|
|
} else if (item.children && item.children.length > 0 && item.children[0].path) {
|
|
// It resolves false, when the user has displayed the page, then changed the slide to a secondary navigation component
|
|
// and wants to reopen the slide, where the first item is the currently displayed page
|
|
const navigationSucceeds = await this.navigateToFirstPageOfTheCategory(item.children[0].path);
|
|
|
|
if (!navigationSucceeds) {
|
|
this.expand(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
cleanExpandedState(): void {
|
|
this._expandedItems.set([]);
|
|
}
|
|
|
|
expandItemHierarchy(
|
|
item: NavigationItem,
|
|
shouldExpand: (item: NavigationItem) => boolean,
|
|
skipExpandPredicateFn?: (item: NavigationItem) => boolean,
|
|
): void {
|
|
if (skipExpandPredicateFn && skipExpandPredicateFn(item)) {
|
|
// When `skipExpandPredicateFn` returns `true` then we should trigger `cleanExpandedState`
|
|
// to be sure that first navigation slide will be displayed.
|
|
this.cleanExpandedState();
|
|
return;
|
|
}
|
|
// Returns item when parent node was already expanded
|
|
const parentItem = this.actualExpandedItems().find(
|
|
(expandedItem) =>
|
|
item.parent?.label === expandedItem.label && item.parent?.path === expandedItem.path,
|
|
);
|
|
|
|
if (parentItem) {
|
|
// If the parent item is expanded, then we should display all expanded items up to the active item level.
|
|
// This provides us with an appropriate list of expanded elements also when the user navigates using browser buttons.
|
|
this._expandedItems.update((expandedItems) =>
|
|
expandedItems.filter(
|
|
(item) =>
|
|
item.level !== undefined &&
|
|
parentItem.level !== undefined &&
|
|
item.level <= parentItem.level,
|
|
),
|
|
);
|
|
} else {
|
|
let itemsToExpand: NavigationItem[] = [];
|
|
|
|
let node = item.parent;
|
|
|
|
while (node && shouldExpand(node)) {
|
|
itemsToExpand.push({...node, isExpanded: true});
|
|
node = node.parent;
|
|
}
|
|
|
|
this._expandedItems.set(itemsToExpand.reverse());
|
|
}
|
|
}
|
|
|
|
setActiveNavigationItem(item: NavigationItem | null): void {
|
|
this._activeNavigationItem.set(item);
|
|
}
|
|
|
|
setMobileNavigationListVisibility(isVisible: boolean): void {
|
|
this._isMobileNavVisible.set(isVisible);
|
|
}
|
|
|
|
private expand(item: NavigationItem): void {
|
|
// Add item to the expanded items list
|
|
this._expandedItems.update(() => {
|
|
return [...(this.actualExpandedItems() ?? []), {...item, isExpanded: true}];
|
|
});
|
|
|
|
// No need to update the level here, this is handled by linkedSignal already
|
|
}
|
|
|
|
private collapse(item: NavigationItem): void {
|
|
item.isExpanded = false;
|
|
|
|
// We won't remove the item, just update the level,
|
|
// this allows animation on the items to hide them without destroying them
|
|
this._level.set(this.actualExpandedItems().length - 1);
|
|
}
|
|
|
|
/**
|
|
* return the actual navigation items, that is to say the one that match the current level
|
|
*/
|
|
private actualExpandedItems(): NavigationItem[] {
|
|
return this.expandedItems().slice(0, this.level());
|
|
}
|
|
|
|
private async navigateToFirstPageOfTheCategory(path: string): Promise<boolean> {
|
|
return this.router.navigateByUrl(path);
|
|
}
|
|
}
|