@vielzeug/codex 26.10.1 → 26.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/catalog.json +52 -45
- package/data/llms-full.txt +299 -86
- package/data/llms.txt +1 -1
- package/data/manifest.json +1 -1
- package/data/packages/focus.json +7 -4
- package/data/packages/prism.json +51 -48
- package/data/packages/tandem.json +6 -4
- package/data/packages/tavern.json +15 -12
- package/data/search.json +20 -20
- package/package.json +2 -2
package/data/llms.txt
CHANGED
package/data/manifest.json
CHANGED
package/data/packages/focus.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"apiSource": "export { FocusConfigError, FocusError } from './errors.js';\nexport type {\n GridColumns,\n GridKeyResult,\n GridNavigation,\n GridNavigationAction,\n GridNavigationChange,\n GridNavigationOptions,\n} from './grid-navigation.js';\nexport { createGridNavigation } from './grid-navigation.js';\nexport type {\n ListKeyAction,\n ListKeyResult,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation.js';\nexport { createListNavigation } from './list-navigation.js';\nexport type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus.js';\nexport { captureFocus, rescueFocus, restoreFocus } from './restore-focus.js';\n",
|
|
2
|
+
"apiSource": "export { FocusConfigError, FocusError } from './errors.js';\nexport type {\n GridColumns,\n GridKeyResult,\n GridNavigation,\n GridNavigationAction,\n GridNavigationChange,\n GridNavigationOptions,\n} from './grid-navigation.js';\nexport { createGridNavigation } from './grid-navigation.js';\nexport type {\n ListKeyAction,\n ListKeyResult,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation.js';\nexport { createListNavigation } from './list-navigation.js';\nexport type { QuickLookGrid, QuickLookGridOptions } from './quick-look-grid.js';\nexport { createQuickLookGrid } from './quick-look-grid.js';\nexport type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus.js';\nexport { captureFocus, rescueFocus, restoreFocus } from './restore-focus.js';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: 'Focus: Navigation and restoration'\ndescription: Framework-neutral list navigation and focus restoration primitives.\npackage: focus\ncategory: input\nkeywords: [focus, roving, keyboard, accessibility, list navigation, grid navigation]\nexports: [createListNavigation, createGridNavigation, captureFocus, restoreFocus, rescueFocus]\nrelated: [refine, keymap, ore]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"focus\" />\n\n## Why Focus?\n\nComposite widgets need consistent keyboard navigation and predictable return focus behavior. Focus centralizes those primitives without coupling to component rendering or framework state.\n\n```ts\n// Before\nlist.addEventListener('keydown', (event) => {\n // arrow/home/end bookkeeping, disabled filtering, wrapping\n});\n\n// After\nconst nav = createListNavigation({ getItems });\nlist.addEventListener('keydown', (event) => {\n nav.handleKeydown(event)?.change?.item.focus();\n});\n```\n\n| Feature | Per-component navigation | Focus |\n| --- | --- | --- |\n| Bundle size | n/a | <PackageInfo package=\"focus\" type=\"size\" /> |\n| Zero dependencies | n/a | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| RTL mirroring | Manual | Built in |\n| Grid navigation | Flat only | `createGridNavigation()` |\n| Typeahead | Manual | Optional via `typeahead` |\n| Focus restoration | Manual capture | `captureFocus()` / `restoreFocus()` |\n\n<div class=\"decision-callout\">\n\n**Use Focus when** a widget needs arrow-key navigation, Home/End, and controlled focus restoration.\n\n**Consider direct focus calls when** interaction is a single isolated element with no composite navigation.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/focus\n```\n\n```sh [npm]\nnpm install @vielzeug/focus\n```\n\n```sh [yarn]\nyarn add @vielzeug/focus\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { captureFocus, createListNavigation } from '@vielzeug/focus';\n\nconst restore = captureFocus();\nconst nav = createListNavigation({\n getItems: () => items,\n loop: true,\n});\nconst onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\n\ncontainer.addEventListener('keydown', onKeydown);\n\nrestore();\ncontainer.removeEventListener('keydown', onKeydown);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createListNavigation()`: pure composite navigation with explicit handled/change results\n- `createGridNavigation()`: two-dimensional arrow-key navigation with fixed or measured columns\n- Orientation and direction support: vertical/horizontal/both with LTR/RTL defaults\n- Dynamic item queries: disabled filtering and loop control\n- Optional typeahead: label-based navigation in key-driven lists\n- `captureFocus()`, `restoreFocus()`, and `rescueFocus()`: explicit return-focus and unmount-rescue helpers\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](/refine/): component primitives integrating list navigation.\n- [Keymap](/keymap/): global and scoped keyboard shortcuts.\n- [Ore](/ore/): lifecycle ownership used by consumer components.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
-
"api": "---\ntitle: 'Focus: API Reference'\ndescription: API reference for @vielzeug/focus navigation and restoration primitives.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createListNavigation()` | Build keyboard navigation for composite widgets | Sync | Apply returned changes to DOM focus |\n| `createGridNavigation()` | Build two-dimensional arrow-key navigation for grids | Sync | Columns resolve per navigation: responsive grids need a getter |\n| `restoreFocus()` | Restore focus to a target or fallback | Sync | Returns `false` when neither target can receive focus |\n| `rescueFocus()` | Re-home focus after the focused element unmounts | Sync | Returns `false` when focus is already on a real element |\n| `captureFocus()` | Capture active focus for one later restoration | Sync | The returned function is one-shot |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/focus` | List navigation and focus restoration primitives. |\n\n## Core Functions\n\n### `createListNavigation()`\n\n```ts\nfunction createListNavigation<T>(options: ListNavigationOptions<T>): ListNavigation<T>;\n```\n\nCreates a keyboard navigation controller with an internal active index.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `ListNavigationOptions<T>` | Item lookup, key mapping, navigation, dynamic direction/orientation, and typeahead options. |\n\n**Returns:** `ListNavigation<T>`.\n\n**Example**\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst nav = createListNavigation({\n getItems: () => rows,\n isItemDisabled: (item) => item.matches('[aria-disabled=\"true\"]'),\n});\n\nconst result = nav.handleKeydown(event);\nresult?.change?.item.focus();\n```\n\n| Member | Return | Contract |\n| --- | --- | --- |\n| `handleKeydown(event)` | `ListKeyResult<T> \\| null` | Returns explicit handled/change state for navigation and typeahead keys. |\n| `navigate(action)` | `ListNavigationChange<T> \\| null` | Moves programmatically and returns the committed change. |\n| `set(index)` | `number` | Sets an integer usable index, or resets to `-1`. |\n| `reset()` | `void` | Clears the active index and typeahead sequence. |\n| `getIndex()` | `number` | Returns the current usable index, or `-1`. |\n| `getActiveItem()` | `T \\| undefined` | Returns the item at the current usable index. |\n\nBoundary navigation keys return `{ handled: true, change: null }` and remain consumed. Unrecognized, already-prevented, composing, and disabled events return `null`. Successful typeahead returns a change and follows `typeahead.preventDefault`.\n\nKeys are matched through `matchKey` from `@vielzeug/keymap`, so `keys` overrides accept shortcut patterns with aliases (`esc`, `space`, `up`) and modifiers (`shift+Home`), and modifier state must match exactly: a plain `ArrowDown` binding does not fire on Ctrl+ArrowDown.\n\n---\n\n### `createGridNavigation()`\n\n```ts\nfunction createGridNavigation<T>(options: GridNavigationOptions<T>): GridNavigation<T>;\n```\n\nCreates a two-dimensional keyboard navigation controller: horizontal arrows step one item, vertical arrows step one row (the column count), Home/End jump to the grid's ends.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `GridNavigationOptions<T>` | Item lookup, column resolution, active-index source, key mapping, and wrapping options. |\n\n**Returns:** `GridNavigation<T>`.\n\n**Example**\n\n```ts\nimport { createGridNavigation } from '@vielzeug/focus';\n\nconst grid = createGridNavigation<HTMLElement>({\n columns: 4,\n getActiveIndex: () => tiles().indexOf(document.activeElement as HTMLElement),\n getItems: tiles,\n});\n\nconst result = grid.handleKeydown(event);\nif (result?.change) result.change.item.focus();\n```\n\n| Member | Return | Contract |\n| --- | --- | --- |\n| `handleKeydown(event)` | `GridKeyResult<T> \\| null` | Returns handled state for navigation keys; recognized keys are always consumed, even at clamped edges. |\n| `navigate(action)` | `GridNavigationChange<T> \\| null` | Moves programmatically and returns the committed change. |\n| `set(index)` | `void` | Sets the tracked index for grids without `getActiveIndex`. |\n| `reset()` | `void` | Clears the tracked index. |\n| `getIndex()` | `number` | Returns the active index: derived from `getActiveIndex` when provided, or `-1`. |\n| `getActiveItem()` | `T \\| undefined` | Returns the item at the active index. |\n\nUnlike `createListNavigation`, items are not skipped when disabled: skipping in two dimensions would break row alignment. With no active index, forward moves start at the first item and backward moves at the last. `columns` may be a getter resolved on every navigation, so responsive grids can read a media query and measured grids can read the rendered row length. `FocusConfigError` is thrown when `columns` resolves below `1`.\n\n---\n\n### `restoreFocus()`\n\n```ts\nfunction restoreFocus(target: FocusTarget, options?: RestoreFocusOptions): boolean;\n```\n\nAttempts to focus a connected target that is neither disabled nor inert. Throwing target getters or `focus()` implementations count as failed attempts and fall through to the configured fallback.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `FocusTarget` | Element or getter resolved when `restoreFocus()` is called. |\n| `options` | `RestoreFocusOptions` | Optional lazy fallback and `preventScroll` flag. |\n\n**Returns:** `boolean`: `true` when focus moved to the target or fallback.\n\n**Example**\n\n```ts\nimport { restoreFocus } from '@vielzeug/focus';\n\nrestoreFocus(() => triggerElement, {\n fallback: () => document.body,\n preventScroll: true,\n});\n```\n\n---\n\n### `rescueFocus()`\n\n```ts\nfunction rescueFocus(target: FocusTarget, options?: RestoreFocusOptions): boolean;\n```\n\nHands focus to `target` when focus has been lost to the document body: the state left behind when the focused element unmounts mid-swap, where keydown never reaches a handler.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `FocusTarget` | Element or getter resolved when the rescue runs. |\n| `options` | `RestoreFocusOptions` | Optional lazy fallback and `preventScroll` flag, as in `restoreFocus()`. |\n\n**Returns:** `boolean`: `true` when focus was rescued; `false` when focus is already on a real element or neither target can receive focus.\n\n**Example**\n\n```ts\nimport { rescueFocus } from '@vielzeug/focus';\n\n// After a swap that may have unmounted the focused element.\nrescueFocus(() => dialog.querySelector<HTMLElement>('footer button'));\n```\n\n---\n\n### `captureFocus()`\n\n```ts\nfunction captureFocus(options?: CaptureFocusOptions): FocusRestorer;\n```\n\nCaptures the deepest active element immediately and returns a one-shot restoration function.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `CaptureFocusOptions` | Optional lazy fallback, `preventScroll`, and cancellation signal. |\n\n**Returns:** `FocusRestorer`. Its first call attempts restoration; later calls return `false`.\n\n**Example**\n\n```ts\nimport { captureFocus } from '@vielzeug/focus';\n\nconst restore = captureFocus({ fallback: () => document.body });\n\ndialog.showModal();\ndialog.addEventListener('close', restore, { once: true });\n```\n\n## Types\n\n```ts\ntype MaybeGetter<T> = T | (() => T);\n\ntype ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\ntype ListKeyAction = ListNavigationAction | 'typeahead';\n\ntype ListNavigationChange<T> = {\n readonly action: ListKeyAction;\n readonly event?: KeyboardEvent;\n readonly index: number;\n readonly item: T;\n};\n\ntype ListKeyResult<T> = {\n readonly change: ListNavigationChange<T> | null;\n readonly handled: true;\n};\n\ntype ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n preventDefault?: boolean;\n};\n\ntype ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>; // shortcut patterns matched via @vielzeug/keymap\n loop?: boolean;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\ntype ListNavigation<T> = {\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): ListKeyResult<T> | null;\n navigate(action: ListNavigationAction): ListNavigationChange<T> | null;\n reset(): void;\n set(index: number): number;\n};\n\ntype GridNavigationAction = 'first' | 'last' | 'next' | 'nextRow' | 'prev' | 'prevRow';\n\ntype GridNavigationChange<T> = {\n readonly action: GridNavigationAction;\n readonly event?: KeyboardEvent;\n readonly index: number;\n readonly item: T;\n};\n\ntype GridKeyResult<T> = {\n readonly change: GridNavigationChange<T> | null;\n readonly handled: true;\n};\n\ntype GridColumns = number | (() => number);\n\ntype GridNavigationOptions<T> = {\n columns: GridColumns;\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean>;\n getActiveIndex?: () => number;\n getItems: () => readonly T[];\n keys?: Partial<Record<GridNavigationAction, readonly string[]>>;\n loop?: boolean;\n};\n\ntype GridNavigation<T> = {\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): GridKeyResult<T> | null;\n navigate(action: GridNavigationAction): GridNavigationChange<T> | null;\n reset(): void;\n set(index: number): void;\n};\n\ntype FocusTarget = HTMLElement | SVGElement | null | undefined | (() => HTMLElement | SVGElement | null | undefined);\n\ntype RestoreFocusOptions = {\n fallback?: FocusTarget;\n preventScroll?: boolean;\n};\n\ntype CaptureFocusOptions = RestoreFocusOptions & {\n signal?: AbortSignal;\n};\n\ntype FocusRestorer = () => boolean;\n```\n\n`typeahead.delayMs` defaults to `500`; supplied values must be positive and finite. `preventDefault` defaults to `false`, which preserves editable combobox input. Set it to `true` for menu-style typeahead.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `FocusError` | Base class for every focus-originated error | `instanceof FocusError` catches any focus error |\n| `FocusConfigError` | Invalid navigation configuration | Extends `FocusError`. Thrown for a key assigned to two actions, a non-positive `typeahead.delayMs`, or grid `columns` resolving below `1` |\n",
|
|
6
|
-
"usage": "---\ntitle: 'Focus: Usage Guide'\ndescription: Build keyboard navigation and restore focus across transient browser interfaces.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one navigation model for the items in a composite widget. Focus remains an application side effect: apply the returned change after each key operation.\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst nav = createListNavigation({\n getItems: () => items,\n isItemDisabled: (item) => item.matches('[aria-disabled=\"true\"]'),\n loop: true,\n});\n\nconst onKeydown = (event: KeyboardEvent) => {\n const result = nav.handleKeydown(event);\n result?.change?.item.focus();\n};\n\nlist.addEventListener('keydown', onKeydown);\n```\n\n`createListNavigation()` owns only index and typeahead state. The layer that attaches the event listener owns its cleanup.\n\n## Inspect Keyboard Results\n\n`handleKeydown()` returns `null` when Focus did not handle the event. Recognized keys return explicit handling state:\n\n```ts\nconst result = nav.handleKeydown(event);\n\nif (result?.handled && result.change) {\n console.log(result.change.action, result.change.index, result.change.item);\n}\n```\n\nA boundary arrow may return `{ handled: true, change: null }`: the composite consumed the key but its active item did not move. Events that are already prevented, disabled, or part of IME composition return `null`.\n\nProgrammatic navigation returns only committed changes:\n\n```ts\nnav.navigate('first')?.item.focus();\nnav.navigate('next')?.item.focus();\nnav.navigate('prev')?.item.focus();\nnav.navigate('last')?.item.focus();\n```\n\n`set(index)` accepts integer indexes. Invalid or disabled targets reset navigation to `-1`.\n\n## Configure Orientation and Direction\n\nDirection, orientation, and disabled state may be live getters:\n\n```ts\nconst nav = createListNavigation({\n direction: () => document.documentElement.dir === 'rtl' ? 'rtl' : 'ltr',\n disabled: () => panel.hidden,\n getItems: () => items,\n orientation: () => 'horizontal',\n});\n```\n\nHorizontal arrows mirror in RTL. Vertical arrows do not. Custom key tables override defaults and are matched through `matchKey` from `@vielzeug/keymap`, so entries accept aliases and modifiers and require exact modifier state; assigning one key to multiple actions throws `FocusConfigError`.\n\n## Add Typeahead\n\nProvide stable labels for menu, listbox, or command navigation:\n\n```ts\nconst nav = createListNavigation({\n getItems: () => menuItems,\n typeahead: {\n delayMs: 300,\n getLabel: (item) => item.textContent ?? '',\n preventDefault: true,\n },\n});\n```\n\n`delayMs` defaults to `500` and must be positive and finite. Repeated characters cycle matching items. Set `preventDefault: true` for menu-style typeahead; leave it false when an editable combobox input must receive the printable key.\n\n## Navigate Grids\n\nUse `createGridNavigation()` when items lay out in rows: vertical arrows step one row, horizontal arrows one item, and Home/End jump to the ends. Point `getActiveIndex` at the DOM so focus moved by pointer or Tab stays in sync:\n\n```ts\nimport { createGridNavigation } from '@vielzeug/focus';\n\nconst tiles = () => [...grid.querySelectorAll<HTMLElement>('.tile')];\n\nconst nav = createGridNavigation<HTMLElement>({\n columns: 4,\n getActiveIndex: () => tiles().indexOf(document.activeElement as HTMLElement),\n getItems: tiles,\n});\n\ngrid.addEventListener('keydown', (event) => {\n nav.handleKeydown(event)?.change?.item.focus();\n});\n```\n\n`columns` resolves on every navigation, so responsive grids can read a media query and measured grids can read the rendered row length. Clamping is the default; set `loop: true` to wrap moves around the grid's edges by flat index. Items are never skipped when disabled: skipping in two dimensions would break row alignment.\n\n## Restore Focus\n\nCapture focus before opening a transient surface, then invoke the one-shot restorer when it closes:\n\n```ts\nimport { captureFocus } from '@vielzeug/focus';\n\nconst restore = captureFocus({\n fallback: () => document.querySelector<HTMLElement>('#main'),\n preventScroll: true,\n});\n\ndialog.showModal();\ndialog.addEventListener('close', restore, { once: true });\n```\n\nA supplied signal can cancel pending restoration:\n\n```ts\nconst controller = new AbortController();\nconst restore = captureFocus({ signal: controller.signal });\n\ncontroller.abort();\nrestore(); // false\n```\n\nUse `restoreFocus()` directly when the target is already known. Disconnected, disabled, inert, throwing, or non-focusable targets fall through to the lazy fallback.\n\nWhen the focused element unmounts: browsing a dialog whose swap removes the item that held focus: the browser drops focus to `<body>`, where keydown never reaches a handler. Call `rescueFocus()` after the swap to hand focus to a control that survives it:\n\n```ts\nimport { rescueFocus } from '@vielzeug/focus';\n\nconst onAfterSwap = () => {\n rescueFocus(() => footer.querySelector<HTMLElement>('button'));\n};\n```\n\n`rescueFocus()` returns `false` without touching anything when focus is already on a real element, so it is safe to call from both a watcher and a transition's after-leave hook.\n\n## Framework Integration\n\nCreate navigation once per mounted widget. Framework cleanup removes only the event listener because the navigation model owns no external resources.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\nimport { createListNavigation } from '@vielzeug/focus';\n\nfunction Tabs({ tabs }: { tabs: string[] }) {\n const listRef = useRef<HTMLDivElement>(null);\n const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n useEffect(() => {\n const list = listRef.current;\n if (!list) return;\n\n const nav = createListNavigation({\n getItems: () => tabRefs.current.filter((item): item is HTMLButtonElement => item !== null),\n loop: true,\n orientation: 'horizontal',\n });\n const onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\n\n list.addEventListener('keydown', onKeydown);\n return () => list.removeEventListener('keydown', onKeydown);\n }, []);\n\n return (\n <div ref={listRef} role=\"tablist\">\n {tabs.map((tab, index) => (\n <button key={tab} ref={(element) => { tabRefs.current[index] = element; }}>{tab}</button>\n ))}\n </div>\n );\n}\n```\n\n```ts [Vue]\nimport { onMounted, onUnmounted, ref } from 'vue';\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst list = ref<HTMLElement>();\nlet onKeydown: ((event: KeyboardEvent) => void) | undefined;\n\nonMounted(() => {\n const nav = createListNavigation({ getItems: () => items, orientation: 'horizontal' });\n onKeydown = (event) => nav.handleKeydown(event)?.change?.item.focus();\n list.value?.addEventListener('keydown', onKeydown);\n});\n\nonUnmounted(() => {\n if (onKeydown) list.value?.removeEventListener('keydown', onKeydown);\n});\n```\n\n```ts [Svelte]\nimport { onMount } from 'svelte';\nimport { createListNavigation } from '@vielzeug/focus';\n\nonMount(() => {\n const nav = createListNavigation({ getItems: () => items, orientation: 'horizontal' });\n const onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\n list.addEventListener('keydown', onKeydown);\n return () => list.removeEventListener('keydown', onKeydown);\n});\n```\n\n:::\n\n## Integrate with Keymap\n\nUse Focus for local composite navigation and Keymap for application shortcuts:\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst nav = createListNavigation({ getItems: () => items });\nconst onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\nconst map = createKeymap([\n { id: 'palette', shortcut: 'mod+k', handler: () => openPalette() },\n { id: 'reset', shortcut: 'escape', handler: () => nav.reset() },\n]);\n\nlist.addEventListener('keydown', onKeydown);\nmap.mount(document);\n```\n\n## Best Practices\n\n- Keep item discovery in one live `getItems()` function.\n- Apply focus from returned changes rather than hiding effects in navigation state.\n- Treat `{ handled: true, change: null }` as a consumed boundary key.\n- Ignore events before Focus when an inner widget already called `preventDefault()`.\n- Reset navigation when an overlay closes or its item context changes.\n- Use typeahead only with stable labels, and set `preventDefault` according to whether the keyboard surface is editable.\n- Capture return focus before opening transient surfaces.\n- Rescue focus after swaps that can unmount the focused element.\n- Remove the owning event listener when a widget unmounts.\n",
|
|
4
|
+
"index": "---\ntitle: 'Focus: Navigation and restoration'\ndescription: Framework-neutral list navigation and focus restoration primitives.\npackage: focus\ncategory: input\nkeywords: [focus, roving, keyboard, accessibility, list navigation, grid navigation, quick look]\nexports: [createListNavigation, createGridNavigation, createQuickLookGrid, captureFocus, restoreFocus, rescueFocus]\nrelated: [refine, keymap, ore]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"focus\" />\n\n## Why Focus?\n\nComposite widgets need consistent keyboard navigation and predictable return focus behavior. Focus centralizes those primitives without coupling to component rendering or framework state.\n\n```ts\n// Before\nlist.addEventListener('keydown', (event) => {\n // arrow/home/end bookkeeping, disabled filtering, wrapping\n});\n\n// After\nconst nav = createListNavigation({ getItems });\nlist.addEventListener('keydown', (event) => {\n nav.handleKeydown(event)?.change?.item.focus();\n});\n```\n\n| Feature | Per-component navigation | Focus |\n| --- | --- | --- |\n| Bundle size | n/a | <PackageInfo package=\"focus\" type=\"size\" /> |\n| Zero dependencies | n/a | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| RTL mirroring | Manual | Built in |\n| Grid navigation | Flat only | `createGridNavigation()` |\n| Quick Look grids | Manual Space handling | `createQuickLookGrid()` |\n| Typeahead | Manual | Optional via `typeahead` |\n| Focus restoration | Manual capture | `captureFocus()` / `restoreFocus()` |\n\n<div class=\"decision-callout\">\n\n**Use Focus when** a widget needs arrow-key navigation, Home/End, and controlled focus restoration.\n\n**Consider direct focus calls when** interaction is a single isolated element with no composite navigation.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/focus\n```\n\n```sh [npm]\nnpm install @vielzeug/focus\n```\n\n```sh [yarn]\nyarn add @vielzeug/focus\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { captureFocus, createListNavigation } from '@vielzeug/focus';\n\nconst restore = captureFocus();\nconst nav = createListNavigation({\n getItems: () => items,\n loop: true,\n});\nconst onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\n\ncontainer.addEventListener('keydown', onKeydown);\n\nrestore();\ncontainer.removeEventListener('keydown', onKeydown);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createListNavigation()`: pure composite navigation with explicit handled/change results\n- `createGridNavigation()`: two-dimensional arrow-key navigation with fixed or measured columns\n- `createQuickLookGrid()`: tile-grid arrow browsing with a Space preview key for card pickers\n- Orientation and direction support: vertical/horizontal/both with LTR/RTL defaults\n- Dynamic item queries: disabled filtering and loop control\n- Optional typeahead: label-based navigation in key-driven lists\n- `captureFocus()`, `restoreFocus()`, and `rescueFocus()`: explicit return-focus and unmount-rescue helpers\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](/refine/): component primitives integrating list navigation.\n- [Keymap](/keymap/): global and scoped keyboard shortcuts.\n- [Ore](/ore/): lifecycle ownership used by consumer components.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: 'Focus: API Reference'\ndescription: API reference for @vielzeug/focus navigation and restoration primitives.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createListNavigation()` | Build keyboard navigation for composite widgets | Sync | Apply returned changes to DOM focus |\n| `createGridNavigation()` | Build two-dimensional arrow-key navigation for grids | Sync | Columns resolve per navigation: responsive grids need a getter |\n| `createQuickLookGrid()` | Build arrow-key tile browsing with a preview key for card grids | Sync | The preview key consumes Space: the focused button never activates |\n| `restoreFocus()` | Restore focus to a target or fallback | Sync | Returns `false` when neither target can receive focus |\n| `rescueFocus()` | Re-home focus after the focused element unmounts | Sync | Returns `false` when focus is already on a real element |\n| `captureFocus()` | Capture active focus for one later restoration | Sync | The returned function is one-shot |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/focus` | List navigation and focus restoration primitives. |\n\n## Core Functions\n\n### `createListNavigation()`\n\n```ts\nfunction createListNavigation<T>(options: ListNavigationOptions<T>): ListNavigation<T>;\n```\n\nCreates a keyboard navigation controller with an internal active index.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `ListNavigationOptions<T>` | Item lookup, key mapping, navigation, dynamic direction/orientation, and typeahead options. |\n\n**Returns:** `ListNavigation<T>`.\n\n**Example**\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst nav = createListNavigation({\n getItems: () => rows,\n isItemDisabled: (item) => item.matches('[aria-disabled=\"true\"]'),\n});\n\nconst result = nav.handleKeydown(event);\nresult?.change?.item.focus();\n```\n\n| Member | Return | Contract |\n| --- | --- | --- |\n| `handleKeydown(event)` | `ListKeyResult<T> \\| null` | Returns explicit handled/change state for navigation and typeahead keys. |\n| `navigate(action)` | `ListNavigationChange<T> \\| null` | Moves programmatically and returns the committed change. |\n| `set(index)` | `number` | Sets an integer usable index, or resets to `-1`. |\n| `reset()` | `void` | Clears the active index and typeahead sequence. |\n| `getIndex()` | `number` | Returns the current usable index, or `-1`. |\n| `getActiveItem()` | `T \\| undefined` | Returns the item at the current usable index. |\n\nBoundary navigation keys return `{ handled: true, change: null }` and remain consumed. Unrecognized, already-prevented, composing, and disabled events return `null`. Successful typeahead returns a change and follows `typeahead.preventDefault`.\n\nKeys are matched through `matchKey` from `@vielzeug/keymap`, so `keys` overrides accept shortcut patterns with aliases (`esc`, `space`, `up`) and modifiers (`shift+Home`), and modifier state must match exactly: a plain `ArrowDown` binding does not fire on Ctrl+ArrowDown.\n\n---\n\n### `createGridNavigation()`\n\n```ts\nfunction createGridNavigation<T>(options: GridNavigationOptions<T>): GridNavigation<T>;\n```\n\nCreates a two-dimensional keyboard navigation controller: horizontal arrows step one item, vertical arrows step one row (the column count), Home/End jump to the grid's ends.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `GridNavigationOptions<T>` | Item lookup, column resolution, active-index source, key mapping, and wrapping options. |\n\n**Returns:** `GridNavigation<T>`.\n\n**Example**\n\n```ts\nimport { createGridNavigation } from '@vielzeug/focus';\n\nconst grid = createGridNavigation<HTMLElement>({\n columns: 4,\n getActiveIndex: () => tiles().indexOf(document.activeElement as HTMLElement),\n getItems: tiles,\n});\n\nconst result = grid.handleKeydown(event);\nif (result?.change) result.change.item.focus();\n```\n\n| Member | Return | Contract |\n| --- | --- | --- |\n| `handleKeydown(event)` | `GridKeyResult<T> \\| null` | Returns handled state for navigation keys; recognized keys are always consumed, even at clamped edges. |\n| `navigate(action)` | `GridNavigationChange<T> \\| null` | Moves programmatically and returns the committed change. |\n| `set(index)` | `void` | Sets the tracked index for grids without `getActiveIndex`. |\n| `reset()` | `void` | Clears the tracked index. |\n| `getIndex()` | `number` | Returns the active index: derived from `getActiveIndex` when provided, or `-1`. |\n| `getActiveItem()` | `T \\| undefined` | Returns the item at the active index. |\n\nUnlike `createListNavigation`, items are not skipped when disabled: skipping in two dimensions would break row alignment. With no active index, forward moves start at the first item and backward moves at the last. `columns` may be a getter resolved on every navigation, so responsive grids can read a media query and measured grids can read the rendered row length. `FocusConfigError` is thrown when `columns` resolves below `1`.\n\n---\n\n### `createQuickLookGrid()`\n\n```ts\nfunction createQuickLookGrid(options: QuickLookGridOptions): QuickLookGrid;\n```\n\nCreates keyboard browsing over a tile grid with a Quick Look key: arrows and Home/End move focus through the items (delegating to `createGridNavigation()`) and `inspectKey` previews the item under the event instead of activating it: the pair every card picker needs.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `QuickLookGridOptions` | Item lookup, active-index source, column resolution, preview key, wrapping, and inspection callback. |\n\n**Returns:** `QuickLookGrid`.\n\n**Example**\n\n```ts\nimport { createQuickLookGrid } from '@vielzeug/focus';\n\nconst quickLook = createQuickLookGrid({\n getItems: () => [...grid.querySelectorAll<HTMLElement>('.tile')],\n onInspect: (item, index) => previewCard(index),\n});\n\ngrid.addEventListener('keydown', (event) => quickLook.handleKeydown(event));\n```\n\n| Member | Return | Contract |\n| --- | --- | --- |\n| `handleKeydown(event)` | `void` | Routes one `keydown`: navigation keys move focus to the reported item, `inspectKey` calls `preventDefault()`/`stopPropagation()` then `onInspect(item, index)`. Unrecognized, already-prevented, composing, modifier, and disabled events pass through. |\n\n`columns` defaults to the rendered row length at the active item (items sharing its top edge), which follows the grid's own responsive wrapping without a media query; supply `columns` to read a track list instead. `getActiveIndex` defaults to the item that is or contains `document.activeElement`; supply it when focus sits on a wrapper around the item. `inspectKey` defaults to `' '` (Space), the platform preview key. The recognizer pairs with a pointer long-press at the call site as the touch counterpart of `onInspect`.\n\n---\n\n### `restoreFocus()`\n\n```ts\nfunction restoreFocus(target: FocusTarget, options?: RestoreFocusOptions): boolean;\n```\n\nAttempts to focus a connected target that is neither disabled nor inert. Throwing target getters or `focus()` implementations count as failed attempts and fall through to the configured fallback.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `FocusTarget` | Element or getter resolved when `restoreFocus()` is called. |\n| `options` | `RestoreFocusOptions` | Optional lazy fallback and `preventScroll` flag. |\n\n**Returns:** `boolean`: `true` when focus moved to the target or fallback.\n\n**Example**\n\n```ts\nimport { restoreFocus } from '@vielzeug/focus';\n\nrestoreFocus(() => triggerElement, {\n fallback: () => document.body,\n preventScroll: true,\n});\n```\n\n---\n\n### `rescueFocus()`\n\n```ts\nfunction rescueFocus(target: FocusTarget, options?: RestoreFocusOptions): boolean;\n```\n\nHands focus to `target` when focus has been lost to the document body: the state left behind when the focused element unmounts mid-swap, where keydown never reaches a handler.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `FocusTarget` | Element or getter resolved when the rescue runs. |\n| `options` | `RestoreFocusOptions` | Optional lazy fallback and `preventScroll` flag, as in `restoreFocus()`. |\n\n**Returns:** `boolean`: `true` when focus was rescued; `false` when focus is already on a real element or neither target can receive focus.\n\n**Example**\n\n```ts\nimport { rescueFocus } from '@vielzeug/focus';\n\n// After a swap that may have unmounted the focused element.\nrescueFocus(() => dialog.querySelector<HTMLElement>('footer button'));\n```\n\n---\n\n### `captureFocus()`\n\n```ts\nfunction captureFocus(options?: CaptureFocusOptions): FocusRestorer;\n```\n\nCaptures the deepest active element immediately and returns a one-shot restoration function.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `CaptureFocusOptions` | Optional lazy fallback, `preventScroll`, and cancellation signal. |\n\n**Returns:** `FocusRestorer`. Its first call attempts restoration; later calls return `false`.\n\n**Example**\n\n```ts\nimport { captureFocus } from '@vielzeug/focus';\n\nconst restore = captureFocus({ fallback: () => document.body });\n\ndialog.showModal();\ndialog.addEventListener('close', restore, { once: true });\n```\n\n## Types\n\n```ts\ntype MaybeGetter<T> = T | (() => T);\n\ntype ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\ntype ListKeyAction = ListNavigationAction | 'typeahead';\n\ntype ListNavigationChange<T> = {\n readonly action: ListKeyAction;\n readonly event?: KeyboardEvent;\n readonly index: number;\n readonly item: T;\n};\n\ntype ListKeyResult<T> = {\n readonly change: ListNavigationChange<T> | null;\n readonly handled: true;\n};\n\ntype ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n preventDefault?: boolean;\n};\n\ntype ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>; // shortcut patterns matched via @vielzeug/keymap\n loop?: boolean;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\ntype ListNavigation<T> = {\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): ListKeyResult<T> | null;\n navigate(action: ListNavigationAction): ListNavigationChange<T> | null;\n reset(): void;\n set(index: number): number;\n};\n\ntype GridNavigationAction = 'first' | 'last' | 'next' | 'nextRow' | 'prev' | 'prevRow';\n\ntype GridNavigationChange<T> = {\n readonly action: GridNavigationAction;\n readonly event?: KeyboardEvent;\n readonly index: number;\n readonly item: T;\n};\n\ntype GridKeyResult<T> = {\n readonly change: GridNavigationChange<T> | null;\n readonly handled: true;\n};\n\ntype GridColumns = number | (() => number);\n\ntype GridNavigationOptions<T> = {\n columns: GridColumns;\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean>;\n getActiveIndex?: () => number;\n getItems: () => readonly T[];\n keys?: Partial<Record<GridNavigationAction, readonly string[]>>;\n loop?: boolean;\n};\n\ntype GridNavigation<T> = {\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): GridKeyResult<T> | null;\n navigate(action: GridNavigationAction): GridNavigationChange<T> | null;\n reset(): void;\n set(index: number): void;\n};\n\ntype QuickLookGridOptions = {\n columns?: GridColumns;\n disabled?: MaybeGetter<boolean>;\n getActiveIndex?: () => number;\n getItems: () => readonly HTMLElement[];\n inspectKey?: string;\n loop?: boolean;\n onInspect: (item: HTMLElement, index: number) => void;\n};\n\ntype QuickLookGrid = {\n handleKeydown(event: KeyboardEvent): void;\n};\n\ntype FocusTarget = HTMLElement | SVGElement | null | undefined | (() => HTMLElement | SVGElement | null | undefined);\n\ntype RestoreFocusOptions = {\n fallback?: FocusTarget;\n preventScroll?: boolean;\n};\n\ntype CaptureFocusOptions = RestoreFocusOptions & {\n signal?: AbortSignal;\n};\n\ntype FocusRestorer = () => boolean;\n```\n\n`typeahead.delayMs` defaults to `500`; supplied values must be positive and finite. `preventDefault` defaults to `false`, which preserves editable combobox input. Set it to `true` for menu-style typeahead.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `FocusError` | Base class for every focus-originated error | `instanceof FocusError` catches any focus error |\n| `FocusConfigError` | Invalid navigation configuration | Extends `FocusError`. Thrown for a key assigned to two actions, a non-positive `typeahead.delayMs`, or grid `columns` resolving below `1` |\n",
|
|
6
|
+
"usage": "---\ntitle: 'Focus: Usage Guide'\ndescription: Build keyboard navigation and restore focus across transient browser interfaces.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one navigation model for the items in a composite widget. Focus remains an application side effect: apply the returned change after each key operation.\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst nav = createListNavigation({\n getItems: () => items,\n isItemDisabled: (item) => item.matches('[aria-disabled=\"true\"]'),\n loop: true,\n});\n\nconst onKeydown = (event: KeyboardEvent) => {\n const result = nav.handleKeydown(event);\n result?.change?.item.focus();\n};\n\nlist.addEventListener('keydown', onKeydown);\n```\n\n`createListNavigation()` owns only index and typeahead state. The layer that attaches the event listener owns its cleanup.\n\n## Inspect Keyboard Results\n\n`handleKeydown()` returns `null` when Focus did not handle the event. Recognized keys return explicit handling state:\n\n```ts\nconst result = nav.handleKeydown(event);\n\nif (result?.handled && result.change) {\n console.log(result.change.action, result.change.index, result.change.item);\n}\n```\n\nA boundary arrow may return `{ handled: true, change: null }`: the composite consumed the key but its active item did not move. Events that are already prevented, disabled, or part of IME composition return `null`.\n\nProgrammatic navigation returns only committed changes:\n\n```ts\nnav.navigate('first')?.item.focus();\nnav.navigate('next')?.item.focus();\nnav.navigate('prev')?.item.focus();\nnav.navigate('last')?.item.focus();\n```\n\n`set(index)` accepts integer indexes. Invalid or disabled targets reset navigation to `-1`.\n\n## Configure Orientation and Direction\n\nDirection, orientation, and disabled state may be live getters:\n\n```ts\nconst nav = createListNavigation({\n direction: () => document.documentElement.dir === 'rtl' ? 'rtl' : 'ltr',\n disabled: () => panel.hidden,\n getItems: () => items,\n orientation: () => 'horizontal',\n});\n```\n\nHorizontal arrows mirror in RTL. Vertical arrows do not. Custom key tables override defaults and are matched through `matchKey` from `@vielzeug/keymap`, so entries accept aliases and modifiers and require exact modifier state; assigning one key to multiple actions throws `FocusConfigError`.\n\n## Add Typeahead\n\nProvide stable labels for menu, listbox, or command navigation:\n\n```ts\nconst nav = createListNavigation({\n getItems: () => menuItems,\n typeahead: {\n delayMs: 300,\n getLabel: (item) => item.textContent ?? '',\n preventDefault: true,\n },\n});\n```\n\n`delayMs` defaults to `500` and must be positive and finite. Repeated characters cycle matching items. Set `preventDefault: true` for menu-style typeahead; leave it false when an editable combobox input must receive the printable key.\n\n## Navigate Grids\n\nUse `createGridNavigation()` when items lay out in rows: vertical arrows step one row, horizontal arrows one item, and Home/End jump to the ends. Point `getActiveIndex` at the DOM so focus moved by pointer or Tab stays in sync:\n\n```ts\nimport { createGridNavigation } from '@vielzeug/focus';\n\nconst tiles = () => [...grid.querySelectorAll<HTMLElement>('.tile')];\n\nconst nav = createGridNavigation<HTMLElement>({\n columns: 4,\n getActiveIndex: () => tiles().indexOf(document.activeElement as HTMLElement),\n getItems: tiles,\n});\n\ngrid.addEventListener('keydown', (event) => {\n nav.handleKeydown(event)?.change?.item.focus();\n});\n```\n\n`columns` resolves on every navigation, so responsive grids can read a media query and measured grids can read the rendered row length. Clamping is the default; set `loop: true` to wrap moves around the grid's edges by flat index. Items are never skipped when disabled: skipping in two dimensions would break row alignment.\n\n## Browse Card Grids with Quick Look\n\nUse `createQuickLookGrid()` for card pickers, where arrows walk the tiles and Space previews the focused card instead of activating it: the WAI-ARIA Quick Look convention. The primitive pairs the navigation with the preview key, consumes the Space press so the focused button never toggles, and reports the item through `onInspect`:\n\n```ts\nimport { createQuickLookGrid } from '@vielzeug/focus';\n\nconst tiles = () => [...grid.querySelectorAll<HTMLElement>('.tile')];\n\nconst quickLook = createQuickLookGrid({\n getItems: tiles,\n onInspect: (_item, index) => openPreview(index),\n});\n\ngrid.addEventListener('keydown', (event) => quickLook.handleKeydown(event));\n```\n\nLeave `columns` unset to measure the rendered row length at the focused tile, which follows the grid's own responsive wrapping without a media query. Supply `getActiveIndex` when focus may sit on a wrapper around the item rather than the item itself. Enter keeps activating the focused tile through its own handler: only the preview key is intercepted. A pointer long-press recognizer is the touch counterpart of `onInspect`; pair them at the call site.\n\n## Restore Focus\n\nCapture focus before opening a transient surface, then invoke the one-shot restorer when it closes:\n\n```ts\nimport { captureFocus } from '@vielzeug/focus';\n\nconst restore = captureFocus({\n fallback: () => document.querySelector<HTMLElement>('#main'),\n preventScroll: true,\n});\n\ndialog.showModal();\ndialog.addEventListener('close', restore, { once: true });\n```\n\nA supplied signal can cancel pending restoration:\n\n```ts\nconst controller = new AbortController();\nconst restore = captureFocus({ signal: controller.signal });\n\ncontroller.abort();\nrestore(); // false\n```\n\nUse `restoreFocus()` directly when the target is already known. Disconnected, disabled, inert, throwing, or non-focusable targets fall through to the lazy fallback.\n\nWhen the focused element unmounts: browsing a dialog whose swap removes the item that held focus: the browser drops focus to `<body>`, where keydown never reaches a handler. Call `rescueFocus()` after the swap to hand focus to a control that survives it:\n\n```ts\nimport { rescueFocus } from '@vielzeug/focus';\n\nconst onAfterSwap = () => {\n rescueFocus(() => footer.querySelector<HTMLElement>('button'));\n};\n```\n\n`rescueFocus()` returns `false` without touching anything when focus is already on a real element, so it is safe to call from both a watcher and a transition's after-leave hook.\n\n## Framework Integration\n\nCreate navigation once per mounted widget. Framework cleanup removes only the event listener because the navigation model owns no external resources.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\nimport { createListNavigation } from '@vielzeug/focus';\n\nfunction Tabs({ tabs }: { tabs: string[] }) {\n const listRef = useRef<HTMLDivElement>(null);\n const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n useEffect(() => {\n const list = listRef.current;\n if (!list) return;\n\n const nav = createListNavigation({\n getItems: () => tabRefs.current.filter((item): item is HTMLButtonElement => item !== null),\n loop: true,\n orientation: 'horizontal',\n });\n const onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\n\n list.addEventListener('keydown', onKeydown);\n return () => list.removeEventListener('keydown', onKeydown);\n }, []);\n\n return (\n <div ref={listRef} role=\"tablist\">\n {tabs.map((tab, index) => (\n <button key={tab} ref={(element) => { tabRefs.current[index] = element; }}>{tab}</button>\n ))}\n </div>\n );\n}\n```\n\n```ts [Vue]\nimport { onMounted, onUnmounted, ref } from 'vue';\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst list = ref<HTMLElement>();\nlet onKeydown: ((event: KeyboardEvent) => void) | undefined;\n\nonMounted(() => {\n const nav = createListNavigation({ getItems: () => items, orientation: 'horizontal' });\n onKeydown = (event) => nav.handleKeydown(event)?.change?.item.focus();\n list.value?.addEventListener('keydown', onKeydown);\n});\n\nonUnmounted(() => {\n if (onKeydown) list.value?.removeEventListener('keydown', onKeydown);\n});\n```\n\n```ts [Svelte]\nimport { onMount } from 'svelte';\nimport { createListNavigation } from '@vielzeug/focus';\n\nonMount(() => {\n const nav = createListNavigation({ getItems: () => items, orientation: 'horizontal' });\n const onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\n list.addEventListener('keydown', onKeydown);\n return () => list.removeEventListener('keydown', onKeydown);\n});\n```\n\n:::\n\n## Integrate with Keymap\n\nUse Focus for local composite navigation and Keymap for application shortcuts:\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst nav = createListNavigation({ getItems: () => items });\nconst onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\nconst map = createKeymap([\n { id: 'palette', shortcut: 'mod+k', handler: () => openPalette() },\n { id: 'reset', shortcut: 'escape', handler: () => nav.reset() },\n]);\n\nlist.addEventListener('keydown', onKeydown);\nmap.mount(document);\n```\n\n## Best Practices\n\n- Keep item discovery in one live `getItems()` function.\n- Apply focus from returned changes rather than hiding effects in navigation state.\n- Treat `{ handled: true, change: null }` as a consumed boundary key.\n- Ignore events before Focus when an inner widget already called `preventDefault()`.\n- Reset navigation when an overlay closes or its item context changes.\n- Use typeahead only with stable labels, and set `preventDefault` according to whether the keyboard surface is editable.\n- Capture return focus before opening transient surfaces.\n- Rescue focus after swaps that can unmount the focused element.\n- Remove the owning event listener when a widget unmounts.\n",
|
|
7
7
|
"examples": "---\ntitle: 'Focus: Examples'\ndescription: Worked examples for @vielzeug/focus.\n---\n\n## Examples\n\n- [Roving Tabs Keyboard Navigation](./examples/roving-tabs-keyboard-navigation.md)\n- [Dialog Return Focus Restoration](./examples/dialog-return-focus-restoration.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
"ListNavigationTypeaheadOptions": "export type {\n ListKeyAction,\n ListKeyResult,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation.js';",
|
|
38
38
|
"MaybeGetter": "export type {\n ListKeyAction,\n ListKeyResult,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation.js';",
|
|
39
39
|
"createListNavigation": "export { createListNavigation } from './list-navigation.js';",
|
|
40
|
+
"QuickLookGrid": "export type { QuickLookGrid, QuickLookGridOptions } from './quick-look-grid.js';",
|
|
41
|
+
"QuickLookGridOptions": "export type { QuickLookGrid, QuickLookGridOptions } from './quick-look-grid.js';",
|
|
42
|
+
"createQuickLookGrid": "export { createQuickLookGrid } from './quick-look-grid.js';",
|
|
40
43
|
"CaptureFocusOptions": "export type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus.js';",
|
|
41
44
|
"FocusRestorer": "export type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus.js';",
|
|
42
45
|
"FocusTarget": "export type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus.js';",
|