rovingFocus
elements man ui/behaviors/roving-focusOne Tab stop for a group of items, arrow keys between them: toolbars, tabs,
radio groups, menus, chips. The current item has tabindex="0" and every
other item tabindex="-1". The behavior owns tabindex on the items, so a
template never binds it.
Signaturefunction rovingFocus(container: HTMLElement, opts: RovingOptions): Roving; type Orientation = "horizontal" | "vertical" | "both";
Options
| Option | Type | Default | What it does |
|---|---|---|---|
items |
string |
required | Selector for the items, matched inside the container. |
orientation |
Orientation |
"horizontal" |
Which arrows move. Left and Right flip under rtl. |
loop |
boolean |
true |
Wrap from the last item to the first. |
dir |
"ltr" | "rtl" |
the container's computed direction | Reading direction. |
homeEnd |
boolean |
true |
Home and End jump to the first and last item. |
page |
number |
0 (off) |
PageUp and PageDown move this many items. |
typeahead |
boolean |
false |
Letters jump to a matching item (typeahead (opens in a new tab)). |
focusDisabled |
boolean |
false |
Disabled items stay focusable, as in a menu. |
current |
() => HTMLElement | null |
the first item | The item that holds the tab stop when focus is outside: the selected tab. |
onFocusItem |
(el: HTMLElement) => void |
none | Called after an item receives focus by keyboard. Automatic activation goes here. |
Keys
| Keys | Action |
|---|---|
ArrowRight, ArrowLeft |
Next and previous item in a horizontal or both group; flipped under rtl. |
ArrowDown, ArrowUp |
Next and previous item in a vertical or both group. |
Home, End |
First and last item, with homeEnd. |
PageDown, PageUp |
page items forward or back, stopping at the ends. |
| Letters | The next item whose text starts with what was typed, with typeahead. |
Disabled items (disabled, aria-disabled="true", data-disabled) are
skipped unless focusDisabled. Hidden items hold -1, so an item shown
later by a class or style change joins without a second Tab stop, and a
group started while hidden takes its stop when it is shown. Items added
later are picked up on the next key press or focus; a stop that becomes
disabled or hidden hands the stop to the next usable item. A text field or
another arrow-key widget inside an item keeps its arrows, Home, End and
letters. When the focused item is removed (a deleted chip, a closed tab),
focus moves to the item that took its place, or the one before it, instead
of falling to the body.
The handle
| Member | What it does |
|---|---|
items() |
The usable items in DOM order: visible and, unless focusDisabled, enabled. |
focus(el) |
Moves the tab stop to el and focuses it. |
sync() |
Re-applies tabindex after the items changed. Runs on its own on DOM changes. |
setCurrent(el) |
Moves the tab stop to el without focusing it, when a selection changed from outside. |
release() |
Stops and puts back the tabindex it changed. |
In a template of your own
app/shared/templates/chip-group/index.ehtmlimport { rovingFocus, type Roving } from "@elements/ui/behaviors"; interface ChipGroupState { roving?: Roving; } <ChipGroup (options: string[], value: string = "", private ui: ChipGroupState = {})> <div role="radiogroup" class="chip-group" oninsert={(e) => ui.roving = rovingFocus(e.target as HTMLElement, { items: "[role=radio]", current: () => (e.target as HTMLElement).querySelector("[aria-checked=true]"), onFocusItem: (el) => value = el.dataset.value ?? "", })} onremove={() => ui.roving?.release()}> <button e:for={option of options} type="button" role="radio" class="chip" data-value={option} aria-checked={option === value ? "true" : "false"} onclick={() => value = option}>{option}</button> </div> </ChipGroup>
Tab lands on the checked chip, arrows move between chips, and onFocusItem
selects as focus moves.
Writes
tabindex on every item, put back on release.
Used by
DataTable (opens in a new tab), Kanban (opens in a new tab), Menubar (opens in a new tab), RichText (opens in a new tab), Tabs (opens in a new tab), ToggleGroup (opens in a new tab).