activeDescendant
elements man ui/behaviors/active-descendantA virtual highlight in a list while DOM focus stays on another element: a
combobox input, a command palette's input, a select-only button. The
behavior sets aria-activedescendant on the focus element and
data-highlighted on the item, giving the item an id when it has none. It
never sets aria-selected: selection is the template's.
Signaturesfunction activeDescendant(focusEl: HTMLElement, list: HTMLElement, opts?: ActiveDescendantOptions): ActiveDescendant; function scrollIntoNearest(item: HTMLElement, list: HTMLElement, toTop?: boolean): void;
Start it when the list exists (the popup's oninsert) and release it when
the list goes. scrollIntoNearest is the scroll it uses, exported for a
list that highlights its own items: nearest edge inside the list's scroll
container, never the page, keeping the container's scroll-padding.
Options
| Option | Type | Default | What it does |
|---|---|---|---|
items |
string |
"[role=option]"; "[role=gridcell]" with layout grid, "[role=treeitem]" with layout tree |
Selector for the items, matched inside the list. |
order |
(items: HTMLElement[]) => HTMLElement[] |
DOM order | Puts the items in the order the keys walk them, for a list ranked with CSS order. |
commandKeys |
boolean |
false |
Command palette keys: Ctrl+N and Ctrl+J move down, Ctrl+P and Ctrl+K up, Meta+Arrow to the ends, Alt+Arrow to the next or previous group. |
groups |
string |
"[role=group]" |
With commandKeys: the selector for item groups. |
loop |
boolean |
false |
Wrap from the last item to the first. |
orientation |
"vertical" | "horizontal" |
"vertical" |
Which arrows move. A horizontal list follows reading direction. |
dir |
"ltr" | "rtl" |
the list's computed direction | Reading direction for a horizontal list. |
homeEnd |
boolean |
false on a text input, true otherwise |
Home and End jump to the ends. On a text input they move the caret by default. |
page |
number |
0 (off) |
PageUp and PageDown move this many items. |
typeahead |
boolean |
false |
Letters jump to a matching item, for a select-only combobox. |
focusDisabled |
boolean |
false |
Disabled items can be highlighted, never selected. |
autoHighlight |
boolean |
false |
Keep the first item highlighted whenever nothing is; every search change highlights the first match. |
pointer |
boolean |
true |
The highlight follows the mouse. |
clearOnLeave |
boolean |
false |
The mouse leaving the list, or moving onto a disabled item or a gap, clears the highlight. Ignored with autoHighlight. |
enabled |
() => boolean |
always | Keys are handled only while this returns true: the popup is open. Otherwise they pass through untouched, so ArrowDown can open it. |
current |
() => HTMLElement | null |
none | The item to highlight on start, such as the selected option. |
highlightOnFocus |
boolean |
true when focusEl is the list |
Focusing focusEl with nothing highlighted highlights current() or the first item, without selecting it. |
onHighlight |
(el: HTMLElement | null) => void |
none | The highlight changed, to the item or to nothing. |
onSelect |
(el: HTMLElement, e: Event) => void |
none | Enter on the highlighted item, or a click on an item. |
multiple |
boolean |
false |
Multi-select listbox keys, below. Ignored on a text input. |
onSelectRange |
(items: HTMLElement[], e: KeyboardEvent) => void |
none | A range key selected these items, in DOM order, with multiple. |
selectionFollowsFocus |
boolean |
false |
With multiple: plain arrows, Home, End and Page keys select the item they move to and unselect every other; Ctrl+Arrow moves without selecting and Ctrl+Space toggles. |
onSelectOnly |
(el: HTMLElement, e: KeyboardEvent) => void |
onSelectRange([el]) |
Select only this item, with selectionFollowsFocus. |
layout |
"list" | "grid" | "tree" |
"list" |
The popup's shape. Grid: items are cells in rows, arrows move by cell, Home and End to the row's ends, Ctrl+Home and Ctrl+End to the grid's, Page keys page rows. Tree: Right opens a closed node or enters an open one, Left closes an open node or goes to the parent. |
rows |
string |
"[role=row]" |
With layout grid: the selector for rows. |
onExpand |
(el: HTMLElement, open: boolean, e: KeyboardEvent) => void |
sets aria-expanded itself |
With layout tree: a node opens or closes. |
onMove |
(el: HTMLElement, dir: 1 | -1, e: KeyboardEvent) => void |
none | Alt+Down and Alt+Up on the highlighted item: the template moves it one place. |
onDelete |
(el: HTMLElement, e: KeyboardEvent) => void |
none | Delete or Backspace on the highlighted item of a list that is not a text input. |
Keys
| Keys | Action |
|---|---|
ArrowDown, ArrowUp |
Next and previous item. From nothing, Down goes to the first item and Up to the last. |
Home, End |
First and last item, with homeEnd. |
PageDown, PageUp |
page items forward or back. |
Enter |
Selects the highlighted item. |
Space |
Selects too on a focus element that is not a text input, except while a typeahead search is running. |
| Letters | Jump to a matching item, with typeahead. |
ArrowLeft, ArrowRight |
On an editable combobox with a vertical list: clear the highlight and move the caret. |
Typing, Backspace, Delete |
On an editable combobox: clear the highlight, unless autoHighlight. |
Shift+ArrowDown, Shift+ArrowUp |
With multiple: move and toggle the next item (onSelect). |
Ctrl+Space |
With multiple: toggle the highlighted item. |
Ctrl+ArrowDown, Ctrl+ArrowUp |
With multiple: move without selecting. |
Shift+Space |
With multiple: onSelectRange from the last item selected to the highlight. |
Ctrl+Shift+Home, Ctrl+Shift+End |
With multiple: onSelectRange from the highlight to an end, moving the highlight there. |
Ctrl+A |
With multiple: onSelectRange with every item. |
Alt+ArrowDown, Alt+ArrowUp |
onMove with the highlighted item. |
Cmd works for Ctrl. Disabled items (disabled, aria-disabled,
data-disabled) are skipped unless focusDisabled. An IME composition is
never intercepted.
Pointer: moving a mouse over an item highlights it (on move, not on enter,
so a list scrolling under a still pointer does not steal the highlight;
touch and pen moves are scrolls). A press inside the list keeps focus on the
input, and a click on an enabled item selects it. The highlighted item
scrolls into view inside the list; the first item scrolls the list to its
top so a group heading above it shows. When the highlighted item is
removed, hidden or disabled (the list filtered), the highlight clears, or
moves to the first item with autoHighlight.
The handle
| Member | What it does |
|---|---|
items() |
Usable items in order: visible, and enabled unless focusDisabled. |
highlighted() |
The highlighted item, or null. |
highlight(el, { scroll }) |
Highlights el, or clears with null, and scrolls it into view unless scroll: false. |
first(), last(), next(), prev() |
Move the highlight. |
sync() |
Re-checks the highlight after the items changed. Runs on its own on DOM changes. |
release() |
Stops and clears the highlight. |
In a template of your own
app/shared/templates/picker/index.ehtmlimport { activeDescendant, type ActiveDescendant } from "@elements/ui/behaviors"; interface PickerState { input?: HTMLElement; ad?: ActiveDescendant; } function matches(option: string, query: string): boolean { return option.toLowerCase().includes(query.toLowerCase()); } <Picker (options: string[], value: string = "", private query: string = "", private ui: PickerState = {})> <input role="combobox" aria-expanded="true" aria-autocomplete="list" value={query} oninsert={(e) => ui.input = e.target as HTMLElement}> <ul role="listbox" oninsert={(e) => ui.ad = activeDescendant(ui.input!, e.target as HTMLElement, { autoHighlight: true, onSelect: (el) => { value = el.textContent ?? ""; query = value; }, })} onremove={() => ui.ad?.release()}> <li e:for={option of options.filter((o) => matches(o, query))} role="option" aria-selected={option === value ? "true" : "false"}>{option}</li> </ul> </Picker>
Writes
aria-activedescendant on the focus element; data-highlighted and, when
it has none, an id on the highlighted item. With layout tree and no
onExpand, aria-expanded on the node.
Used by
Combobox (opens in a new tab), DropdownMenu (opens in a new tab),
ContextMenu (opens in a new tab), Menubar (opens in a new tab),
Select (opens in a new tab), TagsInput (opens in a new tab).
scrollIntoNearest: Command (opens in a new tab).