Skip to content

Commit

Permalink
fix(components): Post-tabs cross-root aria-id (#2777)
Browse files Browse the repository at this point in the history
Workaround or alternative with light DOM (but not ready to use):
#2776
  • Loading branch information
imagoiq authored Mar 14, 2024
1 parent 770b6b3 commit cd4f946
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 50 deletions.
5 changes: 5 additions & 0 deletions .changeset/calm-spoons-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@swisspost/design-system-components': patch
---

Fixed accessibility of aria controls through post-tabs components.
28 changes: 10 additions & 18 deletions packages/components/cypress/e2e/tabs.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

Expand All @@ -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', () => {
Expand Down Expand Up @@ -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',
);
});
});
});
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,15 @@ export class PostTabHeader {

render() {
return (
<Host data-version={version}>
<button
aria-selected="false"
class="tab-title"
id={this.tabId}
role="tab"
tabindex="-1"
type="button"
>
<slot />
</button>
<Host
id={this.tabId}
role="tab"
data-version={version}
aria-selected="false"
tabindex="-1"
class="tab-title"
>
<slot />
</Host>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ export class PostTabPanel {

render() {
return (
<Host data-version={version}>
<div class="tab-pane" id={this.panelId} role="tabpanel">
<slot />
</div>
<Host data-version={version} id={this.panelId} role="tabpanel">
<slot />
</Host>
);
}
Expand Down
34 changes: 18 additions & 16 deletions packages/components/src/components/post-tabs/post-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand All @@ -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;
}
Expand Down Expand Up @@ -187,8 +190,7 @@ export class PostTabs {

if (!nextTab) return;

const nextTabTitle = nextTab.shadowRoot.querySelector('.tab-title') as HTMLAnchorElement;
nextTabTitle.focus();
nextTab.focus();
}

render() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down

0 comments on commit cd4f946

Please sign in to comment.