Skip to content

Commit

Permalink
fix(a11y): Focus on first navigation item when mobile menu expands (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
sdrozdsap authored Oct 1, 2024
1 parent 3354a79 commit 7a927f7
Show file tree
Hide file tree
Showing 5 changed files with 35 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,11 @@ export interface FeatureTogglesInterface {
*/
a11yStoreFinderOverflow?: boolean;

/**
* `StorefrontComponent` focuses on the first navigation item after hamburger menu expansion
*/
a11yMobileFocusOnFirstNavigationItem?: boolean;

/**
* Corrects heading order inside 'OrderSummaryComponent' template.
*/
Expand Down Expand Up @@ -647,6 +652,7 @@ export const defaultFeatureToggles: Required<FeatureTogglesInterface> = {
a11yReplenishmentOrderFieldset: false,
a11yListOversizedFocus: false,
a11yStoreFinderOverflow: false,
a11yMobileFocusOnFirstNavigationItem: false,
a11yCartSummaryHeadingOrder: false,
a11ySearchBoxMobileFocus: false,
a11yFacetKeyboardNavigation: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ if (environment.cpq) {
a11yReplenishmentOrderFieldset: true,
a11yListOversizedFocus: true,
a11yStoreFinderOverflow: true,
a11yMobileFocusOnFirstNavigationItem: true,
a11yCartSummaryHeadingOrder: true,
a11ySearchBoxMobileFocus: true,
a11yFacetKeyboardNavigation: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,18 +448,6 @@ describe('Navigation UI Component', () => {
expect(navigationComponent.getTabIndex(childNode, 1)).toEqual(0);
});

it('should focus on the first focusable element when the hamburger menu is expanded', fakeAsync(() => {
const firstFocusableElement =
element.nativeElement.querySelector('[tabindex="0"]');
spyOn(firstFocusableElement, 'focus');
navigationComponent.navAriaLabel = 'menu';

navigationComponent.focusOnMenuExpansion();
tick();

expect(firstFocusableElement.focus).toHaveBeenCalled();
}));

it('return focus to node header after navigating back', fakeAsync(() => {
const mockNode = document.createElement('li');
const mockHeader = document.createElement('a');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,26 +117,6 @@ export class NavigationUIComponent implements OnInit, OnDestroy {
if (this.resetMenuOnClose) {
this.resetOnMenuCollapse();
}
if (
this.featureConfigService?.isEnabled('a11yNavigationUiKeyboardControls')
) {
this.focusOnMenuExpansion();
}
}

/**
* Focus on the first focusable element in the hamburger menu when the menu is opened.
*/
focusOnMenuExpansion(): void {
this.subscriptions.add(
this.hamburgerMenuService?.isExpanded.subscribe((isExpanded) => {
if (isExpanded && this.navAriaLabel?.includes('menu')) {
setTimeout(() => {
this.elemRef.nativeElement.querySelector('[tabindex="0"]').focus();
});
}
})
);
}

/**
Expand Down
35 changes: 28 additions & 7 deletions projects/storefrontlib/layout/main/storefront.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
HostListener,
OnDestroy,
OnInit,
Optional,
ViewChild,
inject,
} from '@angular/core';
Expand All @@ -20,7 +19,7 @@ import {
RoutingService,
useFeatureStyles,
} from '@spartacus/core';
import { Observable, Subscription } from 'rxjs';
import { Observable, Subscription, tap } from 'rxjs';
import {
FocusConfig,
KeyboardFocusService,
Expand All @@ -39,22 +38,20 @@ export class StorefrontComponent implements OnInit, OnDestroy {

readonly StorefrontOutlets = StorefrontOutlets;

@Optional() featureConfigService = inject(FeatureConfigService, {
optional: true,
});
private featureConfigService = inject(FeatureConfigService);

@HostBinding('class.start-navigating') startNavigating: boolean;
@HostBinding('class.stop-navigating') stopNavigating: boolean;

// TODO: (CXSPA-7464) - Remove feature flags and following bindings next major release.
@HostBinding('attr.role') role = this?.featureConfigService?.isEnabled(
@HostBinding('attr.role') role = this?.featureConfigService.isEnabled(
'a11yScreenReaderBloatFix'
)
? null
: 'presentation';

// required by esc focus
@HostBinding('tabindex') tabindex = this?.featureConfigService?.isEnabled(
@HostBinding('tabindex') tabindex = this?.featureConfigService.isEnabled(
'a11yScreenReaderBloatFix'
)
? '-1'
Expand Down Expand Up @@ -93,6 +90,19 @@ export class StorefrontComponent implements OnInit, OnDestroy {
this.startNavigating = val === true;
this.stopNavigating = val === false;
});
if (
this.featureConfigService.isEnabled(
'a11yMobileFocusOnFirstNavigationItem'
)
) {
this.isExpanded$ = this.hamburgerMenuService.isExpanded.pipe(
tap((isExpanded) => {
if (isExpanded) {
this.focusOnFirstNavigationItem();
}
})
);
}
}

collapseMenuIfClickOutside(event: any): void {
Expand All @@ -109,6 +119,17 @@ export class StorefrontComponent implements OnInit, OnDestroy {
this.hamburgerMenuService.toggle(true);
}

protected focusOnFirstNavigationItem() {
const closestNavigationUi =
this.elementRef.nativeElement.querySelector('cx-navigation-ui');
const focusable = closestNavigationUi?.querySelector<HTMLElement>(
'li:not(.back) button, [tabindex="0"]'
);
if (focusable) {
setTimeout(() => focusable.focus());
}
}

ngOnDestroy(): void {
if (this.navigateSubscription) {
this.navigateSubscription.unsubscribe();
Expand Down

0 comments on commit 7a927f7

Please sign in to comment.