Behaviors
elements man ui/behaviorsBehaviors are reusable user interface functions that handle focus, the
keyboard, positioning and dismissal for you. Trap focus inside a dialog
(focusScope). Move focus through a toolbar or a list with the arrow keys
(rovingFocus). Jump to an item by typing its first letters (typeahead).
Close a popup on Escape or a click outside it (pushLayer). Position a popup
beside its trigger and keep it on screen (float). Animate an element in and
out (presence). Stop the page scrolling behind a modal (lockScroll). Hide
the rest of the page from screen readers (hideOthers). Announce a change to
a screen reader (announce). Drag items with the pointer or the keyboard
(drag). Track the active option in a list (activeDescendant). Each is a
function you call on a DOM element from a template of your own. The shipped
templates call the same functions, so a popup of yours closes and stacks the
way a menu's does, and a scroll lock yours takes nests with a dialog's.
Importimport { focusScope, pushLayer, lockScroll } from "@elements/ui/behaviors";
Each behavior is a function that takes DOM elements and returns a release
function, or an object with release(). Start it in oninsert and release
it in onremove. A controller object held in a private attribute carries
the handle between the two:
app/shared/templates/toolbar/index.ehtmlimport { rovingFocus, type Roving } from "@elements/ui/behaviors"; interface ToolbarState { roving?: Roving; } <Toolbar (label: string, private ui: ToolbarState = {})> <div role="toolbar" aria-label={label} oninsert={(e) => ui.roving = rovingFocus(e.target as HTMLElement, { items: "button" })} onremove={() => ui.roving?.release()}> <slot/> </div> </Toolbar>
A behavior writes to the DOM (a tabindex, a data attribute, a CSS
variable) and puts back what it changed on release. Each page lists what its
behavior writes, so a template never binds the same attribute itself.
Behaviors that stack (dismiss layers, focus scopes, scroll locks, hidden
siblings) count their nesting, so a menu inside a dialog closes on Escape
before the dialog does, and the page unlocks when the last lock goes.
Exports
| Export | Page | What it does |
|---|---|---|
uid(prefix) |
uid (opens in a new tab) | A page-unique id, "menu-1", for aria wiring. |
effect(fn) |
effect (opens in a new tab) | Runs fn now and whenever a reactive value it read changes; returns stop. |
untracked(fn) |
effect (opens in a new tab) | Calls fn without subscribing to what it reads. |
watchValue(read, fn) |
effect (opens in a new tab) | Runs fn(value, previous) when read() changes, not on the first run. |
createPresence(read) |
presence (opens in a new tab) | A Presence matching open's current value, for a private attribute. |
presence(p, read, opts) |
presence (opens in a new tab) | Keeps the Presence in step with open through enter and exit animations. |
animationsDone(el) |
presence (opens in a new tab) | Resolves when every finite animation on el and its subtree has finished. |
focusScope(container, opts) |
focusScope (opens in a new tab) | Moves focus in, traps it, returns it on release. |
focusScopeDepth() |
focusScope (opens in a new tab) | How many focus scopes are open. |
rovingFocus(container, opts) |
rovingFocus (opens in a new tab) | One Tab stop for a group, arrows between its items. |
typeahead(timeout) |
typeahead (opens in a new tab) | A search function: the item whose text starts with what was typed. |
isTypeaheadKey(e) |
typeahead (opens in a new tab) | True for one printable character with no modifier. |
pushLayer(el, opts) |
pushLayer (opens in a new tab) | A dismiss layer: Escape and outside presses, stacked. |
layerCount(), topLayer() |
pushLayer (opens in a new tab) | The number of open layers, and the top one. |
onScrollbar(e) |
pushLayer (opens in a new tab) | True when a press landed on the pressed element's scrollbar. |
float(anchor, el, opts) |
float (opens in a new tab) | Positions el against anchor and follows until stopped. |
computeFloat(anchor, el, opts) |
float (opens in a new tab) | Finds the position once without moving el. |
lockScroll(opts) |
lockScroll (opens in a new tab) | Stops the page scrolling; returns unlock. |
scrollLockCount() |
lockScroll (opens in a new tab) | How many locks are held. |
hideOthers(keep) |
hideOthers (opens in a new tab) | aria-hidden on everything outside keep; returns the release. |
announce(message, politeness, opts) |
announce (opens in a new tab) | Speaks a message through a live region. |
clearAnnouncements(politeness) |
announce (opens in a new tab) | Empties one region or both. |
destroyAnnouncer() |
announce (opens in a new tab) | Removes the regions; the next announce creates them again. |
drag(el, opts) |
drag (opens in a new tab) | Pointer and keyboard dragging with threshold, axis and velocity. |
activeDescendant(focusEl, list, opts) |
activeDescendant (opens in a new tab) | A virtual highlight in list while focus stays on focusEl. |
scrollIntoNearest(item, list) |
activeDescendant (opens in a new tab) | Scrolls an item into view inside its list, never the page. |
gridFocus(grid, opts) |
gridFocus (opens in a new tab) | One Tab stop for a grid, arrows by row and column. |
scrollFocus(root, opts) |
scrollFocus (opens in a new tab) | A scrolling region is a Tab stop while it overflows. |
tabbables(container) |
DOM helpers (opens in a new tab) | Every element Tab can reach, in order. |
isDisabled(el), isVisible(el) |
DOM helpers (opens in a new tab) | Whether an element can be acted on, and whether it renders. |
hiddenWithRoot(el, root) |
DOM helpers (opens in a new tab) | el is inside a root that renders no box: hidden, not gone. |
focusElement(el, opts), focusFirst(els, opts) |
DOM helpers (opens in a new tab) | Focus without scrolling the page. |
clickPointer(e) |
DOM helpers (opens in a new tab) | The pointer behind a click: mouse, touch, pen or none. |
isInside(target, roots) |
DOM helpers (opens in a new tab) | target is inside any of the roots. |
directionOf(el), textValue(el) |
DOM helpers (opens in a new tab) | The writing direction in effect, and the text a typeahead matches. |
Types
Each page lists the types its behavior exports. The names, for an import:
| Page | Types |
|---|---|
| presence (opens in a new tab) | Presence, PresenceOptions, PresencePhase |
| focusScope (opens in a new tab) | FocusScope, FocusScopeOptions, FocusManagerOptions, FocusTarget |
| rovingFocus (opens in a new tab) | Roving, RovingOptions, Orientation |
| typeahead (opens in a new tab) | Typeahead |
| pushLayer (opens in a new tab) | Layer, LayerOptions, DismissReason |
| float (opens in a new tab) | FloatOptions, FloatStop, FloatPlacement, FloatSize, FloatMiddleware, MiddlewareState, OffsetData, OffsetFn, Anchor, Side, LogicalSide, Align |
| lockScroll (opens in a new tab) | ScrollLockOptions |
| announce (opens in a new tab) | Politeness, Announcement, AnnounceOptions |
| drag (opens in a new tab) | Drag, DragOptions, DragState, DragStep, DragKeyboard, DragAxis, DragSource |
| activeDescendant (opens in a new tab) | ActiveDescendant, ActiveDescendantOptions |
| gridFocus (opens in a new tab) | GridFocus, GridFocusOptions, GridPosition |
| scrollFocus (opens in a new tab) | ScrollFocusOptions |