diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index 51d45448ebe..ca3ac570fa7 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -811,6 +811,12 @@ export interface FeatureTogglesInterface { */ a11yAddPaddingToCarouselPanel?: boolean; + /** + * Removes invalid aria-level usage on button elements and ensures buttons have a proper accessible name via aria-label or aria-labelledby. + * Affects: NavigationUIComponent + */ + a11yNavigationButtonsAriaFixes?: boolean; + /** * Restores the focus to the card once a option has been selected and the checkout has updated. * Affects: CheckoutPaymentMethodComponent, CheckoutDeliveryAddressComponent @@ -1114,6 +1120,7 @@ export const defaultFeatureToggles: Required = { a11yQuickOrderSearchBoxRefocusOnClose: false, a11yKeyboardFocusInSearchBox: false, a11yAddPaddingToCarouselPanel: false, + a11yNavigationButtonsAriaFixes: false, a11yFocusOnCardAfterSelecting: false, a11ySearchableDropdownFirstElementFocus: false, a11yHideConsentButtonWhenBannerVisible: false, diff --git a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts index ab1c1fa6c9b..9623bf6507b 100644 --- a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts +++ b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts @@ -424,6 +424,7 @@ if (environment.cpq) { a11yQuickOrderSearchBoxRefocusOnClose: true, a11yKeyboardFocusInSearchBox: true, a11yAddPaddingToCarouselPanel: true, + a11yNavigationButtonsAriaFixes: true, a11yFocusOnCardAfterSelecting: true, a11ySearchableDropdownFirstElementFocus: true, a11yHideConsentButtonWhenBannerVisible: true, diff --git a/projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.html b/projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.html index eb8c9e4a790..0eb6be824c6 100644 --- a/projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.html +++ b/projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.html @@ -105,29 +105,49 @@ - + + + + + + @@ -144,7 +164,7 @@
diff --git a/projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.spec.ts b/projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.spec.ts index 790a9e51c70..7513b158589 100644 --- a/projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.spec.ts +++ b/projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.spec.ts @@ -317,10 +317,10 @@ describe('Navigation UI Component', () => { .query(By.css('nav > ul > li:nth-child(2) > button')) .nativeElement.click(); element - .query(By.css('button[aria-controls="Child 1"]')) + .query(By.css('button[aria-controls="child-1"]')) .nativeElement.click(); element - .query(By.css('button[aria-controls="Sub child 1"]')) + .query(By.css('button[aria-controls="sub-child-1"]')) .nativeElement.click(); expect(element.queryAll(By.css('li.is-open:not(.back)')).length).toBe(1); @@ -365,10 +365,10 @@ describe('Navigation UI Component', () => { it('should apply role="heading" to nested dropdown trigger button while on desktop', () => { fixture.detectChanges(); const nestedTriggerButton = fixture.debugElement.query( - By.css('button[aria-controls="Child 1"]') + By.css('button[aria-controls="child-1"]') ).nativeElement; const rootTriggerButton = fixture.debugElement.query( - By.css('button[aria-controls="Root 1"]') + By.css('button[aria-controls="root-1"]') ).nativeElement; expect(nestedTriggerButton.getAttribute('role')).toEqual('heading'); @@ -385,7 +385,7 @@ describe('Navigation UI Component', () => { const spy = spyOn(navigationComponent, 'toggleOpen'); const spaceEvent = new KeyboardEvent('keydown', { code: 'Space' }); const dropDownButton = element.query( - By.css('button[aria-controls="Sub child 1"]') + By.css('button[aria-controls="sub-child-1"]') ).nativeElement; Object.defineProperty(spaceEvent, 'target', { value: dropDownButton }); @@ -399,7 +399,7 @@ describe('Navigation UI Component', () => { const spy = spyOn(firstChild.nativeElement, 'focus'); const spaceEvent = new KeyboardEvent('keydown', { code: 'Space' }); const dropDownButton = element.query( - By.css('button[aria-controls="Sub child 1"]') + By.css('button[aria-controls="sub-child-1"]') ).nativeElement; Object.defineProperty(spaceEvent, 'target', { value: dropDownButton }); @@ -420,7 +420,7 @@ describe('Navigation UI Component', () => { }); const spaceEvent = new KeyboardEvent('keydown', { code: 'Space' }); const dropDownButton = element.query( - By.css('button[aria-controls="Sub child 1"]') + By.css('button[aria-controls="sub-child-1"]') ).nativeElement; Object.defineProperty(spaceEvent, 'target', { value: dropDownButton }); Object.defineProperty(arrowDownEvent, 'target', { @@ -471,22 +471,21 @@ describe('Navigation UI Component', () => { const childNode = rootNode?.children?.[0]; const rootTitle = rootNode?.title; const childTitle = childNode?.title; + const sanitizedRootTitle = + navigationComponent.getSanitizedTitle(rootTitle); + const sanitizedChildTitle = + navigationComponent.getSanitizedTitle(childTitle); + fixture.detectChanges(); const nestedTriggerButton = fixture.debugElement.query( - By.css(`button[aria-label="${childTitle}"]`) + By.css(`button[aria-label="${sanitizedRootTitle}"]`) ).nativeElement; const rootTriggerButton = fixture.debugElement.query( - By.css(`button[aria-label="${rootTitle}"]`) + By.css(`button[aria-label="${sanitizedChildTitle}"]`) ).nativeElement; expect(nestedTriggerButton).toBeDefined(); expect(rootTriggerButton).toBeDefined(); - expect(rootTriggerButton.getAttribute('title')).toEqual( - `navigation.menuButonTitle title:${rootTitle}` - ); - expect(nestedTriggerButton.getAttribute('title')).toEqual( - `navigation.menuButonTitle title:${childTitle}` - ); }); }); }); diff --git a/projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.ts b/projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.ts index 893a4c9072d..e4eca55899a 100644 --- a/projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.ts +++ b/projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.ts @@ -397,4 +397,18 @@ export class NavigationUIComponent implements OnInit, OnDestroy { } return depth > 0 && !node?.children ? -1 : 0; } + + /** + * // Replace spaces with hyphens and convert to lowercase + */ + getSanitizedTitle(title: string | undefined): string | null { + return title ? title.replace(/\s+/g, '-').toLowerCase() : null; + } + + /** + * Returns the value for the `aria-control` and the `aria-label` attribute of a button. + */ + getAriaLabelAndControl(node: NavigationNode): string | null { + return this.getSanitizedTitle(node.title) || null; + } }