Roving tabindex 0.2.0
GitHubA simple HTML web component that implements the roving tabindex pattern for building accessible menus and grids.
Wrap it around some markup and give it a selector to determine which elements become navigable!
<roving-tabindex selector="button">
<ul class="menu" role="toolbar" aria-label="toolbar example">
<li>
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use href="#cut" />
</svg>
<span>Cut</span>
</button>
</li>
<li>
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use href="#copy" />
</svg>
<span>Copy</span>
</button>
</li>
<li>
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use href="#paste" />
</svg>
<span>Paste</span>
</button>
</li>
</ul>
</roving-tabindex>
Usage
<roving-tabindex>
isn't on NPM; instead, you install it by downloading
roving-tabindex-0.2.0.js
and adding it to your codebase. The default export is a class with a static register
method:
<script type="module">
import RovingTabindex from "/rovingtabindex-0.2.0.js";
RovingTabIndex.register();
<script/>
By default, it registers the tag name <roving-tabindex>
. You can customize it by passing a
different string:
<script type="module">
import RovingTabindex from "/rovingtabindex-0.2.0.js";
RovingTabIndex.register("other-tagname");
<script/>
Once you've registered the tag, wrap <roving-tabindex>
(or whatever name you chose) around
the markup for which you want arrow key navigation. Use the selector
prop to determine which
descendant elements should be included.
<roving-tabindex selector="button, a" direction="horziontal">
<ul class="menu" role="toolbar" aria-label="toolbar-example">
<li><button>One</button></li>
<li><button>Two</button></li>
<li><button>Three</button></li>
<li><a href="#">Link</a></li>
</ul>
</roving-tabindex>
Make sure you set WAI-ARIA-related properties as
appropriate. For example, the previous demo implements keyboard navigation described in the
toolbar pattern, but
<roving-tabindex>
doesn't manage the role
and
aria-label
attributes.
Examples
Menu Bar
The simplest use of <roving-tabindex>
is to make a one dimensional list of buttons or links
navigable with arrow keys.
By default, the left/up and right/down arrow keys move forward and backward in the list, respectively, but you
can set the direction
attribute to "horizontal"
or "vertical"
to use only
one dimension.
Source
<roving-tabindex selector="button, a" direction="horziontal">
<ul class="menu" role="toolbar" aria-label="toolbar-example">
<li><button>One</button></li>
<li><button>Two</button></li>
<li><button>Three</button></li>
<li><a href="#">Link</a></li>
</ul>
</roving-tabindex>
This works especially well as ✨ progressive enhancement ✨ on
menus that are already keyboard navigable with the tab key. For instance, here's this example without
<roving-tabindex>
. The arrow keys don't work, but you can still access each menu item by
tabbing through.
Spreadsheet
<roving-tabindex>
can also be used to navigate in two dimensions by setting the
direction
attribute to "grid"
and setting the columns
attribute to the
number of columns. Here, it's used to replicate a spreadsheet interface.
Each table cell contains a <label>
and a hidden <input>
. When the user
presses a key other than the keys <roving-tabindex>
uses for navigation on a
<label>
, hide it and show the corresponding <input>
. When the user presses
Enter
or Escape
on the <input>
, swap it back.
Call stopPropagation
on keydown
events originating from
<input>
elements to prevent <roving-tabindex>
from changing the focused
element while editing text.
A | B | C | D | E | |
---|---|---|---|---|---|
1 | |||||
2 | |||||
3 | |||||
4 | |||||
5 |
Source
<roving-tabindex selector="label" direction="grid" columns="5">
<table id="spreadsheet">
<thead>
<tr>
<th width="0"></th>
<th width="20%">A</th>
<th width="20%">B</th>
<th width="20%">C</th>
<th width="20%">D</th>
<th width="20%">E</th>
</tr>
</thead>
<tbody>
<tr>
<th>1</th>
<td><label for="a1"></label><input class="hidden" id="a1" /></td>
<td><label for="a2"></label><input class="hidden" id="a2" /></td>
<td><label for="a3"></label><input class="hidden" id="a3" /></td>
<td><label for="a4"></label><input class="hidden" id="a4" /></td>
<td><label for="a5"></label><input class="hidden" id="a5" /></td>
</tr>
<tr>
<th>2</th>
<td><label for="b1"></label><input class="hidden" id="b1" /></td>
<td><label for="b2"></label><input class="hidden" id="b2" /></td>
<td><label for="b3"></label><input class="hidden" id="b3" /></td>
<td><label for="b4"></label><input class="hidden" id="b4" /></td>
<td><label for="b5"></label><input class="hidden" id="b5" /></td>
</tr>
<tr>
<th>3</th>
<td><label for="c1"></label><input class="hidden" id="c1" /></td>
<td><label for="c2"></label><input class="hidden" id="c2" /></td>
<td><label for="c3"></label><input class="hidden" id="c3" /></td>
<td><label for="c4"></label><input class="hidden" id="c4" /></td>
<td><label for="c5"></label><input class="hidden" id="c5" /></td>
</tr>
<tr>
<th>4</th>
<td><label for="d1"></label><input class="hidden" id="d1" /></td>
<td><label for="d2"></label><input class="hidden" id="d2" /></td>
<td><label for="d3"></label><input class="hidden" id="d3" /></td>
<td><label for="d4"></label><input class="hidden" id="d4" /></td>
<td><label for="d5"></label><input class="hidden" id="d5" /></td>
</tr>
<tr>
<th>5</th>
<td><label for="a1"></label><input class="hidden" id="e1" /></td>
<td><label for="a2"></label><input class="hidden" id="e2" /></td>
<td><label for="a3"></label><input class="hidden" id="e3" /></td>
<td><label for="a4"></label><input class="hidden" id="e4" /></td>
<td><label for="a5"></label><input class="hidden" id="e5" /></td>
</tr>
</tbody>
</table>
</roving-tabindex>
<script>
spreadsheet.addEventListener("keydown", evt => {
if (evt.target instanceof HTMLLabelElement) {
switch (evt.key) {
// send arrow keys and home/end through to <roving-tabindex>
case "ArrowUp":
case "ArrowDown":
case "ArrowLeft":
case "ArrowRight":
case "Home":
case "End":
break;
// for all other keys, swap in the corresponding <input> element
default:
const input = spreadsheet.querySelector("#" + evt.target.htmlFor);
evt.target.classList.add("hidden");
input.classList.remove("hidden");
input.focus();
break;
}
}
if (evt.target instanceof HTMLInputElement) {
// stop keydown events on <input> elements from reaching <roving-tabindex>
evt.stopPropagation();
const label = spreadsheet.querySelector(`[for="${evt.target.id}"]`);
switch (evt.key) {
case "Enter":
label.innerText = evt.target.value;
// fallthrough
case "Escape":
evt.target.classList.add("hidden");
label.classList.remove("hidden");
label.focus();
break;
}
}
});
spreadsheet.addEventListener("focusout", evt => {
if (evt.target instanceof HTMLInputElement) {
const label = spreadsheet.querySelector(`[for="${evt.target.id}"]`);
label.innerText = evt.target.value;
evt.target.classList.add("hidden");
label.classList.remove("hidden");
}
});
</script>
API
There are three main ways to control <roving-tabindex>
and the focus of the elements it
manages: attributes, methods and
events.
Attributes
Attributes are applied to the <roving-tabindex>
tag in HTML to control its behavior.
Name | Description | Default |
---|---|---|
selector |
A CSS selector that determines which descendant elements should be navigable. | |
direction |
Determines the behavior of the arrow keys.
|
"both" |
columns |
The number of columns each grid row has. | 0 |
loop |
Whether to wrap around to the beginning of the selection when moving past the end, and vice versa. | false |
Methods
<roving-tabindex>
provides methods that can be called with a direct reference to its DOM
node.
Name | Description |
---|---|
focus() |
Return focus to the most recently focused descendant. |
Events
The focus of elements can also be controlled by dispatching custom events, either directly on the
<roving-tabindex>
element itself or on one of its descendants.
Name | Description | Detail |
---|---|---|
rove |
Move focus by a given number of rows or columns. | { rows?: number; cols?: number } |