Skip to content

Commit a6ce71d

Browse files
CloudAnalystGogithub-actions[bot]Josh-Cena
authored
Align aria-tab example with APG (mdn#39423)
* Update index.md Updated the example combining tab with tablist. * Update files/en-us/web/accessibility/aria/reference/roles/tab_role/index.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update files/en-us/web/accessibility/aria/reference/roles/tab_role/index.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update files/en-us/web/accessibility/aria/reference/roles/tab_role/index.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update index.md Linted * Update demo --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
1 parent 4173f52 commit a6ce71d

File tree

1 file changed

+112
-74
lines changed
  • files/en-us/web/accessibility/aria/reference/roles/tab_role

1 file changed

+112
-74
lines changed

files/en-us/web/accessibility/aria/reference/roles/tab_role/index.md

Lines changed: 112 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,15 @@ From the assistive technology user's perspective, the heading does not exist sin
5858

5959
### Keyboard interactions
6060

61-
| Key | Action |
62-
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63-
| <kbd>Tab</kbd> | When focus is outside of the `tablist` moves focus to the active tab. If focus is on the active tab moves focus to the next element in the keyboard focus order, ideally the active tab's associated `tabpanel`. |
64-
| <kbd>→</kbd> | Focuses and optionally activates the next tab in the tab list. If the current tab is the last tab in the tab list it activates the first tab. |
65-
| <kbd>←</kbd> | Focuses and optionally activates the previous tab in the tab list. If the current tab is the first tab in the tab list it activates the last tab. |
66-
| <kbd>Delete</kbd> | When allowed removes the currently selected tab from the tab list. |
61+
| Key | Action |
62+
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63+
| <kbd>Tab</kbd> | When focus is outside of the `tablist` moves focus to the active tab. If focus is on the active tab moves focus to the next element in the keyboard focus order, ideally the active tab's associated `tabpanel`. |
64+
| <kbd>→</kbd> | Focuses and optionally activates the next tab in the tab list. If the current tab is the last tab in the tab list it activates the first tab. |
65+
| <kbd>←</kbd> | Focuses and optionally activates the previous tab in the tab list. If the current tab is the first tab in the tab list it activates the last tab. |
66+
| <kbd>Enter</kbd>/<kbd>Space</kbd> | When a tab has focus, activates the tab, causing its associated panel to be displayed. |
67+
| <kbd>Home</kbd> | Focuses and optionally activates the first tab in the tab list. |
68+
| <kbd>End</kbd> | Focuses and optionally activates the last tab in the tab list. |
69+
| <kbd>Delete</kbd> | When allowed removes the currently selected tab from the tab list. |
6770

6871
### Required JavaScript features
6972

@@ -74,48 +77,65 @@ From the assistive technology user's perspective, the heading does not exist sin
7477

7578
This example combines the role `tab` with `tablist` and elements with `tabpanel` to create an interactive group of tabbed content. Here we are enclosing our group of content in a `div`, with our `tablist` having an `aria-label` which labels it for assistive technology. Each `tab` is a `button` with the attributes previously mentioned. The first `tab` has both `tabindex="0"` and `aria-selected="true"` applied. These two attributes must always be coordinated as such—so when another tab is selected, it will then have `tabindex="0"` and `aria-selected="true"` applied. All unselected tabs must have `aria-selected="false"` and `tabindex="-1"`.
7679

77-
All of the `tabpanel` elements have `tabindex="0"` to make them tabbable, and all but the currently active one have the `hidden` attribute. The `hidden` attribute will be removed when a `tabpanel` becomes visible with JavaScript. There is some basic styling applied that restyles the buttons and changes the [`z-index`](/en-US/docs/Web/CSS/z-index) of `tab` elements to give the illusion of it connecting to the `tabpanel` for active elements, and the illusion that inactive elements are behind the active `tabpanel`.
80+
All of the `tabpanel` elements have `tabindex="0"` to make them tabbable, and all but the currently active one have the `hidden` attribute. The `hidden` attribute will be removed when a `tabpanel` becomes visible with JavaScript.
81+
82+
> [!NOTE]
83+
> Setting `tabindex` on the tab panel is unnecessary if the first element in the tab panel is focusable (such as a link), because tabbing to the link will also start reading the panel's content. However, if there are any panels in the set whose first content element is not focusable, then all tabpanel elements in a tab set should be focusable, so that screen reader users can navigate to the panel content consistently.
7884
7985
```html
8086
<div class="tabs">
81-
<div role="tablist" aria-label="Sample Tabs">
87+
<div role="tablist" aria-label="Select your operating system">
8288
<button
8389
role="tab"
8490
aria-selected="true"
8591
aria-controls="panel-1"
8692
id="tab-1"
8793
tabindex="0">
88-
First Tab
94+
Windows
8995
</button>
9096
<button
9197
role="tab"
9298
aria-selected="false"
9399
aria-controls="panel-2"
94100
id="tab-2"
95101
tabindex="-1">
96-
Second Tab
102+
macOS
97103
</button>
98104
<button
99105
role="tab"
100106
aria-selected="false"
101107
aria-controls="panel-3"
102108
id="tab-3"
103109
tabindex="-1">
104-
Third Tab
110+
Linux
105111
</button>
106112
</div>
107-
<div id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1">
108-
<p>Content for the first panel</p>
109-
</div>
110-
<div id="panel-2" role="tabpanel" tabindex="0" aria-labelledby="tab-2" hidden>
111-
<p>Content for the second panel</p>
112-
</div>
113-
<div id="panel-3" role="tabpanel" tabindex="0" aria-labelledby="tab-3" hidden>
114-
<p>Content for the third panel</p>
113+
<div class="tab-panels">
114+
<div id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1">
115+
<p>How to run this application on Windows</p>
116+
</div>
117+
<div
118+
id="panel-2"
119+
role="tabpanel"
120+
tabindex="0"
121+
aria-labelledby="tab-2"
122+
hidden="hidden">
123+
<p>How to run this application on macOS</p>
124+
</div>
125+
<div
126+
id="panel-3"
127+
role="tabpanel"
128+
tabindex="0"
129+
aria-labelledby="tab-3"
130+
hidden="hidden">
131+
<p>How to run this application on Linux</p>
132+
</div>
115133
</div>
116134
</div>
117135
```
118136

137+
There is some basic styling applied that restyles the buttons and changes the [`z-index`](/en-US/docs/Web/CSS/z-index) of `tab` elements to give the illusion of it connecting to the `tabpanel` for active elements, and the illusion that inactive elements are behind the active `tabpanel`. You need to clearly distinguish the active tab from the inactive tabs, such as thicker borders or larger size.
138+
119139
```css hidden
120140
.tabs {
121141
padding: 1em;
@@ -137,6 +157,7 @@ All of the `tabpanel` elements have `tabindex="0"` to make them tabbable, and al
137157

138158
[role="tab"][aria-selected="true"] {
139159
z-index: 3;
160+
border-top-width: 4px;
140161
}
141162

142163
[role="tabpanel"] {
@@ -154,73 +175,90 @@ All of the `tabpanel` elements have `tabindex="0"` to make them tabbable, and al
154175
}
155176
```
156177

157-
There are two things we need to do with JavaScript: we need to change focus and tab index of our `tab` elements with the right and left arrows, and we need to change the active `tab` and `tabpanel` when we click on a `tab`.
158-
159-
To accomplish the first, we listen for the [`keydown`](/en-US/docs/Web/API/Element/keydown_event) event on the `tablist`. If the event's [`key`](/en-US/docs/Web/API/KeyboardEvent/key) is `ArrowRight` or `ArrowLeft`, we react to the event. We start by setting the `tabindex` of the current `tab` element to -1, making it no longer tabbable. Then, if the right arrow is being pressed, we increase our tab focus counter by one. If the counter is greater than the number of `tab` elements we have, we circle back to the first tab by setting that counter to 0. If the left arrow is being pressed, we decrease our tab focus counter by one, and if it is then less than 0, we set it to the number of `tab` elements minus one (to get to the last element). Finally, we set `focus` to the `tab` element whose index is equal to the tab focus counter, and set its `tabindex` to 0 to make it tabbable.
160-
161-
To handle changing the active `tab` and `tabpanel`, we have a function that takes in the event, gets the element that triggered the event, the triggering element's parent element, and its grandparent element. We then find all tabs with `aria-selected="true"` inside the parent element and sets it to `false`, then sets the triggering element's `aria-selected` to `true`. After that, we find all `tabpanel` elements in the grandparent element, make them all `hidden`, and finally select the element whose `id` is equal to the triggering `tab`'s `aria-controls` and removes the `hidden` attribute, making it visible.
178+
The user interaction is handled with JavaScript. We first get references to our `tablist`, all the `tab` elements inside it, the container of our `tabpanel` elements, and all the `tabpanel` elements inside that container. This is based on some assumptions about the structure of our HTML, so if you change the structure, you will need to change this code. If you have multiple tabbed interfaces on a page, you can wrap this code in a function and pass `tabsContainer` as an argument.
162179

163180
```js
164-
// Only handle one particular tablist; if you have multiple tab
165-
// lists (might even be nested), you have to apply this code for each one
166-
const tabList = document.querySelector('[role="tablist"]');
167-
const tabs = tabList.querySelectorAll(':scope > [role="tab"]');
168-
169-
// Add a click event handler to each tab
170-
tabs.forEach((tab) => {
171-
tab.addEventListener("click", changeTabs);
172-
});
181+
const tabsContainer = document.querySelector(".tabs");
182+
const tabList = tabsContainer.querySelector(':scope > [role="tablist"]');
183+
const tabs = Array.from(tabList.querySelectorAll(':scope > [role="tab"]'));
184+
const tabPanelsContainer = tabsContainer.querySelector(":scope > .tab-panels");
185+
const tabPanels = Array.from(
186+
tabPanelsContainer.querySelectorAll(':scope > [role="tabpanel"]'),
187+
);
188+
```
173189

174-
// Enable arrow navigation between tabs in the tab list
175-
let tabFocus = 0;
190+
For keyboard interactions, we listen for the [`keydown`](/en-US/docs/Web/API/Element/keydown_event) event on the `tablist`. In this demo, we chose to not activate the `tab` when the user navigates with the arrow keys, but instead only move focus. If you want to display the `tab` when it receives focus, you can call the `showTab()` function (defined later) instead of just calling `focus()` on the new tab.
176191

192+
```js
177193
tabList.addEventListener("keydown", (e) => {
178-
// Move right
179-
if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
180-
tabs[tabFocus].setAttribute("tabindex", -1);
181-
if (e.key === "ArrowRight") {
182-
tabFocus++;
183-
// If we're at the end, go to the start
184-
if (tabFocus >= tabs.length) {
185-
tabFocus = 0;
186-
}
187-
// Move left
188-
} else if (e.key === "ArrowLeft") {
189-
tabFocus--;
190-
// If we're at the start, move to the end
191-
if (tabFocus < 0) {
192-
tabFocus = tabs.length - 1;
193-
}
194-
}
195-
196-
tabs[tabFocus].setAttribute("tabindex", 0);
197-
tabs[tabFocus].focus();
194+
const currentTab = e.target;
195+
const currentIndex = tabs.indexOf(currentTab);
196+
if (currentIndex === -1) return; // Exit if the focused element is not a tab
197+
let newIndex = 0;
198+
199+
switch (e.key) {
200+
case "ArrowRight":
201+
newIndex = (currentIndex + 1) % tabs.length;
202+
break;
203+
case "ArrowLeft":
204+
newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
205+
break;
206+
case "Home":
207+
newIndex = 0;
208+
break;
209+
case "End":
210+
newIndex = tabs.length - 1;
211+
break;
212+
default:
213+
return; // Exit if the key is not recognized
198214
}
199-
});
200215

201-
function changeTabs(e) {
202-
const targetTab = e.target;
203-
const tabList = targetTab.parentNode;
204-
const tabGroup = tabList.parentNode;
216+
e.preventDefault();
217+
e.stopPropagation();
218+
tabs[newIndex].focus();
219+
});
220+
```
205221

206-
// Remove all current selected tabs
207-
tabList
208-
.querySelectorAll(':scope > [aria-selected="true"]')
209-
.forEach((t) => t.setAttribute("aria-selected", false));
222+
The tab panel is only activated either by pressing <kbd>Enter</kbd> or <kbd>Space</kbd> while a `tab` has focus, or by clicking on a `tab`. We first define a function `showTab()` that takes in the `tab` element to be shown.
210223

211-
// Set this tab as selected
224+
```js
225+
function showTab(targetTab) {
226+
// Unselect other tabs and set this tab as selected
227+
for (const tab of tabs) {
228+
if (tab === targetTab) continue;
229+
tab.setAttribute("aria-selected", false);
230+
tab.tabIndex = -1;
231+
}
212232
targetTab.setAttribute("aria-selected", true);
233+
targetTab.tabIndex = 0;
234+
235+
// Hide other tab panels and show the selected panel
236+
const targetTabPanel = document.getElementById(
237+
targetTab.getAttribute("aria-controls"),
238+
);
239+
for (const panel of tabPanels) {
240+
if (panel === targetTabPanel) continue;
241+
panel.hidden = true;
242+
}
243+
targetTabPanel.hidden = false;
244+
}
245+
```
213246

214-
// Hide all tab panels
215-
tabGroup
216-
.querySelectorAll(':scope > [role="tabpanel"]')
217-
.forEach((p) => p.setAttribute("hidden", true));
247+
Now we can call this function either on a `click` event or on a `keydown` event.
218248

219-
// Show the selected panel
220-
tabGroup
221-
.querySelector(`#${targetTab.getAttribute("aria-controls")}`)
222-
.removeAttribute("hidden");
223-
}
249+
```js
250+
tabs.forEach((tab) => {
251+
tab.addEventListener("click", (e) => {
252+
showTab(e.target);
253+
});
254+
tab.addEventListener("keydown", (e) => {
255+
if (e.key === "Enter" || e.key === " ") {
256+
e.preventDefault();
257+
e.stopPropagation();
258+
showTab(e.target);
259+
}
260+
});
261+
});
224262
```
225263

226264
{{EmbedLiveSample("Example", 600, 130)}}

0 commit comments

Comments
 (0)