Skip to content

Commit

Permalink
Add tabs widget example that does not follow focus
Browse files Browse the repository at this point in the history
  • Loading branch information
Michiel Bijl committed Feb 2, 2017
1 parent 69a36ff commit eb14297
Show file tree
Hide file tree
Showing 7 changed files with 573 additions and 55 deletions.
103 changes: 53 additions & 50 deletions aria-practices.html

Large diffs are not rendered by default.

File renamed without changes.
File renamed without changes.
17 changes: 12 additions & 5 deletions examples/tabs/tabs.html → examples/tabs/tabs-1/tabs.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
<!-- Give this example implementation a name and put it in the title element, e.g., "Checkbox" if implementing the checkbox pattern.
The name should be unique if the pattern will have multiple implementations, e.g., "Simple Two-State Checkbox" or "Three-State Checkbox".
-->
<title>Tabs | WAI-ARIA Authoring Practices 1.1</title>
<title>Tabs (Follows focus) | WAI-ARIA Authoring Practices 1.1</title>

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

<!-- js and css for this example.
When developing an example implementation of a design pattern,
Expand All @@ -28,13 +28,20 @@
<body>
<main>
<!-- Repeat the name of the example in this H1 -->
<h1>Tabs</h1>
<h1>Tabs (Follows focus)</h1>

<p>
<!-- Provide an overview of the example where the first sentence provides a link to the section of aria-practices.html that describes the pattern this example implements. -->
The below example section demonstrates a tabs widget that implements the <a href="../../#checkbox">design pattern for tabs</a>.
This example section demonstrates a tabs widget that implements the <a href="../../#checkbox">design pattern for tabs</a>.
In this example panels are automatically activated when its tab receives focus.
</p>

<p>Similar examples include: </p>
<ul>
<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>
<!-- list other examples that implement the same design pattern. -->
</ul>

<section>
<h2 id="ex_label">Example</h2>
<div aria-labelledby="ex_label" role="region">
Expand Down
49 changes: 49 additions & 0 deletions examples/tabs/tabs-2/css/tabs.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.tabs {
width: 20em;
font-family: "lucida grande", sans-serif;
}

[role="tab"] {
margin: 0 0 -2px;
border: 2px solid #444;
border-radius: 0.4em 0.4em 0 0;
font-family: inherit;
font-size: inherit;
background: #fff;
}

[role="tab"][aria-selected="true"] {
border-color: hsl(218, 96%, 38%);
color: #fff;
background: hsl(212, 96%, 38%);
outline: 0;
}

[role="tab"]:hover,
[role="tab"]:focus {
border-color: hsl(20, 96%, 38%);
color: #000;
background: hsl(20, 96%, 70%);
outline: 0;
}

[role="tabpanel"] {
position: relative;
z-index: 2;
padding: 0.5em;
border: 2px solid #444;
border-radius: 0 0.4em 0.4em 0.4em;
}

[role="tabpanel"]:focus {
border-color: hsl(20, 96%, 38%);
outline: 0;
}

[role="tabpanel"] p {
margin: 0;
}

