diff --git a/.changeset/calm-spoons-bake.md b/.changeset/calm-spoons-bake.md new file mode 100644 index 0000000000..8868dad1fe --- /dev/null +++ b/.changeset/calm-spoons-bake.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-components': patch +--- + +Fixed accessibility of aria controls through post-tabs components. diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts index b7d603fb0e..0b302db62a 100644 --- a/packages/components/cypress/e2e/tabs.cy.ts +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -16,10 +16,8 @@ describe('tabs', () => { }); it('should only show the first tab header as active', () => { - cy.get('@headers').each(($header, index) => { - cy.wrap($header) - .find('.active') - .should(index === 0 ? 'exist' : 'not.exist'); + cy.get('post-tab-header.active').each(($header, index) => { + cy.wrap($header).should(index === 0 ? 'exist' : 'not.exist'); }); }); @@ -37,8 +35,8 @@ describe('tabs', () => { it('should activate a clicked tab header and deactivate the tab header that was previously activated', () => { cy.get('@headers').last().click(); - cy.get('@headers').first().find('.active').should('not.exist'); - cy.get('@headers').last().find('.active').should('exist'); + cy.get('@headers').first().should('not.have.class', 'active'); + cy.get('@headers').last().should('have.class', 'active'); }); it('should show the panel associated with a clicked tab header and hide the panel that was previously shown', () => { @@ -82,9 +80,9 @@ describe('tabs', () => { cy.wrap($header) .invoke('attr', 'panel') .then(panel => { - cy.wrap($header) - .find('.active') - .should(panel === activePanel ? 'exist' : 'not.exist'); + cy.wrap($header.filter('.active')).should( + panel === activePanel ? 'exist' : 'not.exist', + ); }); }); }); @@ -121,8 +119,8 @@ describe('tabs', () => { cy.get('post-tab-header').as('headers'); cy.get('@headers').last().click(); - cy.get('@headers').first().find('.active').should('not.exist'); - cy.get('@headers').last().find('.active').should('exist'); + cy.get('@headers').first().should('not.have.class', 'active'); + cy.get('@headers').last().should('have.class', 'active'); }); it('should display the tab panel associated with the newly added tab after clicking on it', () => { @@ -169,12 +167,6 @@ describe('tabs', () => { describe('Accessibility', () => { it('Has no detectable a11y violations on load for all variants', () => { cy.getSnapshots('tabs'); - cy.checkA11y('#root-inner', { - rules: { - 'aria-valid-attr-value': { - enabled: false, - }, - }, - }); + cy.checkA11y('#root-inner'); }); }); diff --git a/packages/components/src/components/post-tab-header/post-tab-header.tsx b/packages/components/src/components/post-tab-header/post-tab-header.tsx index eeaf8e6322..b26ae72da4 100644 --- a/packages/components/src/components/post-tab-header/post-tab-header.tsx +++ b/packages/components/src/components/post-tab-header/post-tab-header.tsx @@ -32,17 +32,15 @@ export class PostTabHeader { render() { return ( - - + + ); } diff --git a/packages/components/src/components/post-tab-panel/post-tab-panel.tsx b/packages/components/src/components/post-tab-panel/post-tab-panel.tsx index cf9035aece..2808a81245 100644 --- a/packages/components/src/components/post-tab-panel/post-tab-panel.tsx +++ b/packages/components/src/components/post-tab-panel/post-tab-panel.tsx @@ -27,10 +27,8 @@ export class PostTabPanel { render() { return ( - -
- -
+ + ); } diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 7e513d746c..57a0033499 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -106,19 +106,24 @@ export class PostTabs { this.tabs.forEach(async tab => { await tab.componentOnReady(); - const tabTitle = tab.shadowRoot.querySelector('.tab-title'); - // if the tab has an "aria-controls" attribute it was already linked to its panel: do nothing - if (tabTitle.getAttribute('aria-controls')) return; + if (tab.getAttribute('aria-controls')) return; - const tabPanel = this.getPanel(tab.panel).shadowRoot.querySelector('.tab-pane'); - tabTitle.setAttribute('aria-controls', tabPanel.id); - tabPanel.setAttribute('aria-labelledby', tabTitle.id); + const tabPanel = this.getPanel(tab.panel); + tab.setAttribute('aria-controls', tabPanel.id); + tabPanel.setAttribute('aria-labelledby', tab.id); tab.addEventListener('click', () => { void this.show(tab.panel); }); + tab.addEventListener('keydown', (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + void this.show(tab.panel); + } + }); + tab.addEventListener('keydown', ({ key }) => { if (key === 'ArrowRight' || key === 'ArrowLeft') this.navigateTabs(tab, key); }); @@ -132,16 +137,14 @@ export class PostTabs { private activateTab(tab: HTMLPostTabHeaderElement) { if (this.activeTab) { - const tabTitle = this.activeTab.shadowRoot.querySelector('.tab-title'); - tabTitle.setAttribute('aria-selected', 'false'); - tabTitle.setAttribute('tabindex', '-1'); - tabTitle.classList.remove('active'); + this.activeTab.setAttribute('aria-selected', 'false'); + this.activeTab.setAttribute('tabindex', '-1'); + this.activeTab.classList.remove('active'); } - const tabTitle = tab.shadowRoot.querySelector('.tab-title'); - tabTitle.setAttribute('aria-selected', 'true'); - tabTitle.removeAttribute('tabindex'); - tabTitle.classList.add('active'); + tab.setAttribute('aria-selected', 'true'); + tab.setAttribute('tabindex', '0'); + tab.classList.add('active'); this.activeTab = tab; } @@ -187,8 +190,7 @@ export class PostTabs { if (!nextTab) return; - const nextTabTitle = nextTab.shadowRoot.querySelector('.tab-title') as HTMLAnchorElement; - nextTabTitle.focus(); + nextTab.focus(); } render() { diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index 80ffd6e013..b0a7b85c8d 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -75,7 +75,7 @@ export const Async: Story = { document.querySelectorAll('post-tab-header'); const activeHeader: HTMLPostTabHeaderElement | undefined = Array.from(headers).find( - header => header.shadowRoot?.querySelector('.active'), + header => document.querySelectorAll('post-tab-header.active'), ); activeHeader?.remove();