# activeDescendant A 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. ```ts Signatures function 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 ```ehtml app/shared/templates/picker/index.ehtml import { 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()); } ui.input = e.target as HTMLElement}> ``` ## 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](../templates/combobox), [DropdownMenu](../templates/dropdown-menu), [ContextMenu](../templates/context-menu), [Menubar](../templates/menubar), [Select](../templates/select), [TagsInput](../templates/tags-input). `scrollIntoNearest`: [Command](../templates/command).