/// /// Copyright 2015-2026 Micro Focus or one of its affiliates. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// import { FocusKeyManager } from '@angular/cdk/a11y'; import { Injectable, OnDestroy, QueryList } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; import { tick } from '../../../common/operators/index'; import { PageHeaderNavigationItemComponent } from './navigation-item/navigation-item.component'; @Injectable() export class PageHeaderNavigationService implements OnDestroy { /** Store an instance of the focus key manager */ private _focusManager: FocusKeyManager; /** Store the query list */ private _items: QueryList; /** * Emit when focus changes. We can't directly use the FocusKeyManager * `change` observable as it cannot be instantiate until after the view * has been instantiated. */ private readonly _onChange = new Subject(); /** Unsubscribe on destroy */ private readonly _onDestroy = new Subject(); ngOnDestroy(): void { this._onDestroy.next(); this._onDestroy.complete(); } /** Make menu items navigable using arrow keys */ initialize(items: QueryList): void { // store the query list for future lookups this._items = items; // create new focus key manager with horizontal orientation this._focusManager = new FocusKeyManager( items ).withHorizontalOrientation('ltr'); // listen for changes to the focused item this._focusManager.change .pipe(takeUntil(this._onDestroy)) .subscribe(() => this._onChange.next()); // make the first item tabbable initially this._focusManager.updateActiveItem(0); // on changes ensure there is always an active item this._items.changes.subscribe(() => { if ( this._items.length > 0 && this._items.toArray().indexOf(this._focusManager.activeItem) === -1 ) { this._focusManager.updateActiveItem(0); } }); // emit the initial change this._onChange.next(); } /** Listen for keyboard events */ onKeydown(event: KeyboardEvent): void { this._focusManager.onKeydown(event); } /** Get the tab index for this item as an observable */ getTabIndex(item: PageHeaderNavigationItemComponent): Observable { return this._onChange.pipe( map(() => this.getItemTabIndex(item)), tick(), takeUntil(this._onDestroy) ); } /** Determine the tab index of a given item */ private getItemTabIndex(item: PageHeaderNavigationItemComponent): number { // until the focus key manager is set up make everything tabbable if (!this._items) { return 0; } // get the index within the query list const index = this._items.toArray().indexOf(item); // if it is the current active element then it is tabbable return index === this._focusManager.activeItemIndex ? 0 : -1; } }