Skip to content

Commit cc10106

Browse files
♿ [#5] Implement tabs on landing page in an accessible way
All the credit goes to the MDN docs: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/tab_role The markup, JS and CSS are updated so that it's accessible and understood by screenreaders. Following Chris' feedback, the JS is updated to use accessible queries to select elements, which helps lock in the a11y requirements. The CSS is modified slightly to account for the (new) button element which comes with some default background styling out of the box, and the 'active' modifier is no longer required on the tab panels. Note that each tab element is now a button because that handles the Enter and spacebar keys properly to activate the associated tab panel.
1 parent b77094b commit cc10106

File tree

3 files changed

+100
-20
lines changed

3 files changed

+100
-20
lines changed

maykin_common/static/maykin_common/css/api.css

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
padding-block: 0.5rem;
109109
padding-inline: 1rem;
110110
border: solid 1px transparent;
111+
background-color: transparent;
111112
cursor: pointer;
112113
color: var(--tabs-item-color, var(--link-color));
113114

@@ -133,9 +134,7 @@
133134
}
134135

135136
.tabs__pane {
136-
display: none;
137-
138-
&.tabs__pane--active {
137+
&:not('[hidden]') {
139138
display: flex;
140139
flex-direction: column;
141140
}

maykin_common/static/maykin_common/js/nav-tabs.js

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,70 @@
1+
// See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/tab_role
2+
// for the accessibility requirements.
3+
14
const initTabs = () => {
25
const tabNodes = document.querySelectorAll('.tabs');
36
for (const node of tabNodes) {
4-
// bind events to toggle tabs for each component occurrence
5-
for (const tabBtn of node.querySelectorAll('.tabs__item')) {
6-
tabBtn.addEventListener('click', event => {
7+
const tabList = node.querySelector('[role="tablist"]');
8+
const tabs = tabList.querySelectorAll(':scope > [role="tab"]');
9+
10+
// Handle clicks on each tab to activate the associated panel
11+
for (const tabBtn of tabs) {
12+
tabBtn.addEventListener('click', () => {
713
// ignore if it's already active
8-
if (tabBtn.classList.contains('.tabs__item--active')) {
14+
if (tabBtn.getAttribute('aria-selected') === 'true') {
915
return;
1016
}
1117

12-
// otherwise, remove the active class from the active button and pane
13-
node.querySelector('.tabs__item--active').classList.remove('tabs__item--active');
14-
node.querySelector('.tabs__pane--active').classList.remove('tabs__pane--active');
18+
// Remove all current selected tabs
19+
// and remove the active class from the active button and pane
20+
tabList
21+
.querySelectorAll(':scope > [aria-selected="true"]')
22+
.forEach(tab => {
23+
tab.setAttribute("aria-selected", false);
24+
tab.classList.remove('tabs__item--active');
25+
const panelId = tab.getAttribute('aria-controls');
26+
const panel = node.querySelector(`#${panelId}`);
27+
panel.setAttribute('hidden', true)
28+
});
1529

16-
// and activate the target
30+
// mark this tab as selected/active
31+
tabBtn.setAttribute('aria-selected', true);
1732
tabBtn.classList.add('tabs__item--active');
18-
const id = tabBtn.dataset.id;
19-
const tabPane = document.getElementById(id);
20-
tabPane.classList.add("tabs__pane--active");
33+
34+
// lookup the panel to activate
35+
const panelId = tabBtn.getAttribute('aria-controls');
36+
const panel = node.querySelector(`#${panelId}`);
37+
panel.removeAttribute('hidden');
2138
});
2239
}
40+
41+
let tabFocusIndex = 0;
42+
43+
// Handle keyboard navigation to focus other tabs
44+
tabList.addEventListener('keydown', event => {
45+
// move right or left -> remove tab focus from current tab
46+
if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
47+
tabs[tabFocusIndex].setAttribute("tabindex", -1);
48+
49+
if (event.key === "ArrowRight") {
50+
tabFocusIndex++;
51+
// If we're at the end, go to the start
52+
if (tabFocusIndex >= tabs.length) {
53+
tabFocusIndex = 0;
54+
}
55+
// Move left
56+
} else if (event.key === "ArrowLeft") {
57+
tabFocusIndex--;
58+
// If we're at the start, move to the end
59+
if (tabFocusIndex < 0) {
60+
tabFocusIndex = tabs.length - 1;
61+
}
62+
}
63+
64+
tabs[tabFocusIndex].setAttribute("tabindex", 0);
65+
tabs[tabFocusIndex].focus();
66+
}
67+
});
2368
}
2469
};
2570

maykin_common/templates/maykin_common/api/index_base.html

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
{% block content_en %}
1414
<p>This API is awesome because ...</p>
1515
{% endblock %}
16+
17+
See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/tab_role
18+
for the accessible markup.
1619
{% endcomment %}
1720

1821
{% block view_class %}{{ block.super }} landing-page{% endblock %}
@@ -35,16 +38,49 @@ <h1 class="page-title__title">
3538

3639
<article class="page-content page-content--centered">
3740
<nav class="tabs">
38-
<ul class="tabs__list">
39-
<li class="tabs__item tabs__item--active" data-id="nl" aria-role="tab">Nederlands</li>
40-
<li class="tabs__item" data-id="en" aria-role="tab">English</li>
41-
</ul>
41+
<div role="tablist" class="tabs__list">
42+
<button
43+
class="tabs__item tabs__item--active"
44+
id="tab-nl"
45+
role="tab"
46+
aria-controls="content-nl"
47+
aria-selected="true"
48+
tabindex="0"
49+
>
50+
Nederlands
51+
</button>
52+
<button
53+
class="tabs__item"
54+
id="tab-en"
55+
role="tab"
56+
aria-controls="content-en"
57+
aria-selected="false"
58+
tabindex="-1"
59+
>
60+
English
61+
</button>
62+
</div>
4263

4364
<div class="tabs__content">
44-
<div class="tabs__pane tabs__pane--active" id="nl" lang="nl">
65+
<div
66+
class="tabs__pane"
67+
id="content-nl"
68+
lang="nl"
69+
role="tabpanel"
70+
tabindex="0"
71+
aria-labelledby="tab-nl"
72+
>
4573
{% block content_nl %}Override block content_nl{% endblock content_nl %}
4674
</div>
47-
<div class="tabs__pane" id="en" lang="en">
75+
<div
76+
class="tabs__pane"
77+
id="content-en"
78+
lang="en"
79+
role="tabpanel"
80+
tabindex="0"
81+
aria-labelledby="tab-en"
82+
hidden
83+
>
4884
{% block content_en %}Override block content_en{% endblock content_en %}
4985
</div>
5086
</div>

0 commit comments

Comments
 (0)