diff --git a/projects/storefrontlib/cms-components/product/product-list/product-facet-navigation/facet/facet.component.ts b/projects/storefrontlib/cms-components/product/product-list/product-facet-navigation/facet/facet.component.ts index 27c8c75bddf..892caa6881c 100644 --- a/projects/storefrontlib/cms-components/product/product-list/product-facet-navigation/facet/facet.component.ts +++ b/projects/storefrontlib/cms-components/product/product-list/product-facet-navigation/facet/facet.component.ts @@ -21,7 +21,10 @@ import { import { Facet, FacetValue, FeatureConfigService } from '@spartacus/core'; import { Observable } from 'rxjs'; import { ICON_TYPE } from '../../../../../cms-components/misc/icon/icon.model'; -import { FocusDirective } from '../../../../../layout/a11y/keyboard-focus/focus.directive'; +import { + FocusDirective, + disableTabbingForTick, +} from '../../../../../layout/a11y'; import { FacetCollapseState } from '../facet.model'; import { FacetService } from '../services/facet.service'; @@ -153,7 +156,21 @@ export class FacetComponent implements AfterViewInit { case 'ArrowUp': this.onArrowUp(event, targetIndex); break; + case 'Tab': + this.onTabNavigation(); + break; + } + } + + /** + * If a11yTabComponent is enabled, we temporarily disable tabbing for the facet values. + * This is to use proper keyboard navigation keys(ArrowUp/ArrowDown) for navigating through the facet values. + */ + protected onTabNavigation(): void { + if (!this.featureConfigService?.isEnabled('a11yTabComponent')) { + return; } + disableTabbingForTick(this.values.map((el) => el.nativeElement)); } /** diff --git a/projects/storefrontlib/layout/a11y/keyboard-focus/index.ts b/projects/storefrontlib/layout/a11y/keyboard-focus/index.ts index a82dc7fcd75..6e6cdbdacfa 100644 --- a/projects/storefrontlib/layout/a11y/keyboard-focus/index.ts +++ b/projects/storefrontlib/layout/a11y/keyboard-focus/index.ts @@ -11,6 +11,7 @@ export { FocusConfig, TrapFocus, TrapFocusType } from './keyboard-focus.model'; export * from './keyboard-focus.module'; export * from './focus-testing.module'; export * from './services/index'; +export * from './keyboard-focus.utils'; // export * from './autofocus/index'; // export * from './base/index'; diff --git a/projects/storefrontlib/layout/a11y/keyboard-focus/keyboard-focus.utils.spec.ts b/projects/storefrontlib/layout/a11y/keyboard-focus/keyboard-focus.utils.spec.ts new file mode 100644 index 00000000000..96d7b8a47e6 --- /dev/null +++ b/projects/storefrontlib/layout/a11y/keyboard-focus/keyboard-focus.utils.spec.ts @@ -0,0 +1,30 @@ +import { disableTabbingForTick } from './keyboard-focus.utils'; +import { fakeAsync, tick } from '@angular/core/testing'; + +describe('disableTabbingForTick', () => { + let elements: HTMLElement[]; + + beforeEach(() => { + elements = [document.createElement('div'), document.createElement('div')]; + elements.forEach((el) => document.body.appendChild(el)); + }); + + afterEach(() => { + elements.forEach((el) => document.body.removeChild(el)); + }); + + it('should set tabIndex to -1 for each element', () => { + disableTabbingForTick(elements); + elements.forEach((el) => { + expect(el.tabIndex).toBe(-1); + }); + }); + + it('should reset tabIndex to 0 after a tick', fakeAsync(() => { + disableTabbingForTick(elements); + tick(100); + elements.forEach((el) => { + expect(el.tabIndex).toBe(0); + }); + })); +}); diff --git a/projects/storefrontlib/layout/a11y/keyboard-focus/keyboard-focus.utils.ts b/projects/storefrontlib/layout/a11y/keyboard-focus/keyboard-focus.utils.ts new file mode 100644 index 00000000000..182ba966fa2 --- /dev/null +++ b/projects/storefrontlib/layout/a11y/keyboard-focus/keyboard-focus.utils.ts @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Temporarily removes elements from the tabbing flow and restores them after a tick. + * + * This method sets the `tabIndex` of each element in the provided iterable to `-1` + * and resets it back to `0` using `requestAnimationFrame`. While using `requestAnimationFrame` + * may seem like a bad code smell, it is justified here as it ensures a natural tabbing flow + * in cases where determining the next focusable element is complex, such as when directives + * like `TrapFocusDirective` modify the DOM's focus behavior. + * + * This utility is especially useful for scenarios like menus, lists, or carousels where + * `Tab` navigation is intentionally disabled, but other keyboard keys (e.g., `Arrow` keys) + * are used for navigation. It helps prevent these elements from disrupting the tab order + * while allowing other key-based interactions. + * + * @param elements - An iterable of `HTMLElement` objects to temporarily remove from tab navigation. + */ +export const disableTabbingForTick = (elements: Iterable) => { + for (const element of elements) { + element.tabIndex = -1; + } + requestAnimationFrame(() => { + for (const element of elements) { + element.tabIndex = 0; + } + }); +}; diff --git a/projects/storefrontlib/shared/components/carousel/carousel.component.ts b/projects/storefrontlib/shared/components/carousel/carousel.component.ts index 005dcc1053d..746d2ad104e 100644 --- a/projects/storefrontlib/shared/components/carousel/carousel.component.ts +++ b/projects/storefrontlib/shared/components/carousel/carousel.component.ts @@ -21,6 +21,7 @@ import { BehaviorSubject, Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { ICON_TYPE } from '../../../cms-components/misc/icon/icon.model'; import { CarouselService } from './carousel.service'; +import { disableTabbingForTick } from '../../../layout/a11y'; /** * Generic carousel component that can be used to render any carousel items, @@ -128,13 +129,8 @@ export class CarouselComponent implements OnInit, OnChanges { } /** - * Handles "Tab" navigation within the carousel. - * - * Temporarily removes all `cxFocusableCarouselItem` elements from the tab flow - * and restores them after a short delay. While using `requestAnimationFrame` may seem like - * a bad code smell, it is justified here as it ensures natural tabbing flow in - * cases where determining the next focusable element is complex(e.g. if `TrapFocusDirective` is used). - * + * Handles Tab key on carousel items. If the carousel items have `ArrowRight`/`ArrowLeft` + * navigation enabled, it temporarily disables tab navigation for these items. * The `cxFocusableCarouselItem` selector is used because it identifies carousel * items that have `ArrowRight`/`ArrowLeft` navigation enabled. These items should not * use tab navigation according to a11y requirements. @@ -146,14 +142,7 @@ export class CarouselComponent implements OnInit, OnChanges { if (!carouselElements.length) { return; } - carouselElements.forEach((element) => { - element.tabIndex = -1; - }); - requestAnimationFrame(() => { - carouselElements.forEach((element) => { - element.tabIndex = 0; - }); - }); + disableTabbingForTick(carouselElements); } /**