Skip to content

Commit

Permalink
feat: Add a11y tab component to plp facets (#19244)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zeyber authored and ulates-sap committed Nov 7, 2024
1 parent 6f200cd commit 0e52b2e
Show file tree
Hide file tree
Showing 19 changed files with 432 additions and 125 deletions.
3 changes: 2 additions & 1 deletion projects/assets/src/translations/en/product.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"ariaLabelItemsAvailable_other": "{{name}}, {{state}} {{count}} items available",
"decreaseOptionsVisibility": "Options were hidden from the active group, tab backward to read them or forward for the next group",
"increaseOptionsVisibility": "More options were added to the active group, tab backward to read them or forward for the next group",
"backToResults": "Back To Results"
"backToResults": "Back To Results",
"productFacets": "Product Facets"
},
"productSummary": {
"id": "ID",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,10 @@ describe('Kayboard navigation', () => {
context('Facet component', () => {
beforeEach(() => {
cy.visit('/Brands/all/c/brands');
cy.get('cx-facet button').first().as('facetHeader');
cy.get('cx-facet-list button.tab-btn').first().as('facetHeader');
cy.get('cx-facet a').first().as('firstFacetOption');
});

it('focuses first option on ArrowDown while facet header is focused', () => {
cy.get('@facetHeader').focus().type('{downArrow}');
cy.get('@firstFacetOption').should('have.focus');
});

it('toggles facet closed/open with left and right arrow keys', () => {
cy.get('@facetHeader').focus().type('{leftArrow}');
cy.get('@firstFacetOption').should('not.be.visible');
cy.get('@facetHeader').focus().type('{rightArrow}');
cy.get('@firstFacetOption').should('be.visible');
});

it('navigates facet options with down arrow key', () => {
cy.get('@firstFacetOption').focus().type('{downArrow}');
cy.contains('Choshi').should('have.focus').type('{downArrow}');
Expand All @@ -86,5 +74,31 @@ describe('Kayboard navigation', () => {
cy.contains('Chiba').should('have.focus').type('{upArrow}');
cy.contains('Chiba').should('have.focus');
});

it('navigates facet categories with down arrow key', () => {
cy.get('cx-facet-list').get('@facetHeader').focus().type('{downArrow}');
cy.get('cx-facet-list')
.contains('Price')
.should('have.focus')
.type('{downArrow}');
cy.get('cx-facet-list')
.contains('Resolution')
.should('have.focus')
.type('{downArrow}');
cy.get('cx-facet-list').contains('Mounting').should('have.focus');
});

it('navigates facet categories with up arrow key', () => {
cy.get('@facetHeader').focus().type('{upArrow}');
cy.get('cx-facet-list')
.contains('Category')
.should('have.focus')
.type('{upArrow}');
cy.get('cx-facet-list')
.contains('Brand')
.should('have.focus')
.type('{upArrow}');
cy.get('cx-facet-list').contains('Color').should('have.focus');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ context('Product search', { testIsolation: false }, () => {
viewportContext(['mobile', 'desktop'], () => {
isolateTests();
before(() => {
// TODO: No longer needed to toggle a11yTabComponent feature when set to true
// by default.
cy.cxConfig({
features: {
a11yTabComponent: true,
},
});
cy.visit('/');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,21 +231,18 @@ export function clickFacet(header: string) {
cy.onMobile(() => {
cy.get('cx-product-facet-navigation button').click();
});
cy.get('cx-facet .heading')
cy.get('cx-facet-list cx-tab button.tab-btn')
.contains(header)
.then((el) => {
if (el.find('.fa-plus').is(':visible')) {
// TODO Remove force once you can scroll facets on mobile
cy.wrap(el).click({ force: true });
}
});
cy.get('cx-facet .heading')
.contains(header)
.parents('cx-facet')
.within(() => {
// TODO Remove force once you can scroll facets on mobile
cy.get('a.value').first().click({ force: true });
});
cy.get(`cx-facet[aria-label^="${header}"]`).within(() => {
// TODO Remove force once you can scroll facets on mobile
cy.get('a.value').first().click({ force: true });
});
cy.onMobile(() => {
cy.get('cx-product-facet-navigation button.close').click();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
<div
[attr.role]="mode === TAB_MODE.ACCORDIAN ? 'region' : 'tabpanel'"
[tabindex]="0"
[attr.tabindex]="tab.disableBorderFocus ? null : 0"
[attr.aria-labelledby]="tab.id ?? null"
[attr.id]="tab.id ? 'section-' + tab.id : null"
class="active"
[cxFocus]="{ focusOnEscape: true }"
[cxFocus]="{ focusOnEscape: !tab.disableBorderFocus }"
>
<ng-container [ngTemplateOutlet]="tab.content"></ng-container>
<ng-container
*ngIf="tab.content"
[ngTemplateOutlet]="tab.content"
></ng-container>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ describe('TabPanelComponent', () => {
expect(tabPanel?.getAttribute('aria-labelledby')).toEqual('1');
});

it('should have correct attribues when disableBorderFocus is active', () => {
component.tab = {
...mockTab,
disableBorderFocus: true,
};
fixture.detectChanges();

const tabPanel = document.querySelector('div[role="tabpanel"]');
expect(tabPanel?.getAttribute('tabindex')).toEqual(null);
});

it('should display template ref', () => {
const mockFixture = TestBed.createComponent(MockComponent);
mockFixture.detectChanges();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { I18nTestingModule } from '@spartacus/core';
import { of } from 'rxjs';
import { TabComponent } from './tab.component';
import { TAB_MODE } from './tab.model';

Expand All @@ -10,11 +11,9 @@ describe('TabComponent', () => {
const mockTabs = [
{
headerKey: 'tab0',
header: 'tab 0',
id: 0,
},
{
headerKey: 'tab1',
header: 'tab 1',
id: 1,
},
Expand All @@ -30,6 +29,8 @@ describe('TabComponent', () => {
},
];

const mockTabs$ = of(mockTabs);

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [I18nTestingModule],
Expand Down Expand Up @@ -77,6 +78,34 @@ describe('TabComponent', () => {
expect(secondButton.getAttribute('tabindex')).toEqual('-1');
});

it('should display menu buttons for tabs$', () => {
component.tabs = [];
component.tabs$ = mockTabs$;
fixture.detectChanges();

const tabEl = document.querySelector('div[class="tab"]');
expect(tabEl?.role).toEqual('tablist');

const buttonEls = document.querySelectorAll('button[role="tab"]');
expect(buttonEls.length).toEqual(4);

const firstButton = buttonEls[0];
expect(firstButton.getAttribute('id')).toEqual('0');
expect(firstButton.getAttribute('class')).toEqual('tab-btn active');
expect(firstButton.getAttribute('aria-selected')).toEqual('true');
expect(firstButton.getAttribute('aria-expanded')).toEqual(null);
expect(firstButton.getAttribute('aria-controls')).toEqual('section-0');
expect(firstButton.getAttribute('tabindex')).toEqual('0');

const secondButton = buttonEls[1];
expect(secondButton.getAttribute('id')).toEqual('1');
expect(secondButton.getAttribute('class')).toEqual('tab-btn');
expect(secondButton.getAttribute('aria-selected')).toEqual('false');
expect(secondButton.getAttribute('aria-expanded')).toEqual(null);
expect(secondButton.getAttribute('aria-controls')).toEqual('section-1');
expect(secondButton.getAttribute('tabindex')).toEqual('-1');
});

it('should navigate menu buttons with arrow keys', () => {
expect(component.isOpen(0)).toEqual(true);
expect(component.isOpen(1)).toEqual(false);
Expand Down Expand Up @@ -172,6 +201,64 @@ describe('TabComponent', () => {
expect(component.isOpen(3)).toEqual(false);
});

it('should NOT navigate menu buttons with restricted arrow keys', () => {
component.config = {
label: 'test',
mode: TAB_MODE.TAB,
openTabs: [0],
restrictDirectionKeys: true,
};
fixture.detectChanges();

expect(component.isOpen(0)).toEqual(true);
expect(component.isOpen(1)).toEqual(false);
expect(component.isOpen(2)).toEqual(false);

component.handleKeydownEvent(
0,
component.tabs,
component.config.mode,
new KeyboardEvent('keydown', { key: 'ArrowRight' })
);

expect(component.isOpen(0)).toEqual(false);
expect(component.isOpen(1)).toEqual(true);
expect(component.isOpen(2)).toEqual(false);

component.handleKeydownEvent(
1,
component.tabs,
component.config.mode,
new KeyboardEvent('keydown', { key: 'ArrowDown' })
);

expect(component.isOpen(0)).toEqual(false);
expect(component.isOpen(1)).toEqual(true);
expect(component.isOpen(2)).toEqual(false);

component.handleKeydownEvent(
1,
component.tabs,
component.config.mode,
new KeyboardEvent('keydown', { key: 'ArrowLeft' })
);

expect(component.isOpen(0)).toEqual(true);
expect(component.isOpen(1)).toEqual(false);
expect(component.isOpen(2)).toEqual(false);

component.handleKeydownEvent(
0,
component.tabs,
component.config.mode,
new KeyboardEvent('keydown', { key: 'ArrowUp' })
);

expect(component.isOpen(0)).toEqual(true);
expect(component.isOpen(1)).toEqual(false);
expect(component.isOpen(2)).toEqual(false);
});

it('should navigate to last tab with END key', () => {
expect(component.isOpen(0)).toEqual(true);
expect(component.isOpen(1)).toEqual(false);
Expand Down Expand Up @@ -259,6 +346,7 @@ describe('TabComponent', () => {
expect(firstButton.getAttribute('aria-expanded')).toEqual('true');
expect(firstButton.getAttribute('aria-controls')).toEqual('section-0');
expect(firstButton.getAttribute('tabindex')).toEqual('0');
expect(firstButton.getAttribute('title')).toEqual('Collapse tab0');

const secondButton = buttonEls[1];
expect(secondButton.getAttribute('id')).toEqual('1');
Expand All @@ -267,6 +355,7 @@ describe('TabComponent', () => {
expect(secondButton.getAttribute('aria-expanded')).toEqual('false');
expect(secondButton.getAttribute('aria-controls')).toEqual('section-1');
expect(secondButton.getAttribute('tabindex')).toEqual('0');
expect(secondButton.getAttribute('title')).toEqual('Expand tab 1');
});

it('should toggle tabs correctly in accordian mode', () => {
Expand Down Expand Up @@ -294,5 +383,52 @@ describe('TabComponent', () => {
expect(component.isOpen(1)).toEqual(false);
expect(component.isOpen(2)).toEqual(true);
});

it('should NOT navigate menu buttons with restricted arrow keys', () => {
const spy = spyOn(component, 'selectOrFocus');
component.config = {
label: 'test',
mode: TAB_MODE.ACCORDIAN,
openTabs: [0],
restrictDirectionKeys: true,
};
fixture.detectChanges();

component.handleKeydownEvent(
0,
component.tabs,
component.config.mode,
new KeyboardEvent('keydown', { key: 'ArrowRight' })
);

expect(spy).toHaveBeenCalledTimes(0);

component.handleKeydownEvent(
0,
component.tabs,
component.config.mode,
new KeyboardEvent('keydown', { key: 'ArrowDown' })
);

expect(spy).toHaveBeenCalledTimes(1);

component.handleKeydownEvent(
1,
component.tabs,
component.config.mode,
new KeyboardEvent('keydown', { key: 'ArrowLeft' })
);

expect(spy).toHaveBeenCalledTimes(1);

component.handleKeydownEvent(
1,
component.tabs,
component.config.mode,
new KeyboardEvent('keydown', { key: 'ArrowUp' })
);

expect(spy).toHaveBeenCalledTimes(2);
});
});
});
Loading

0 comments on commit 0e52b2e

Please sign in to comment.