Skip to content

Commit 0cedcf1

Browse files
authored
fix: (CXSPA-9641) - header continuum tests improvements (#20137)
1 parent d4e9083 commit 0cedcf1

File tree

5 files changed

+123
-75
lines changed

5 files changed

+123
-75
lines changed

projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/header-and-footer.a11y-e2e.cy.ts

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,60 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import { viewportContext } from '../../helpers/viewport-context';
78
import { standardUser } from '../../sample-data/shared-users';
89

9-
describe('Header and Footer Continuum tests', () => {
10+
describe('Header and Footer Continuum tests', { testIsolation: false }, () => {
1011
beforeEach(() => {
1112
cy.a11yContinuumSetup();
12-
cy.visit('/');
13-
cy.get('[section="header"]').as('header');
14-
cy.get('[section="footer"]').as('footer');
13+
cy.clearLocalStorage();
14+
cy.clearCookies();
1515
});
16+
17+
context('Footer', () => {
18+
it('Main footer body', () => {
19+
cy.visit('/');
20+
cy.get('footer a');
21+
cy.get('footer').a11yRunContinuumTest();
22+
});
23+
24+
it('Consent Management dialog', () => {
25+
cy.get('footer').get('button').contains(' Consent Management').click();
26+
cy.get('.modal-dialog').contains(' Select all ').click();
27+
cy.get('.modal-dialog').a11yRunContinuumTest();
28+
cy.get('.close').first().click();
29+
});
30+
});
31+
1632
context('Header', () => {
1733
it('Main header body', () => {
18-
cy.get('@header').get('a').contains('Brands');
19-
cy.get('@header').a11yRunContinuumTest();
34+
cy.get('header').get('nav button[aria-label="Brands"]').click();
35+
cy.get('header').get('a').contains('Canon');
36+
cy.get('header').a11yRunContinuumTest();
2037
});
2138

2239
it('My Account dropdown', () => {
2340
cy.requireLoggedIn(standardUser);
2441
cy.reload();
25-
cy.get('@header')
26-
.get('nav[aria-label="My Account"]')
27-
.a11yRunContinuumTest();
42+
cy.get('header').get('.accNavComponent button').click();
43+
cy.get('nav a').contains(' Order History ');
44+
cy.get('nav[aria-label="My Account"]').a11yRunContinuumTest();
2845
});
29-
});
3046

31-
context('Footer', () => {
32-
it('Main footer body', () => {
33-
cy.get('@footer').a11yRunContinuumTest();
34-
});
47+
viewportContext(['mobile'], () => {
48+
it('Hamburger menu', () => {
49+
cy.get('cx-hamburger-menu button').click();
50+
cy.get('a').contains('Brands');
51+
cy.get('header').a11yRunContinuumTest();
3552

36-
it('Consent Management dialog', () => {
37-
cy.get('@footer').get('button').contains(' Consent Management').click();
38-
cy.get('.modal-dialog').contains(' Select all ').click();
39-
cy.get('.modal-dialog').a11yRunContinuumTest();
53+
cy.get('button[aria-label="Brands"]').click();
54+
cy.get('button').contains('Cameras');
55+
cy.get('nav[aria-label="Category menu"]').a11yRunContinuumTest();
56+
57+
cy.get('button').contains('Cameras').click();
58+
cy.get('a').contains('Canon');
59+
cy.get('nav[aria-label="Category menu"]').a11yRunContinuumTest();
60+
});
4061
});
4162
});
4263
});

projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.html

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -128,25 +128,34 @@
128128
</button>
129129
</ng-container>
130130
<ng-container *cxFeature="'a11yNavigationButtonsAriaFixes'">
131-
<button
132-
[attr.role]="(isDesktop$ | async) && depth ? 'heading' : 'button'"
133-
[attr.aria-haspopup]="true"
134-
[attr.aria-expanded]="false"
135-
[attr.aria-label]="getAriaLabelAndControl(node)"
136-
[attr.aria-controls]="getAriaLabelAndControl(node)"
137-
(click)="toggleOpen($any($event))"
138-
(mouseenter)="onMouseEnter($event)"
139-
(keydown.space)="onSpace($any($event))"
140-
(keydown.enter)="onSpace($any($event))"
141-
(keydown.esc)="back()"
131+
<h4
132+
*ngIf="(isDesktop$ | async) && depth; else dropdownHeader"
142133
(keydown.arrowDown)="focusOnNode($any($event))"
143-
(focus)="depth || reinitializeMenu()"
134+
(keydown.space)="onSpace($any($event))"
135+
tabindex="0"
144136
>
145-
<ng-container *ngIf="!node.url">
146-
{{ node.title }}
147-
</ng-container>
148-
<cx-icon [type]="iconType.CARET_DOWN"></cx-icon>
149-
</button>
137+
{{ node.title }}
138+
</h4>
139+
<ng-template #dropdownHeader>
140+
<button
141+
[attr.aria-haspopup]="true"
142+
[attr.aria-expanded]="false"
143+
[attr.aria-label]="node.url ? node.title : null"
144+
[attr.aria-controls]="transformIntoValidID(node.title)"
145+
(click)="toggleOpen($any($event))"
146+
(mouseenter)="onMouseEnter($event)"
147+
(keydown.space)="onSpace($any($event))"
148+
(keydown.enter)="onSpace($any($event))"
149+
(keydown.esc)="back()"
150+
(keydown.arrowDown)="focusOnNode($any($event))"
151+
(focus)="depth || reinitializeMenu()"
152+
>
153+
<ng-container *ngIf="!node.url">
154+
{{ node.title }}
155+
</ng-container>
156+
<cx-icon [type]="iconType.CARET_DOWN"></cx-icon>
157+
</button>
158+
</ng-template>
150159
</ng-container>
151160
</ng-container>
152161
</ng-container>
@@ -164,7 +173,7 @@
164173

165174
<!-- we add a wrapper to allow for better layout handling in CSS -->
166175
<div
167-
[id]="getSanitizedTitle(node.title)"
176+
[id]="transformIntoValidID(node.title)"
168177
class="wrapper"
169178
*ngIf="node.children && node.children.length > 0"
170179
>

projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.spec.ts

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -312,16 +312,17 @@ describe('Navigation UI Component', () => {
312312
spyOn(navigationComponent, 'closeIfClickedTheSameLink').and.callThrough();
313313
spyOn(navigationComponent, 'reinitializeMenu').and.callThrough();
314314
spyOn(hamburgerMenuService, 'toggle').and.stub();
315+
navigationComponent.isDesktop$ = of(false);
315316
fixture.detectChanges();
316317

317318
element
318319
.query(By.css('nav > ul > li:nth-child(2) > button'))
319320
.nativeElement.click();
320321
element
321-
.query(By.css('button[aria-controls="child-1"]'))
322+
.query(By.css('button[aria-controls="Child-1"]'))
322323
.nativeElement.click();
323324
element
324-
.query(By.css('button[aria-controls="sub-child-1"]'))
325+
.query(By.css('button[aria-controls="Sub-child-1"]'))
325326
.nativeElement.click();
326327

327328
expect(element.queryAll(By.css('li.is-open:not(.back)')).length).toBe(1);
@@ -363,17 +364,17 @@ describe('Navigation UI Component', () => {
363364
});
364365
});
365366

366-
it('should apply role="heading" to nested dropdown trigger button while on desktop', () => {
367+
it('on desktop, display headings for nested nodes instead of dropdown triggers', () => {
367368
fixture.detectChanges();
368-
const nestedTriggerButton = fixture.debugElement.query(
369-
By.css('button[aria-controls="child-1"]')
369+
const nestedNodeHeading = fixture.debugElement.query(
370+
By.css('#Root-1 h4')
370371
).nativeElement;
371372
const rootTriggerButton = fixture.debugElement.query(
372-
By.css('button[aria-controls="root-1"]')
373+
By.css('button[aria-controls="Root-1"]')
373374
).nativeElement;
374375

375-
expect(nestedTriggerButton.getAttribute('role')).toEqual('heading');
376-
expect(rootTriggerButton.getAttribute('role')).toEqual('button');
376+
expect(nestedNodeHeading.tagName).toEqual('H4');
377+
expect(rootTriggerButton.tagName).toEqual('BUTTON');
377378
});
378379
});
379380

@@ -386,7 +387,7 @@ describe('Navigation UI Component', () => {
386387
const spy = spyOn(navigationComponent, 'toggleOpen');
387388
const spaceEvent = new KeyboardEvent('keydown', { code: 'Space' });
388389
const dropDownButton = element.query(
389-
By.css('button[aria-controls="sub-child-1"]')
390+
By.css('nav button[aria-expanded="false"')
390391
).nativeElement;
391392
Object.defineProperty(spaceEvent, 'target', { value: dropDownButton });
392393

@@ -400,7 +401,7 @@ describe('Navigation UI Component', () => {
400401
const spy = spyOn(firstChild.nativeElement, 'focus');
401402
const spaceEvent = new KeyboardEvent('keydown', { code: 'Space' });
402403
const dropDownButton = element.query(
403-
By.css('button[aria-controls="sub-child-1"]')
404+
By.css('[depth="2"] h4')
404405
).nativeElement;
405406
Object.defineProperty(spaceEvent, 'target', { value: dropDownButton });
406407

@@ -421,7 +422,7 @@ describe('Navigation UI Component', () => {
421422
});
422423
const spaceEvent = new KeyboardEvent('keydown', { code: 'Space' });
423424
const dropDownButton = element.query(
424-
By.css('button[aria-controls="sub-child-1"]')
425+
By.css('[depth="2"] h4')
425426
).nativeElement;
426427
Object.defineProperty(spaceEvent, 'target', { value: dropDownButton });
427428
Object.defineProperty(arrowDownEvent, 'target', {
@@ -466,27 +467,19 @@ describe('Navigation UI Component', () => {
466467
}));
467468
});
468469

469-
describe('trigger buttions ariaLabel/title', () => {
470-
it('should have the ariaLabel and title set', () => {
471-
const rootNode = mockNode.children?.[0];
472-
const childNode = rootNode?.children?.[0];
473-
const rootTitle = rootNode?.title;
474-
const childTitle = childNode?.title;
475-
const sanitizedRootTitle =
476-
navigationComponent.getSanitizedTitle(rootTitle);
477-
const sanitizedChildTitle =
478-
navigationComponent.getSanitizedTitle(childTitle);
479-
480-
fixture.detectChanges();
481-
const nestedTriggerButton = fixture.debugElement.query(
482-
By.css(`button[aria-label="${sanitizedRootTitle}"]`)
483-
).nativeElement;
484-
const rootTriggerButton = fixture.debugElement.query(
485-
By.css(`button[aria-label="${sanitizedChildTitle}"]`)
486-
).nativeElement;
487-
488-
expect(nestedTriggerButton).toBeDefined();
489-
expect(rootTriggerButton).toBeDefined();
470+
describe('transformIntoValidID', () => {
471+
it('should replace invalid characters and provide a valid ID', () => {
472+
const invalidIDs = [
473+
{ input: 'Invalid ID', expected: 'Invalid-ID' },
474+
{ input: 'Inv@lid$Char!', expected: 'Inv-lid-Char-' },
475+
{ input: 'ValidId', expected: 'ValidId' },
476+
];
477+
478+
invalidIDs.forEach(({ input, expected }) => {
479+
expect(navigationComponent.transformIntoValidID(input)).toEqual(
480+
expected
481+
);
482+
});
490483
});
491484
});
492485
});

projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export class NavigationUIComponent implements OnInit, OnDestroy {
119119
})
120120
);
121121
useFeatureStyles('a11yOptimizedMenuSpacing');
122+
useFeatureStyles('a11yNavigationButtonsAriaFixes');
122123
}
123124

124125
/**
@@ -288,10 +289,20 @@ export class NavigationUIComponent implements OnInit, OnDestroy {
288289
* Focuses on the first focusable element in the dropdown
289290
*/
290291
focusOnNode(event: UIEvent): void {
291-
const firstFocusableElement =
292-
(<HTMLElement>event.target).nextElementSibling?.querySelector('button') ||
293-
(<HTMLElement>event.target).nextElementSibling?.querySelector('a');
294-
firstFocusableElement?.focus();
292+
if (
293+
this.featureConfigService?.isEnabled('a11yNavigationButtonsAriaFixes')
294+
) {
295+
const firstFocusableNode = (<HTMLElement>(
296+
event.target
297+
))?.nextElementSibling?.querySelector('button, h4, a') as HTMLElement;
298+
firstFocusableNode?.focus();
299+
} else {
300+
const firstFocusableElement =
301+
(<HTMLElement>event.target).nextElementSibling?.querySelector(
302+
'button'
303+
) || (<HTMLElement>event.target).nextElementSibling?.querySelector('a');
304+
firstFocusableElement?.focus();
305+
}
295306
}
296307

297308
back(): void {
@@ -409,7 +420,7 @@ export class NavigationUIComponent implements OnInit, OnDestroy {
409420
}
410421

411422
/**
412-
* Resores default tabbing order for non flyout navigation.
423+
* Restores default tabbing order for non flyout navigation.
413424
*/
414425
getTabIndex(node: NavigationNode, depth: number): 0 | -1 {
415426
if (!this.flyout) {
@@ -418,17 +429,23 @@ export class NavigationUIComponent implements OnInit, OnDestroy {
418429
return depth > 0 && !node?.children ? -1 : 0;
419430
}
420431

432+
// TODO: Delete deprecated methods once `a11yNavigationButtonsAriaFixes` feature flag is removed.
421433
/**
422-
* // Replace spaces with hyphens and convert to lowercase
434+
* Replace spaces with hyphens and convert to lowercase
435+
* @deprecated
423436
*/
424437
getSanitizedTitle(title: string | undefined): string | null {
425438
return title ? title.replace(/\s+/g, '-').toLowerCase() : null;
426439
}
427-
428440
/**
429441
* Returns the value for the `aria-control` and the `aria-label` attribute of a button.
442+
* @deprecated
430443
*/
431444
getAriaLabelAndControl(node: NavigationNode): string | null {
432445
return this.getSanitizedTitle(node.title) || null;
433446
}
447+
448+
transformIntoValidID(string: string): string | null {
449+
return string?.replace(/[^a-zA-Z0-9-_]/g, '-') || null;
450+
}
434451
}

projects/storefrontstyles/scss/components/content/navigation/_category-navigation.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99
width: 100%;
1010
}
1111

12+
@include forFeature('a11yNavigationButtonsAriaFixes') {
13+
h4 {
14+
text-transform: uppercase;
15+
font-weight: 600;
16+
width: 100%;
17+
}
18+
}
19+
1220
li {
1321
list-style: none;
1422
}

0 commit comments

Comments
 (0)