Interaction hooks

Six hooks carry the keyboard, dismissal, and mount-gating behavior the organisms share. They're exported so a composite widget you build yourself can inherit the same contracts instead of re-implementing them — prefer these over hand-rolled key handlers.

useActiveDescendant

import { useActiveDescendant } from '@elirobinson/react/hooks/useActiveDescendant';

The `aria-activedescendant` half of the WAI-ARIA combobox/listbox pattern: DOM focus stays on the input at all times and the "highlighted" option is conveyed purely by id reference.

Owns one invariant that is easy to get wrong in two different places, which is why it lives here rather than in each widget: the highlighted index must always be inside the current option range. Filtering shrinks that range out from under a previously-valid index, and an unclamped index hands `aria-activedescendant` a dangling id — visually fine, silently broken for assistive tech.

Note that the clamp is applied twice, deliberately. The *derived* `activeIndex` is clamped so the rendered output is always valid, and `moveActive` clamps the stored value again *before* applying its delta. Only clamping the derived value leaves the raw state parked above the valid range, where a single arrow key moves it from (say) 5 to 4 — both of which still clamp to 1, so the highlight appears stuck until the user presses the key enough times to walk back into range.

Used by Combobox and CommandPalette.

useAnchoredPosition

import { useAnchoredPosition } from '@elirobinson/react/hooks/useAnchoredPosition';

Fixed-position layout for a floating panel relative to its trigger, above or below, kept inside the viewport.

Used by Popover, DropdownMenu, Tooltip, Combobox, and DatePicker.

useClickOutside

import { useClickOutside } from '@elirobinson/react/hooks/useClickOutside';

Calls back when a pointer-down lands outside the given refs — the dismissal half of every overlay.

Used by the anchored overlays — Popover, DropdownMenu, Combobox, DatePicker.

useEscapeKey

import { useEscapeKey } from '@elirobinson/react/hooks/useEscapeKey';

Calls back on Escape keydown while enabled — shared dismissal behavior for overlays.

Used by the anchored overlays alongside useClickOutside.

useHasMounted

import { useHasMounted } from '@elirobinson/react/hooks/useHasMounted';

Reports `false` on the server and during the first client render, then `true` once the component has mounted in a browser.

Gate anything that reads a browser global *during render* on this — in this package that means `createPortal(…, document.body)`, which otherwise throws `ReferenceError: document is not defined` when a Next.js or Remix consumer server-renders the tree. Effects and event handlers do not need it: neither runs on the server.

Returning `false` for the first client render (rather than checking `typeof document` directly) is what keeps hydration clean — the client's initial output has to match the server's, so both must skip the portal and the content appears on the commit that follows.

Used by Toaster, PopoverContent, and DropdownMenuContent to skip their portals during server rendering.

useRovingFocus

import { useRovingFocus } from '@elirobinson/react/hooks/useRovingFocus';

Arrow/Home/End traversal for a composite widget that exposes a single tab stop (a "roving tabindex"): Tab moves into the widget, arrows move within it.

Both axes are accepted because the same widget can be laid out either way and a user should not have to guess which; movement wraps at both ends, per the WAI-ARIA authoring practices for tabs and radio groups.

Only the traversal is shared. What a move *means* differs by widget — tabs move focus without selecting, radio-style groups select as they move — so that decision stays with the caller in `onNavigate`.

Used by Tabs and SegmentedControl.