function onKeyDown(e: KeyboardEvent, items: HTMLElement[], i: number): void {
let next = -1;
if (e.key === "ArrowRight") next = (i + 1) % items.length;
if (e.key === "ArrowLeft") next = (i - 1 + items.length) % items.length;
if (next < 0) return;
items[i].tabIndex = -1;
items[next].tabIndex = 0;
items[next].focus();
}
A roving tabindex is a way to make a group of items count as one Tab stop. Inside the group, the user moves with the arrow keys.
How it works:
- Exactly one item has
tabindex="0". All the others havetabindex="-1". - Tab enters the group on the item with
0, and the next Tab leaves the group. - An arrow key moves
0to the next item, sets the old one to-1, and callsfocus()on the new one. - When the user comes back later, focus returns to the last item they used.
Example: a toolbar with 10 buttons, or a row of tabs. Without this, the user presses Tab 10 times to get past the toolbar.
aria-activedescendant is the other way. Real focus stays on one element, usually an input. That element's aria-activedescendant points to the id of the "virtually focused" option. The screen reader announces that option. Use it when focus must stay in a text box while the user arrows through options, as in a combobox (an autocomplete input with a list). The user can keep typing.
In short: roving tabindex moves real focus. aria-activedescendant keeps focus still and points at the active item.
- A roving tabindex makes a group of items a single Tab stop.
- One item has
tabindex="0"and the rest havetabindex="-1". - Arrow keys move the
0and callfocus()on the new item. aria-activedescendantkeeps real focus on one element and points to the active option.- Use
aria-activedescendantwhen focus must stay in an input, like a combobox.