[role="tabpanel"] * + p {
margin-top: 1em;
}
234 changes: 234 additions & 0 deletions examples/tabs/tabs-2/js/tabs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
(function () {
var tablist = document.querySelectorAll('[role="tablist"]')[0];
var tabs;
var panels;

generateArrays();

function generateArrays () {
tabs = document.querySelectorAll('[role="tab"]');
panels = document.querySelectorAll('[role="tabpanel"]');
};

// For easy reference
var keys = {
end: 35,
home: 36,
left: 37,
up: 38,
right: 39,
down: 40,
delete: 46,
enter: 13,
space: 32
};

// Add or substract depenign on key pressed
var direction = {
37: -1,
38: -1,
39: 1,
40: 1
};

// Bind listeners
for (i = 0; i < tabs.length; ++i) {
addListeners(i);
};

function addListeners (index) {
tabs[index].addEventListener('click', clickEventListener);
tabs[index].addEventListener('keydown', keydownEventListener);
tabs[index].addEventListener('keyup', keyupEventListener);

// Build an array with all tabs (<button>s) in it
tabs[index].index = index;
};

// When a tab is clicked, activateTab is fired to activate it
function clickEventListener (event) {
var tab = event.target;
activateTab(tab, false);
};

// Handle keydown on tabs
function keydownEventListener (event) {
var key = event.keyCode;

switch (key) {
case keys.end:
event.preventDefault();
// Activate last tab
focusLastTab();
break;
case keys.home:
event.preventDefault();
// Activate first tab
focusFirstTab();
break;

// Up and down are in keydown
// because we need to prevent page scroll >:)
case keys.up:
case keys.down:
determineOrientation(event);
break;
};
};

// Handle keyup on tabs
function keyupEventListener (event) {
var key = event.keyCode;

switch (key) {
case keys.left:
case keys.right:
determineOrientation(event);
break;
case keys.delete:
determineDeletable(event);
break;
case keys.enter:
case keys.space:
activateTab(event.target);
break;
};
};

// When a tablist’s aria-orientation is set to vertical,
// only up and down arrow should function.
// In all other cases only left and right arrow function.
function determineOrientation (event) {
var key = event.keyCode;
var vertical = tablist.getAttribute('aria-orientation') == 'vertical';
var proceed = false;

if (vertical) {
if (key === keys.up || key === keys.down) {
event.preventDefault();
proceed = true;
};
}
else {
if (key === keys.left || key === keys.right) {
proceed = true;
};
};

if (proceed) {
switchTabOnArrowPress(event);
};
};

// Either focus the next, previous, first, or last tab
// depening on key pressed
function switchTabOnArrowPress (event) {
var pressed = event.keyCode;

if (direction[pressed]) {
var target = event.target;
if (target.index !== undefined) {
if (tabs[target.index + direction[pressed]]) {
tabs[target.index + direction[pressed]].focus();
}
else if (pressed === keys.left || pressed === keys.up) {
focusLastTab();
}
else if (pressed === keys.right || pressed == keys.down) {
focusFirstTab();
};
};
};
};

// Activates any given tab panel
function activateTab (tab, setFocus) {
setFocus = setFocus || true;
// Deactivate all other tabs
deactivateTabs();

// Remove tabindex attribute
tab.removeAttribute('tabindex');

// Set the tab as selected
tab.setAttribute('aria-selected', 'true');

// Get the value of aria-controls (which is an ID)
var controls = tab.getAttribute('aria-controls');

// Remove hidden attribute from tab panel to make it visible
document.getElementById(controls).removeAttribute('hidden');

// Set focus when required
if (setFocus) {
tab.focus();
};
};

// Deactivate all tabs and tab panels
function deactivateTabs () {
for (t = 0; t < tabs.length; t++) {
tabs[t].setAttribute('tabindex', '-1');
tabs[t].setAttribute('aria-selected', 'false');
};

for (p = 0; p < panels.length; p++) {
panels[p].setAttribute('hidden', 'hidden');
};
};

// Make a guess
function focusFirstTab () {
tabs[0].focus();
};

// Make a guess
function focusLastTab () {
tabs[tabs.length - 1].focus();
};

// Detect if a tab is deletable
function determineDeletable (event) {
target = event.target;

if (target.getAttribute('data-deletable') !== null) {
// Delete target tab
deleteTab(event, target);

// Update arrays related to tabs widget
generateArrays();

// Activate the first tab
activateTab(tabs[0]);
};
};

// Deletes a tab and its panel
function deleteTab (event) {
var target = event.target;
var panel = document.getElementById(target.getAttribute('aria-controls'));

target.parentElement.removeChild(target);
panel.parentElement.removeChild(panel);
};

// Determine whether there should be a delay
// when user navigates with the arrow keys
function determineDelay () {
var hasDelay = tablist.hasAttribute('data-delay');
var delay = 0;

if (hasDelay) {
var delayValue = tablist.getAttribute('data-delay');
if (delayValue) {
delay = delayValue;
}
else {
// If no value is specified, default to 300ms
delay = 300;
};
};

return delay;
};
})();
Loading

0 comments on commit eb14297

Please sign in to comment.