5-way navigation for React and SolidJS
0

Configure Feed

Select the types of activity you want to include in your feed.

`useNav` to `useNavnode`

Pavel Hrách (May 22, 2026, 12:51 AM +0200) fa7b67bf dc4b5462

+92 -78
+5 -5
README.md
··· 39 39 Now your components can become navigation nodes: 40 40 41 41 ```tsx 42 - import { useNav, horizontalHandler } from "@fiveway/react"; 42 + import { useNavnode, horizontalHandler } from "@fiveway/react"; 43 43 44 44 const items = [ 45 45 { id: "1", label: "One" }, ··· 48 48 ]; 49 49 50 50 function List() { 51 - const nav = useNav("list", horizontalHandler); 51 + const nav = useNavnode("list", horizontalHandler); 52 52 53 53 return ( 54 54 <nav.Context> ··· 62 62 } 63 63 64 64 function Item(props) { 65 - const nav = useNav(props.item.id, undefined, { order: props.order }); 65 + const nav = useNavnode(props.item.id, undefined, { order: props.order }); 66 66 67 67 return <li className={nav.isFocused() && "focused"}>{props.item.label}</li>; 68 68 } ··· 73 73 ## Packages 74 74 75 75 - **@fiveway/core** — Core library: navigation tree, nodes, handlers, actions, and DOM utilities 76 - - **@fiveway/react** — React integration (NavigationProvider, useNav, useDispatchOnEvent) 77 - - **@fiveway/solid** — SolidJS integration (NavigationProvider, createNav, useDispatchOnEvent) 76 + - **@fiveway/react** — React integration (NavigationProvider, useNavnode, useDispatchOnEvent) 77 + - **@fiveway/solid** — SolidJS integration (NavigationProvider, createNavnode, useDispatchOnEvent) 78 78 - **@fiveway/inspector** — Inspector UI for inspecting and debugging the navigation tree in the browser
+3 -3
docs/getting-started.md
··· 44 44 ::: code-group 45 45 46 46 ```jsx [React] 47 - import { useNav, horizontalHandler } from "@fiveway/react"; 47 + import { useNavnode, horizontalHandler } from "@fiveway/react"; 48 48 49 49 const items = [ 50 50 { id: "1", label: "One" }, ··· 53 53 ]; 54 54 55 55 function List() { 56 - const nav = useNav("list", horizontalHandler); 56 + const nav = useNavnode("list", horizontalHandler); 57 57 58 58 return ( 59 59 <nav.Context> ··· 67 67 } 68 68 69 69 function Item(props) { 70 - const nav = useNav(props.item.id, undefined, { order: props.order }); 70 + const nav = useNavnode(props.item.id, undefined, { order: props.order }); 71 71 72 72 return <li className={nav.isFocused() && "focused"}>{props.item.label}</li>; 73 73 }
+4 -4
examples/react-example/src/Benchmark.tsx
··· 1 - import { Nav, useNav, verticalHandler } from "@fiveway/react"; 1 + import { Navnode, useNavnode, verticalHandler } from "@fiveway/react"; 2 2 3 3 const items = Array.from({ length: 10 }, (_, i) => { 4 4 return { id: `item-${i + 1}`, order: i, label: `Item ${i + 1}` }; 5 5 }); 6 6 7 7 export function Items(props: { id: string; order: number; depth: number }) { 8 - const nav = useNav(props.id, verticalHandler, { order: props.order }); 8 + const nav = useNavnode(props.id, verticalHandler, { order: props.order }); 9 9 10 10 if (props.depth === 0) { 11 11 return ( 12 12 <div> 13 13 <nav.Context> 14 14 {items.map((item) => ( 15 - <Nav key={item.id} id={item.id} order={item.order}> 15 + <Navnode key={item.id} id={item.id} order={item.order}> 16 16 {(node) => ( 17 17 <div> 18 18 {item.label} {node.isFocused() ? "(focused)" : ""} 19 19 </div> 20 20 )} 21 - </Nav> 21 + </Navnode> 22 22 ))} 23 23 </nav.Context> 24 24 </div>
+14 -8
examples/react-example/src/ConditionalShowcase.tsx
··· 1 - import { containerHandler, horizontalHandler, itemHandler, Nav, useNav } from "@fiveway/react"; 1 + import { 2 + containerHandler, 3 + horizontalHandler, 4 + itemHandler, 5 + Navnode, 6 + useNavnode, 7 + } from "@fiveway/react"; 2 8 import { useState } from "react"; 3 9 import { flushSync } from "react-dom"; 4 10 5 11 import css from "./Showcase.module.css"; 6 12 7 13 export function ConditionalShowcase() { 8 - const nav = useNav("section", horizontalHandler); 14 + const nav = useNavnode("section", horizontalHandler); 9 15 10 16 const [isOn, setOn] = useState(false); 11 17 ··· 13 19 <div className={css.section} data-is-focused={nav.isFocused()}> 14 20 <div style={{ display: "flex" }}> 15 21 <nav.Context> 16 - <Nav 22 + <Navnode 17 23 id="toggle" 18 24 handler={itemHandler(() => { 19 25 flushSync(() => { ··· 29 35 {isOn ? "hide" : "show"} 30 36 </button> 31 37 )} 32 - </Nav> 38 + </Navnode> 33 39 34 - <Nav id="content" handler={containerHandler}> 40 + <Navnode id="content" handler={containerHandler}> 35 41 {isOn && ( 36 - <Nav 42 + <Navnode 37 43 id="parking" 38 44 handler={itemHandler(() => { 39 45 setOn(false); ··· 45 51 remove 46 52 </button> 47 53 )} 48 - </Nav> 54 + </Navnode> 49 55 )} 50 - </Nav> 56 + </Navnode> 51 57 </nav.Context> 52 58 </div> 53 59 </div>
+2 -2
examples/react-example/src/ExampleBox.tsx
··· 3 3 containerHandler, 4 4 gridItemHandler, 5 5 type NodeId, 6 - useNav, 6 + useNavnode, 7 7 } from "@fiveway/react"; 8 8 import { type ReactNode } from "react"; 9 9 ··· 18 18 }; 19 19 20 20 export function ExampleBox(props: ExampleBoxProps) { 21 - const nav = useNav(props.navId, containerHandler.compose(gridItemHandler(props.gridPos))); 21 + const nav = useNavnode(props.navId, containerHandler.compose(gridItemHandler(props.gridPos))); 22 22 23 23 return ( 24 24 <nav.Context>
+2 -2
examples/react-example/src/ListExample.tsx
··· 3 3 verticalHandler, 4 4 type ComposedHandler, 5 5 type NavigationHandler, 6 - useNav, 6 + useNavnode, 7 7 } from "@fiveway/react"; 8 8 9 9 import { NavItem } from "./NavItem"; ··· 15 15 }) { 16 16 const handler = props.direction === "vertical" ? verticalHandler : horizontalHandler; 17 17 18 - const nav = useNav("list", props.handler ? props.handler(handler) : handler); 18 + const nav = useNavnode("list", props.handler ? props.handler(handler) : handler); 19 19 20 20 return ( 21 21 <div
+2 -2
examples/react-example/src/NavItem.tsx
··· 4 4 type NavigationHandler, 5 5 type NodeId, 6 6 useElementHandler, 7 - useNav, 7 + useNavnode, 8 8 } from "@fiveway/react"; 9 9 10 10 import css from "./NavItem.module.css"; ··· 27 27 export function NavItem(props: NavItemProps) { 28 28 const elementHandler = useElementHandler(); 29 29 30 - const nav = useNav( 30 + const nav = useNavnode( 31 31 props.navId, 32 32 (props.handler ?? itemHandler()).compose(goBackHandler).compose(elementHandler), 33 33 { order: props.order },
+2 -2
examples/react-example/src/Showcase.tsx
··· 6 6 initialHandler, 7 7 itemHandler, 8 8 useNavigationContext, 9 - useNav, 9 + useNavnode, 10 10 useOnFocusChange, 11 11 } from "@fiveway/react"; 12 12 ··· 21 21 22 22 export function Showcase() { 23 23 const { tree } = useNavigationContext(); 24 - const nav = useNav("showcase", gridHandler().compose(initialHandler("start"))); 24 + const nav = useNavnode("showcase", gridHandler().compose(initialHandler("start"))); 25 25 26 26 useOnFocusChange(nav.id, (id) => { 27 27 if (id === null) {
+2 -2
examples/react-example/src/SpatialExample.tsx
··· 1 - import { captureHandler, spatialHandler, useNav } from "@fiveway/react"; 1 + import { captureHandler, spatialHandler, useNavnode } from "@fiveway/react"; 2 2 import { type CSSProperties } from "react"; 3 3 4 4 import { NavItem } from "./NavItem.tsx"; ··· 6 6 import css from "./SpatialExample.module.css"; 7 7 8 8 export function SpatialExample() { 9 - const nav = useNav("spatial", spatialHandler.compose(captureHandler)); 9 + const nav = useNavnode("spatial", spatialHandler.compose(captureHandler)); 10 10 11 11 return ( 12 12 <nav.Context>
+2 -2
examples/react-example/src/virtual/VirtualGridExample.tsx
··· 3 3 gridHandler, 4 4 gridItemHandler, 5 5 itemHandler, 6 - useNav, 6 + useNavnode, 7 7 useOnFocusChange, 8 8 } from "@fiveway/react"; 9 9 import { type CSSProperties, useState } from "react"; ··· 28 28 const windowRange = offsetWindow(rows, itemRowIndex, 1, 2); 29 29 const gridRange: [number, number] = [windowRange[0] * cols, (windowRange[1] + 1) * cols - 1]; 30 30 31 - const nav = useNav( 31 + const nav = useNavnode( 32 32 "virtual-grid", 33 33 gridHandler().compose((node, action, next) => { 34 34 if (action.kind === "focus") {
+2 -2
examples/react-example/src/virtual/VirtualListExample.tsx
··· 1 - import { childLocalId, useNav, useOnFocusChange, verticalHandler } from "@fiveway/react"; 1 + import { childLocalId, useNavnode, useOnFocusChange, verticalHandler } from "@fiveway/react"; 2 2 import { useState } from "react"; 3 3 4 4 import { NavItem } from "../NavItem.tsx"; ··· 12 12 const [listPosition, setListPosition] = useState(0); 13 13 const windowRange = offsetWindow(items.length, listPosition, 3); 14 14 15 - const nav = useNav( 15 + const nav = useNavnode( 16 16 "virtual-list", 17 17 verticalHandler.compose((node, action, next) => { 18 18 if (action.kind === "focus") {
+2 -2
examples/solid-example/src/ExampleBox.tsx
··· 1 1 import { 2 2 type GridItem, 3 3 containerHandler, 4 - createNav, 4 + createNavnode, 5 5 gridItemHandler, 6 6 type NodeId, 7 7 } from "@fiveway/solid"; ··· 18 18 }; 19 19 20 20 export function ExampleBox(props: ExampleBoxProps) { 21 - const nav = createNav( 21 + const nav = createNavnode( 22 22 () => props.navId, 23 23 containerHandler.compose(gridItemHandler(props.gridPos)), 24 24 );
+2 -2
examples/solid-example/src/ListExample.tsx
··· 1 1 import { 2 - createNav, 2 + createNavnode, 3 3 horizontalHandler, 4 4 verticalHandler, 5 5 type ComposedHandler, ··· 16 16 }) { 17 17 const baseHandler = props.direction === "vertical" ? verticalHandler : horizontalHandler; 18 18 19 - const nav = createNav("list", props.handler ? props.handler(baseHandler) : baseHandler); 19 + const nav = createNavnode("list", props.handler ? props.handler(baseHandler) : baseHandler); 20 20 21 21 return ( 22 22 <div
+9 -5
examples/solid-example/src/NavItem.tsx
··· 1 1 import { 2 2 createElementHandler, 3 - createNav, 3 + createNavnode, 4 4 itemHandler, 5 5 type ComposedHandler, 6 6 type NavigationHandler, ··· 28 28 29 29 const baseHandler = props.handler ?? itemHandler(); 30 30 31 - const nav = createNav(props.navId, baseHandler.compose(goBackHandler).compose(elementHandler), { 32 - get order() { 33 - return props.order; 31 + const nav = createNavnode( 32 + props.navId, 33 + baseHandler.compose(goBackHandler).compose(elementHandler), 34 + { 35 + get order() { 36 + return props.order; 37 + }, 34 38 }, 35 - }); 39 + ); 36 40 37 41 return ( 38 42 <div tabIndex={0} onFocus={() => nav.focus()} ref={elementHandler.register} class={css.item}>
+2 -2
examples/solid-example/src/Showcase.tsx
··· 1 1 import { 2 2 captureHandler, 3 - createNav, 3 + createNavnode, 4 4 elementHandler, 5 5 gridHandler, 6 6 gridItemHandler, ··· 21 21 22 22 export function Showcase() { 23 23 const { tree } = useNavigationContext(); 24 - const nav = createNav("showcase", gridHandler().compose(initialHandler("start"))); 24 + const nav = createNavnode("showcase", gridHandler().compose(initialHandler("start"))); 25 25 26 26 useOnFocusChange(nav, (id) => { 27 27 if (id === null) {
+2 -2
examples/solid-example/src/SpatialExample.tsx
··· 1 - import { captureHandler, createNav, spatialHandler } from "@fiveway/solid"; 1 + import { captureHandler, createNavnode, spatialHandler } from "@fiveway/solid"; 2 2 import { type JSX } from "solid-js"; 3 3 4 4 import { NavItem } from "./NavItem.tsx"; ··· 6 6 import css from "./SpatialExample.module.css"; 7 7 8 8 export function SpatialExample() { 9 - const nav = createNav("spatial", spatialHandler.compose(captureHandler)); 9 + const nav = createNavnode("spatial", spatialHandler.compose(captureHandler)); 10 10 11 11 return ( 12 12 <nav.Context>
+2 -2
examples/solid-example/src/virtual/VirtualGridExample.tsx
··· 1 1 import { 2 - createNav, 2 + createNavnode, 3 3 childLocalId, 4 4 gridHandler, 5 5 gridItemHandler, ··· 31 31 (windowRange()[1] + 1) * cols - 1, 32 32 ]; 33 33 34 - const nav = createNav( 34 + const nav = createNavnode( 35 35 "virtual-grid", 36 36 gridHandler().compose((node, action, next) => { 37 37 if (action.kind === "focus") {
+2 -2
examples/solid-example/src/virtual/VirtualListExample.tsx
··· 1 - import { createNav, childLocalId, useOnFocusChange, verticalHandler } from "@fiveway/solid"; 1 + import { createNavnode, childLocalId, useOnFocusChange, verticalHandler } from "@fiveway/solid"; 2 2 import { createMemo, createSignal, For } from "solid-js"; 3 3 4 4 import { NavItem } from "../NavItem.tsx"; ··· 12 12 const [listPosition, setListPosition] = createSignal(0); 13 13 const windowRange = () => offsetWindow(items.length, listPosition(), 3); 14 14 15 - const nav = createNav( 15 + const nav = createNavnode( 16 16 "virtual-list", 17 17 verticalHandler.compose((node, action, next) => { 18 18 if (action.kind === "focus") {
+3 -3
packages/fiveway-react/README.md
··· 41 41 Now your components can become navigation nodes: 42 42 43 43 ```tsx 44 - import { useNav, horizontalHandler } from "@fiveway/react"; 44 + import { useNavnode, horizontalHandler } from "@fiveway/react"; 45 45 46 46 const items = [ 47 47 { id: "1", label: "One" }, ··· 50 50 ]; 51 51 52 52 function List() { 53 - const nav = useNav("list", horizontalHandler); 53 + const nav = useNavnode("list", horizontalHandler); 54 54 55 55 return ( 56 56 <nav.Context> ··· 64 64 } 65 65 66 66 function Item(props) { 67 - const nav = useNav(props.item.id, undefined, { order: props.order }); 67 + const nav = useNavnode(props.item.id, undefined, { order: props.order }); 68 68 69 69 return <li className={nav.isFocused() && "focused"}>{props.item.label}</li>; 70 70 }
+1 -1
packages/fiveway-react/src/index.ts
··· 3 3 4 4 export { NavigationContext, NavigationProvider, useNavigationContext } from "./context.tsx"; 5 5 6 - export { type NavOptions, type NavProps, useNav, Nav } from "./node.tsx"; 6 + export { type NavnodeOptions, type NavnodeProps, useNavnode, Navnode } from "./node.tsx"; 7 7 8 8 export { 9 9 useIsFocused,
+12 -8
packages/fiveway-react/src/node.tsx
··· 26 26 27 27 import { NavigationContext, useNavigationContext } from "./context.tsx"; 28 28 29 - export type NavOptions = { 29 + export type NavnodeOptions = { 30 30 parent?: NodeId; 31 31 order?: number; 32 32 }; 33 33 34 - export type Nav = { 34 + export type Navnode = { 35 35 id: NodeId; 36 36 isFocused: () => boolean; 37 37 focus: (nodeId?: NodeId, options?: FocusNodeOptions) => void; ··· 41 41 42 42 const NULL_NODE = {} as CreatedNavigationNode; 43 43 44 - export function useNav(id: NodeId, handler?: NavigationHandler, options: NavOptions = {}): Nav { 44 + export function useNavnode( 45 + id: NodeId, 46 + handler?: NavigationHandler, 47 + options: NavnodeOptions = {}, 48 + ): Navnode { 45 49 const { tree, parentNode } = useNavigationContext(); 46 50 const parent = options.parent ?? parentNode; 47 51 ··· 78 82 selectNode(tree, id, options); 79 83 }; 80 84 81 - const Context: Nav["Context"] = useCallback( 85 + const Context: Navnode["Context"] = useCallback( 82 86 (props: { children: ReactNode }) => { 83 87 const context = { 84 88 tree: tree, ··· 97 101 return { id: nodeId, isFocused, focus, select, Context }; 98 102 } 99 103 100 - export type NavProps = NavOptions & { 104 + export type NavnodeProps = NavnodeOptions & { 101 105 id: NodeId; 102 106 handler?: NavigationHandler; 103 - children?: ReactNode | ((props: Omit<Nav, "Context">) => ReactNode); 107 + children?: ReactNode | ((props: Omit<Navnode, "Context">) => ReactNode); 104 108 }; 105 109 106 - export function Nav({ children, ...props }: NavProps): ReactNode { 107 - const { Context, ...node } = useNav(props.id, props.handler, props); 110 + export function Navnode({ children, ...props }: NavnodeProps): ReactNode { 111 + const { Context, ...node } = useNavnode(props.id, props.handler, props); 108 112 109 113 return <Context>{typeof children === "function" ? children(node) : children}</Context>; 110 114 }
+3 -3
packages/fiveway-solid/README.md
··· 42 42 Now your components can become navigation nodes: 43 43 44 44 ```tsx 45 - import { createNav, horizontalHandler } from "@fiveway/solid"; 45 + import { createNavnode, horizontalHandler } from "@fiveway/solid"; 46 46 import { For } from "solid-js"; 47 47 48 48 const items = [ ··· 52 52 ]; 53 53 54 54 function List() { 55 - const nav = createNav("list", horizontalHandler); 55 + const nav = createNavnode("list", horizontalHandler); 56 56 57 57 return ( 58 58 <nav.Context> ··· 64 64 } 65 65 66 66 function Item(props) { 67 - const nav = createNav(props.item.id, undefined, { 67 + const nav = createNavnode(props.item.id, undefined, { 68 68 get order() { 69 69 return props.order; 70 70 },
+1 -1
packages/fiveway-solid/src/index.ts
··· 3 3 4 4 export { NavigationContext, NavigationProvider, useNavigationContext } from "./context.tsx"; 5 5 6 - export { type NavOptions, type NavProps, createNav, Nav } from "./node.tsx"; 6 + export { type NavnodeOptions, type NavnodeProps, createNavnode, Navnode } from "./node.tsx"; 7 7 8 8 export { 9 9 useIsFocused,
+11 -11
packages/fiveway-solid/src/node.tsx
··· 25 25 import { useNavigationContext, NavigationContext } from "./context.tsx"; 26 26 import { useIsFocused, useOnFocus } from "./hooks.ts"; 27 27 28 - export type NavOptions = { 28 + export type NavnodeOptions = { 29 29 order?: number; 30 30 parent?: NodeId; 31 31 }; 32 32 33 - export type Nav = { 33 + export type Navnode = { 34 34 (): NodeId; 35 35 isFocused: Accessor<boolean>; 36 36 focus: (nodeId?: NodeId, options?: FocusNodeOptions) => void; ··· 39 39 Context: Component<ParentProps>; 40 40 }; 41 41 42 - export function createNav( 42 + export function createNavnode( 43 43 id: NodeId | Accessor<NodeId>, 44 44 handler?: NavigationHandler, 45 - options: NavOptions = {}, 46 - ): Nav { 45 + options: NavnodeOptions = {}, 46 + ): Navnode { 47 47 const { tree, parentNode } = useNavigationContext(); 48 48 49 49 const parent = () => options.parent ?? parentNode(); ··· 109 109 return handle; 110 110 } 111 111 112 - type NavChildren = JSX.Element | ((props: Omit<Nav, "Context">) => JSX.Element); 112 + type NavnodeChildren = JSX.Element | ((props: Omit<Navnode, "Context">) => JSX.Element); 113 113 114 - export type NavProps = NavOptions & { 114 + export type NavnodeProps = NavnodeOptions & { 115 115 id: NodeId; 116 116 handler?: NavigationHandler; 117 - children?: NavChildren; 117 + children?: NavnodeChildren; 118 118 }; 119 119 120 - export function Nav(props: NavProps): JSX.Element { 121 - const node = createNav(props.id, props.handler, props); 120 + export function Navnode(props: NavnodeProps): JSX.Element { 121 + const node = createNavnode(props.id, props.handler, props); 122 122 123 123 return <node.Context>{resolveNodeChildren(props.children, node)}</node.Context>; 124 124 } 125 125 126 - function resolveNodeChildren(children: NavChildren, node: Nav): JSX.Element { 126 + function resolveNodeChildren(children: NavnodeChildren, node: Navnode): JSX.Element { 127 127 return createMemo(() => 128 128 typeof children === "function" ? children(node) : children, 129 129 ) as unknown as JSX.Element;