···28282929Handlers, along with nodes, are the core of fiveway. Every node has an associated handler that defines its behavior.
3030In their simplest form, handlers are just functions that accept **actions** and return node IDs. fiveway provides a set of handlers for common scenarios.
3131-These handlers can be composed and chained together with custom handlers to create unique custom behavior.
3131+These handlers can be composed together with custom handlers to create unique custom behavior.
32323333When given an action, the navigation tree passes that action to the focused node and its handler. The handler can either return a node that should get focused
3434or pass the action to another (usually parent) node and its handler.
+26-20
docs/api/index.md
···9696function dispatchAction(tree: NavigationTree, action: NavigationAction, node?: NodeId): void;
9797```
98989999-Runs the current focus node’s handler chain with `action`. If the handler chain returns a target id, focus moves there via `focusNode`.
9999+Runs the focused node’s handler with `action`. If the handler returns a target id, focus moves there via `focusNode`.
100100101101Typical source of actions: keyboard mapping (see [DOM](#dom)) or framework helpers.
102102···257257258258## Handlers {#handlers}
259259260260-Handlers implement navigation behavior. Each node has a `NavigationHandler`; composite behavior is built with `chainedHandler` and specialized handlers.
260260+Handlers implement navigation behavior. Each node has a `NavigationHandler`; composite behavior is built with `composeHandlers` and specialized handlers.
261261262262### `HandlerNext`
263263···277277) => NodeId | null;
278278```
279279280280-### `chainedHandler`
280280+### `composeHandlers`
281281282282```ts
283283-type ChainedHandler = NavigationHandler & {
284284- prepend(another: NavigationHandler | ChainedHandler): ChainedHandler;
283283+type HandlerLink = {
284284+ handler: NavigationHandler;
285285+ next: HandlerLink | null;
285286};
286287287287-function chainedHandler(handlers: NavigationHandler | NavigationHandler[] | null): ChainedHandler;
288288+type ComposedHandler = NavigationHandler & {
289289+ link: HandlerLink | null;
290290+ compose(handler: NavigationHandler | ComposedHandler): ComposedHandler;
291291+};
292292+293293+function composeHandlers(handlers: NavigationHandler | NavigationHandler[] | null): ComposedHandler;
288294```
289295290290-Combines handlers so each receives `next` wired to the rest of the chain. Use `.prepend()` to add behavior at the front (for example `selectHandler` on items).
296296+Combines handlers so each receives `next` wired to the handlers that follow. Use `.compose()` to add behavior at the front (for example `selectHandler` on items).
291297292298### `defaultHandler`
293299294300```ts
295295-const defaultHandler: ChainedHandler;
301301+const defaultHandler: ComposedHandler;
296302```
297303298298-Chains `focusHandler()` with `parentHandler` — typical leaf and general-purpose default.
304304+Composes `focusHandler()` with `parentHandler` — typical leaf and general-purpose default.
299305300306### `containerHandler`
301307302308```ts
303303-const containerHandler: ChainedHandler;
309309+const containerHandler: ComposedHandler;
304310```
305311306312Like `defaultHandler`, but empty containers do not keep focus (`focusHandler({ focusWhenEmpty: false })`).
···316322### `itemHandler`
317323318324```ts
319319-function itemHandler(onSelect?: () => void): ChainedHandler;
325325+function itemHandler(onSelect?: () => void): ComposedHandler;
320326```
321327322322-If `onSelect` is provided, prepends `selectHandler(onSelect)` to `defaultHandler`; otherwise returns `defaultHandler`.
328328+If `onSelect` is provided, composes `selectHandler(onSelect)` onto `defaultHandler` via `.compose()`; otherwise returns `defaultHandler`.
323329324330### `focusHandler`
325331···334340function focusHandler(config?: FocusHandlerOptions): NavigationHandler;
335341```
336342337337-Resolves `focus` actions by walking children (respecting `initialHandler` metadata when direction is initial). When `focusWhenEmpty` is `false`, an empty container does not receive focus (used by `containerHandler`, grid, and spatial chains).
343343+Resolves `focus` actions by walking children (respecting `initialHandler` metadata when direction is initial). When `focusWhenEmpty` is `false`, an empty container does not receive focus (used by `containerHandler`, grid, and spatial handlers).
338344339345### `initialHandler`
340346···373379### `verticalHandler` / `horizontalHandler`
374380375381```ts
376376-const verticalHandler: ChainedHandler;
377377-const horizontalHandler: ChainedHandler;
382382+const verticalHandler: ComposedHandler;
383383+const horizontalHandler: ComposedHandler;
378384```
379385380380-Prebuilt chains: `focusHandler` with direction mapping, movement handler, and `parentHandler`.
386386+Prebuilt compositions: `focusHandler` with direction mapping, movement handler, and `parentHandler`.
381387382388### `GridItem`
383389···406412```ts
407413function gridHandler(config?: {
408414 distance?: (direction: NavigationDirection) => (a: GridItem, b: GridItem) => number | null;
409409-}): ChainedHandler;
415415+}): ComposedHandler;
410416```
411417412412-Chains `focusHandler({ focusWhenEmpty: false })`, `gridMovement` (with optional `distance`), and `parentHandler`.
418418+Composes `focusHandler({ focusWhenEmpty: false })`, `gridMovement` (with optional `distance`), and `parentHandler`.
413419414420Optional `distance` overrides how the nearest cell is chosen for each arrow direction; defaults use row/column heuristics.
415421···443449### `spatialHandler`
444450445451```ts
446446-const spatialHandler: ChainedHandler;
452452+const spatialHandler: ComposedHandler;
447453```
448454449455Combines spatial movement with defaults for arrow-key style navigation using rects.
···466472function selectNode(tree: NavigationTree, nodeId: NodeId, options?: SelectNodeOptions): void;
467473```
468474469469-By default focuses `nodeId` first (`focus` defaults to `true`), then runs the `select` action through that node’s handler chain.
475475+By default focuses `nodeId` first (`focus` defaults to `true`), then runs the `select` action through that node’s handler.
470476471477## Metadata & introspection {#metadata-and-introspection}
472478
···159159160160### `ElementHandler`
161161162162-Same shape as [React](/api/react): `ChainedHandler` plus `register`.
162162+Same shape as [React](/api/react): `ComposedHandler` plus `register`.
163163164164### `createElementHandler`
165165
+3-3
docs/guide/handlers.md
···5656### Extending built-in handlers
57575858Built-in handlers can be extended to add additional functionality or modify the current behavior.
5959-The simplest way to do this is by prepending a handler to an existing one:
5959+The simplest way to do this is by composing a handler onto an existing one with `.compose()`:
60606161```ts
6262import { defaultHandler } from "@fiveway/core";
63636464// defaultHandler is a basic composed handler
6565// that can be extended with custom functionality
6666-const customHandler = defaultHandler.prepend((node, action, next) => {
6666+const customHandler = defaultHandler.compose((node, action, next) => {
6767 if (action.kind === "move" && action.direction === "up") {
6868 console.log("moving up");
6969 }
···8080Sometimes you might want to create a completely new handler by combining multiple primitive handlers together. This is a more advanced approach used by the library itself.
81818282```ts
8383-export const horizontalHandler = chainedHandler([
8383+export const horizontalHandler = composeHandlers([
8484 focusHandler({ skipEmpty: true, direction: horizontalFocusDirection }),
8585 horizontalMovementHandler,
8686 parentHandler,
···11import {
22- chainedHandler,
22+ composeHandlers,
33 dispatchAction,
44 spatialItemHandler,
55 registerListener,
66- type ChainedHandler,
66+ type ComposedHandler,
77 type NavigationAction,
88 type NavigationTree,
99} from "@fiveway/core";
1010import { defaultEventMapping, elementHandler } from "@fiveway/core/dom";
1111import { createEffect, createSignal, onCleanup } from "solid-js";
12121313-export type ElementHandler = ChainedHandler & {
1313+export type ElementHandler = ComposedHandler & {
1414 register: (e: HTMLElement | null) => void;
1515};
1616···1919 const position = () => element()?.getBoundingClientRect() ?? null;
20202121 // handlers doen't need to be reactive
2222- const handler = chainedHandler([
2222+ const handler = composeHandlers([
2323 // eslint-disable-next-line solid/reactivity
2424 elementHandler(element),
2525 // eslint-disable-next-line solid/reactivity
+1-1
packages/fiveway/src/index.ts
···5454 itemHandler,
5555} from "./handler/handler.ts";
56565757-export { type ChainedHandler, chainedHandler } from "./handler/chained.ts";
5757+export { type ComposedHandler, composeHandlers } from "./handler/composed.ts";
58585959export {
6060 type FocusDirection,
+2-2
packages/fiveway/src/inspector.test.ts
···2323 expect(inspectHandler(tree, "#/test")).toEqual([{ name: "test" }]);
2424});
25252626-test("chain handler adds fallback info to link handlers", () => {
2626+test("composed handler adds fallback info to link handlers", () => {
2727 const tree = createNavigationTree();
28282929 const handlerWithoutInfo: NavigationHandler = (_node, _action, next) => {
···3535 createNode({
3636 id: "test",
3737 parent: "#",
3838- handler: defaultHandler.prepend(handlerWithoutInfo),
3838+ handler: defaultHandler.compose(handlerWithoutInfo),
3939 }),
4040 );
4141