From 5b54d040b9ce26533a43c8f7cd8741acf5d1de2a Mon Sep 17 00:00:00 2001 From: Dmitriy Bryokhin Date: Mon, 27 Apr 2026 10:51:32 +0200 Subject: [PATCH 1/3] Added SKILL.md for agents to use this library --- .../norigin-spatial-navigation-react/SKILL.md | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 skills/norigin-spatial-navigation-react/SKILL.md diff --git a/skills/norigin-spatial-navigation-react/SKILL.md b/skills/norigin-spatial-navigation-react/SKILL.md new file mode 100644 index 0000000..f70e32e --- /dev/null +++ b/skills/norigin-spatial-navigation-react/SKILL.md @@ -0,0 +1,233 @@ +--- +name: norigin-spatial-navigation-react +description: Use when building or modifying UIs that need directional focus (arrow keys, TV remotes, gamepads) with the Norigin Spatial Navigation library. Covers `useFocusable`, `FocusContext`, programmatic focus, and common Smart TV / set-top-box patterns. +--- + +# Norigin Spatial Navigation + +Library for arrow-key / remote-control focus management in React apps (Smart TVs, set-top boxes, browsers). It calculates the next focusable element based on spatial position, so you don't wire up directional logic manually. + +**Full documentation:** https://github.com/NoriginMedia/Norigin-Spatial-Navigation/tree/main/docs + +When you need details beyond this skill, read the relevant page directly: + +- Concepts & hierarchy: `docs/guides/concepts.md`, `docs/guides/focus-hierarchy.md` +- Quick start & install: `docs/guides/quick-start.md`, `docs/guides/installation.md` +- `useFocusable` API: `docs/api-reference/useFocusable.md` +- `SpatialNavigation` API (init, setFocus, pause, etc.): `docs/api-reference/SpatialNavigation.md` +- Boundaries / modals: `docs/guides/focus-boundaries.md` +- Programmatic focus: `docs/guides/programmatic-focus.md` +- Distance calculation tuning: `docs/guides/distance-calculation.md` +- Key mapping (custom remotes): `docs/guides/key-mapping.md` +- Event callbacks: `docs/guides/event-callbacks.md` +- Performance: `docs/guides/performance.md` +- Debugging: `docs/guides/debugging.md` +- Recipes (lists, grids, virtualization): `docs/guides/recipes.md` +- Accessibility labels: `docs/guides/accessibility-labels.md` +- RTL: `docs/guides/rtl-support.md` + +## Mental model + +Two component shapes exist: + +1. **Leaf** — interactive element that actually receives focus. Reads `focused` from `useFocusable`. +2. **Container** — wraps focusable children. Reads `focusKey` and provides it via `FocusContext.Provider` so the children participate in the hierarchy. Optionally reads `hasFocusedChild` (requires `trackChildren: true`). + +Every focusable component **must** attach the returned `ref` to a real DOM element. Containers **must** wrap children in `` — otherwise children are orphaned at the root and navigation across them breaks. + +## Initialization + +Call `init` once at app startup (e.g. in `App.tsx`): + +```tsx +import { init as initNavigation } from '@noriginmedia/norigin-spatial-navigation'; + +initNavigation({ + debug: false, + visualDebug: false, + throttle: 0, + distanceCalculationMethod: 'corners' // 'center' | 'edges' | 'corners' +}); +``` + +If you ship custom remote keys, also configure key mapping (see `docs/guides/key-mapping.md`). + +## Patterns + +### Leaf (button, menu item) + +```tsx +import { useFocusable } from '@noriginmedia/norigin-spatial-navigation'; + +function Button({ label, onPress }: Props) { + const { ref, focused } = useFocusable({ onEnterPress: onPress }); + return ( +
+ {label} +
+ ); +} +``` + +### Container + +```tsx +import { + useFocusable, + FocusContext +} from '@noriginmedia/norigin-spatial-navigation'; + +function Menu({ items }: Props) { + const { ref, focusKey, hasFocusedChild } = useFocusable({ + trackChildren: true, + saveLastFocusedChild: true + }); + + return ( + +
+ {items.map((i) => ( + + ))} +
+
+ ); +} +``` + +### Modal / popup (trap focus + grab on mount) + +```tsx +function Popup() { + const { ref, focusKey, focusSelf } = useFocusable({ isFocusBoundary: true }); + + useEffect(() => { + focusSelf(); + }, [focusSelf]); + + return ( + +
{/* buttons */}
+
+ ); +} +``` + +For partial trapping (e.g. block only horizontal), use `focusBoundaryDirections`. See `docs/guides/focus-boundaries.md`. + +### Programmatic focus from anywhere + +```tsx +import { + setFocus, + doesFocusableExist +} from '@noriginmedia/norigin-spatial-navigation'; + +const KEY = 'primary-cta'; +useFocusable({ focusKey: KEY }); + +if (doesFocusableExist(KEY)) setFocus(KEY); +``` + +### Conditional focusability + +```tsx +const { ref } = useFocusable({ focusable: !disabled }); +``` + +### Blocking / overriding directional input + +`onArrowPress(direction, props)` — return `true` to allow default navigation, `false` to block it. Useful for paginated rows, custom carousels, or refusing to leave the current container at edges. + +## `useFocusable` — params worth knowing + +| Param | Purpose | +| --------------------------------------------------- | -------------------------------------------------------------------------- | +| `focusable` | Toggle whether element participates in navigation. Default `true`. | +| `trackChildren` | Enables `hasFocusedChild`. Has a perf cost — only enable when you read it. | +| `saveLastFocusedChild` | Container restores last focused child when re-entered. Default `true`. | +| `isFocusBoundary` / `focusBoundaryDirections` | Trap focus inside the container. | +| `focusKey` | Stable key for `setFocus` / `doesFocusableExist`. | +| `preferredChildFocusKey` | Which child gets initial focus when container is entered. | +| `onEnterPress`, `onArrowPress`, `onFocus`, `onBlur` | Event hooks. See `docs/guides/event-callbacks.md`. | +| `extraProps` | Arbitrary payload forwarded to callbacks (e.g. item id). | + +Returned: `ref` (required), `focusKey`, `focused` (leaf), `hasFocusedChild` (container), `focusSelf()`. + +## Top-level API surface + +```tsx +import { + init, + setFocus, + getCurrentFocusKey, + doesFocusableExist, + navigateByDirection, + pause, + resume, + setKeyMap, + setThrottle, + destroy +} from '@noriginmedia/norigin-spatial-navigation'; +``` + +See `docs/api-reference/SpatialNavigation.md` for the full list and signatures. + +## Best practices + +**Do** + +- Always attach `ref` to a DOM element. +- Wrap container children in ``. +- Enable `trackChildren` only when you actually consume `hasFocusedChild`. +- Use stable `focusKey`s for elements you target programmatically. +- Guard `setFocus` calls with `doesFocusableExist`, especially around route changes / async data. +- Use `isFocusBoundary` for modals, drawers, and overlays. +- Set `focusable: false` on disabled or hidden items rather than unmounting if they may toggle frequently. + +**Don't** + +- Don't read `focused` on a container — use `hasFocusedChild` instead. +- Don't omit `FocusContext.Provider` on containers; children become unreachable from siblings. +- Don't blindly call `setFocus` on keys that may not be mounted (causes silent focus loss). +- Don't generate `focusKey`s that change on every render — focus state will be lost. +- Don't enable `debug` / `visualDebug` in production builds. + +## Debugging + +Turn on diagnostics in `init`: + +```tsx +init({ debug: true, visualDebug: true }); +``` + +Common issues: + +- **Element won't focus** — `ref` not attached, or `focusable: false`, or ancestor missing `FocusContext.Provider`. +- **Focus jumps to wrong neighbor** — try a different `distanceCalculationMethod` (`corners` / `center` / `edges`); enable `visualDebug` to see hitboxes. +- **Focus lost on re-render** — `focusKey` is unstable, or the previously focused node unmounted without a fallback. Use `preferredChildFocusKey` or `setFocus` after the new tree mounts. +- **Modal leaks focus** — missing `isFocusBoundary` on the modal container. + +More: `docs/guides/debugging.md`, `docs/guides/performance.md`. + +## Quick reference + +```tsx +// Leaf +const { ref, focused } = useFocusable(); + +// Container +const { ref, focusKey } = useFocusable(); + +
{children}
+
; + +// Focus on mount +const { focusSelf } = useFocusable({ isFocusBoundary: true }); +useEffect(() => { + focusSelf(); +}, [focusSelf]); + +// Manual focus +if (doesFocusableExist('my-key')) setFocus('my-key'); +``` From 86c3d34fba3a07694d2810f09adf1ac968a0e915 Mon Sep 17 00:00:00 2001 From: Dmitriy Bryokhin Date: Mon, 27 Apr 2026 11:07:39 +0200 Subject: [PATCH 2/3] Readme note on how to install skill --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 896d961..3753e9b 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,12 @@ For more detailed documentation and usage examples, visit our [Developer Portal] 1. Use & benefits of using the Norigin Spatial Navigation library on Smart TVs [here](https://medium.com/p/77ed944d7be7). +## Agentic Skill for Norigin Spatial Navigation + +```bash +npx skills add NoriginMedia/Norigin-Spatial-Navigation +``` + ## Contributing Please follow the [Contribution Guide](https://github.com/NoriginMedia/Norigin-Spatial-Navigation/blob/master/CONTRIBUTING.md) From 5abb82b2eb031651313e8646e1f20d96b2b84bdd Mon Sep 17 00:00:00 2001 From: Dmitriy Bryokhin Date: Mon, 27 Apr 2026 11:53:01 +0200 Subject: [PATCH 3/3] Updated imports to monorepo packages --- skills/norigin-spatial-navigation-react/SKILL.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skills/norigin-spatial-navigation-react/SKILL.md b/skills/norigin-spatial-navigation-react/SKILL.md index f70e32e..f5a0508 100644 --- a/skills/norigin-spatial-navigation-react/SKILL.md +++ b/skills/norigin-spatial-navigation-react/SKILL.md @@ -40,7 +40,7 @@ Every focusable component **must** attach the returned `ref` to a real DOM eleme Call `init` once at app startup (e.g. in `App.tsx`): ```tsx -import { init as initNavigation } from '@noriginmedia/norigin-spatial-navigation'; +import { init as initNavigation } from '@noriginmedia/norigin-spatial-navigation-core'; initNavigation({ debug: false, @@ -57,7 +57,7 @@ If you ship custom remote keys, also configure key mapping (see `docs/guides/key ### Leaf (button, menu item) ```tsx -import { useFocusable } from '@noriginmedia/norigin-spatial-navigation'; +import { useFocusable } from '@noriginmedia/norigin-spatial-navigation-react'; function Button({ label, onPress }: Props) { const { ref, focused } = useFocusable({ onEnterPress: onPress }); @@ -75,7 +75,7 @@ function Button({ label, onPress }: Props) { import { useFocusable, FocusContext -} from '@noriginmedia/norigin-spatial-navigation'; +} from '@noriginmedia/norigin-spatial-navigation-react'; function Menu({ items }: Props) { const { ref, focusKey, hasFocusedChild } = useFocusable({ @@ -121,7 +121,7 @@ For partial trapping (e.g. block only horizontal), use `focusBoundaryDirections` import { setFocus, doesFocusableExist -} from '@noriginmedia/norigin-spatial-navigation'; +} from '@noriginmedia/norigin-spatial-navigation-core'; const KEY = 'primary-cta'; useFocusable({ focusKey: KEY }); @@ -168,7 +168,7 @@ import { setKeyMap, setThrottle, destroy -} from '@noriginmedia/norigin-spatial-navigation'; +} from '@noriginmedia/norigin-spatial-navigation-core'; ``` See `docs/api-reference/SpatialNavigation.md` for the full list and signatures.