Skip to content

Commit 3d6f84e

Browse files
committed
Add tabs widget example where selection does not follow focus
Merge for pull request #269 that resolves issue #152.
2 parents 85aab94 + a6bc19d commit 3d6f84e

File tree

7 files changed

+574
-64
lines changed

7 files changed

+574
-64
lines changed

aria-practices.html

Lines changed: 53 additions & 50 deletions
Large diffs are not rendered by default.
File renamed without changes.
File renamed without changes.

examples/tabs/tabs.html renamed to examples/tabs/tabs-1/tabs.html

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,33 @@
22
<html lang="en">
33
<head>
44
<meta charset="utf-8" />
5-
<title>Tabs | WAI-ARIA Authoring Practices 1.1</title>
5+
<title>Tabs (Follows focus) | WAI-ARIA Authoring Practices 1.1</title>
66

77
<!-- Core js and css shared by all examples; do not modify when using this template. -->
8-
<link href="../css/core.css" rel="stylesheet">
9-
<link href="../css/table.css" rel="stylesheet">
8+
<link href="../../css/core.css" rel="stylesheet">
9+
<link href="../../css/table.css" rel="stylesheet">
1010

1111
<!-- js and css for this example. -->
1212
<link href="css/tabs.css" rel="stylesheet">
1313
</head>
1414
<body>
1515
<main>
16-
<h1>Tabs</h1>
16+
<h1>Tabs (Follows focus)</h1>
1717

1818
<p>
19-
The below example section demonstrates a tabs widget that implements the <a href="../../#tabpanel">design pattern for tabs</a>.
19+
This example section demonstrates a tabs widget that implements the <a href="../../../#tabpanel">design pattern for tabs</a>.
20+
In this example panels are automatically activated when its tab receives focus.
2021
</p>
22+
<p>Similar examples include: </p>
23+
<ul>
24+
<li><a href="../tabs-2/tabs.html">Tabs (Manual activation)</a>: a version of the tabs widget where tabs have to be manually activated via <kbd>space</kbd> or <kbd>enter</kbd>.</li>
25+
<!-- list other examples that implement the same design pattern. -->
26+
</ul>
2127

2228
<section>
2329
<h2 id="ex_label">Example</h2>
2430
<div aria-labelledby="ex_label" role="region">
25-
<!--
26-
Note the ID of the following div that contains the example HTML is used as a parameter for the sourceCode.add() function.
27-
The sourceCode functions in the examples/js/examples.js render the HTML source to show it to the reader of the example page.
28-
If you change the ID of this div, be sure to update the parameters of the sourceCode.add() function call, which is made following the div with id="sc1" where the HTML is render.
29-
The div for the rendered HTML source is in the last section of the page.
30-
-->
3131
<div id="ex1">
32-
<!-- Replace content of this div with the example. -->
3332
<div class="tabs">
3433
<div role="tablist">
3534
<button role="tab" aria-selected="true" aria-controls="nils-tab" id="nils">Nils Frahm</button>
@@ -67,7 +66,6 @@ <h2>Accessibility Features</h2>
6766

6867
<section>
6968
<h2 id="kbd_label">Keyboard Support</h2>
70-
<!-- Update the content of this table to describe which keys are implemented in this example. -->
7169
<table aria-labelledby="kbd_label">
7270
<thead>
7371
<tr>
@@ -187,7 +185,8 @@ <h2 id="sc1_label">HTML Source Code</h2>
187185
</main>
188186

189187
<nav>
190-
<a href="../../#tabpanel">Tabs Design Pattern in WAI-ARIA Authoring Practices 1.1</a>
188+
<!-- Update the pattern_ID parameter of this link to refer to the APG design pattern section related to this example. -->
189+
<a href="../../../#tabpanel">Tabs Design Pattern in WAI-ARIA Authoring Practices 1.1</a>
191190
</nav>
192191

193192

examples/tabs/tabs-2/css/tabs.css

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
.tabs {
2+
width: 20em;
3+
font-family: "lucida grande", sans-serif;
4+
}
5+
6+
[role="tab"] {
7+
margin: 0 0 -2px;
8+
border: 2px solid #444;
9+
border-radius: 0.4em 0.4em 0 0;
10+
font-family: inherit;
11+
font-size: inherit;
12+
background: #fff;
13+
}
14+
15+
[role="tab"][aria-selected="true"] {
16+
border-color: hsl(218, 96%, 38%);
17+
color: #fff;
18+
background: hsl(212, 96%, 38%);
19+
outline: 0;
20+
}
21+
22+
[role="tab"]:hover,
23+
[role="tab"]:focus {
24+
border-color: hsl(20, 96%, 38%);
25+
color: #000;
26+
background: hsl(20, 96%, 70%);
27+
outline: 0;
28+
}
29+
30+
[role="tabpanel"] {
31+
position: relative;
32+
z-index: 2;
33+
padding: 0.5em;
34+
border: 2px solid #444;
35+
border-radius: 0 0.4em 0.4em 0.4em;
36+
}
37+
38+
[role="tabpanel"]:focus {
39+
border-color: hsl(20, 96%, 38%);
40+
outline: 0;
41+
}
42+
43+
[role="tabpanel"] p {
44+
margin: 0;
45+
}
46+
47+
[role="tabpanel"] * + p {
48+
margin-top: 1em;
49+
}

examples/tabs/tabs-2/js/tabs.js

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
(function () {
2+
var tablist = document.querySelectorAll('[role="tablist"]')[0];
3+
var tabs;
4+
var panels;
5+
6+
generateArrays();
7+
8+
function generateArrays () {
9+
tabs = document.querySelectorAll('[role="tab"]');
10+
panels = document.querySelectorAll('[role="tabpanel"]');
11+
};
12+
13+
// For easy reference
14+
var keys = {
15+
end: 35,
16+
home: 36,
17+
left: 37,
18+
up: 38,
19+
right: 39,
20+
down: 40,
21+
delete: 46,
22+
enter: 13,
23+
space: 32
24+
};
25+
26+
// Add or substract depenign on key pressed
27+
var direction = {
28+
37: -1,
29+
38: -1,
30+
39: 1,
31+
40: 1
32+
};
33+
34+
// Bind listeners
35+
for (i = 0; i < tabs.length; ++i) {
36+
addListeners(i);
37+
};
38+
39+
function addListeners (index) {
40+
tabs[index].addEventListener('click', clickEventListener);
41+
tabs[index].addEventListener('keydown', keydownEventListener);
42+
tabs[index].addEventListener('keyup', keyupEventListener);
43+
44+
// Build an array with all tabs (<button>s) in it
45+
tabs[index].index = index;
46+
};
47+
48+
// When a tab is clicked, activateTab is fired to activate it
49+
function clickEventListener (event) {
50+
var tab = event.target;
51+
activateTab(tab, false);
52+
};
53+
54+
// Handle keydown on tabs
55+
function keydownEventListener (event) {
56+
var key = event.keyCode;
57+
58+
switch (key) {
59+
case keys.end:
60+
event.preventDefault();
61+
// Activate last tab
62+
focusLastTab();
63+
break;
64+
case keys.home:
65+
event.preventDefault();
66+
// Activate first tab
67+
focusFirstTab();
68+
break;
69+
70+
// Up and down are in keydown
71+
// because we need to prevent page scroll >:)
72+
case keys.up:
73+
case keys.down:
74+
determineOrientation(event);
75+
break;
76+
};
77+
};
78+
79+
// Handle keyup on tabs
80+
function keyupEventListener (event) {
81+
var key = event.keyCode;
82+
83+
switch (key) {
84+
case keys.left:
85+
case keys.right:
86+
determineOrientation(event);
87+
break;
88+
case keys.delete:
89+
determineDeletable(event);
90+
break;
91+
case keys.enter:
92+
case keys.space:
93+
activateTab(event.target);
94+
break;
95+
};
96+
};
97+
98+
// When a tablist’s aria-orientation is set to vertical,
99+
// only up and down arrow should function.
100+
// In all other cases only left and right arrow function.
101+
function determineOrientation (event) {
102+
var key = event.keyCode;
103+
var vertical = tablist.getAttribute('aria-orientation') == 'vertical';
104+
var proceed = false;
105+
106+
if (vertical) {
107+
if (key === keys.up || key === keys.down) {
108+
event.preventDefault();
109+
proceed = true;
110+
};
111+
}
112+
else {
113+
if (key === keys.left || key === keys.right) {
114+
proceed = true;
115+
};
116+
};
117+
118+
if (proceed) {
119+
switchTabOnArrowPress(event);
120+
};
121+
};
122+
123+
// Either focus the next, previous, first, or last tab
124+
// depening on key pressed
125+
function switchTabOnArrowPress (event) {
126+
var pressed = event.keyCode;
127+
128+
if (direction[pressed]) {
129+
var target = event.target;
130+
if (target.index !== undefined) {
131+
if (tabs[target.index + direction[pressed]]) {
132+
tabs[target.index + direction[pressed]].focus();
133+
}
134+
else if (pressed === keys.left || pressed === keys.up) {
135+
focusLastTab();
136+
}
137+
else if (pressed === keys.right || pressed == keys.down) {
138+
focusFirstTab();
139+
};
140+
};
141+
};
142+
};
143+
144+
// Activates any given tab panel
145+
function activateTab (tab, setFocus) {
146+
setFocus = setFocus || true;
147+
// Deactivate all other tabs
148+
deactivateTabs();
149+
150+
// Remove tabindex attribute
151+
tab.removeAttribute('tabindex');
152+
153+
// Set the tab as selected
154+
tab.setAttribute('aria-selected', 'true');
155+
156+
// Get the value of aria-controls (which is an ID)
157+
var controls = tab.getAttribute('aria-controls');
158+
159+
// Remove hidden attribute from tab panel to make it visible
160+
document.getElementById(controls).removeAttribute('hidden');
161+
162+
// Set focus when required
163+
if (setFocus) {
164+
tab.focus();
165+
};
166+
};
167+
168+
// Deactivate all tabs and tab panels
169+
function deactivateTabs () {
170+
for (t = 0; t < tabs.length; t++) {
171+
tabs[t].setAttribute('tabindex', '-1');
172+
tabs[t].setAttribute('aria-selected', 'false');
173+
};
174+
175+
for (p = 0; p < panels.length; p++) {
176+
panels[p].setAttribute('hidden', 'hidden');
177+
};
178+
};
179+
180+
// Make a guess
181+
function focusFirstTab () {
182+
tabs[0].focus();
183+
};
184+
185+
// Make a guess
186+
function focusLastTab () {
187+
tabs[tabs.length - 1].focus();
188+
};
189+
190+
// Detect if a tab is deletable
191+
function determineDeletable (event) {
192+
target = event.target;
193+
194+
if (target.getAttribute('data-deletable') !== null) {
195+
// Delete target tab
196+
deleteTab(event, target);
197+
198+
// Update arrays related to tabs widget
199+
generateArrays();
200+
201+
// Activate the first tab
202+
activateTab(tabs[0]);
203+
};
204+
};
205+
206+
// Deletes a tab and its panel
207+
function deleteTab (event) {
208+
var target = event.target;
209+
var panel = document.getElementById(target.getAttribute('aria-controls'));
210+
211+
target.parentElement.removeChild(target);
212+
panel.parentElement.removeChild(panel);
213+
};
214+
215+
// Determine whether there should be a delay
216+
// when user navigates with the arrow keys
217+
function determineDelay () {
218+
var hasDelay = tablist.hasAttribute('data-delay');
219+
var delay = 0;
220+
221+
if (hasDelay) {
222+
var delayValue = tablist.getAttribute('data-delay');
223+
if (delayValue) {
224+
delay = delayValue;
225+
}
226+
else {
227+
// If no value is specified, default to 300ms
228+
delay = 300;
229+
};
230+
};
231+
232+
return delay;
233+
};
234+
})();

0 commit comments

Comments
 (0)