Skip to content

Commit 5b54d04

Browse files
committed
Added SKILL.md for agents to use this library
1 parent eda97ef commit 5b54d04

1 file changed

Lines changed: 233 additions & 0 deletions

File tree

  • skills/norigin-spatial-navigation-react
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
---
2+
name: norigin-spatial-navigation-react
3+
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.
4+
---
5+
6+
# Norigin Spatial Navigation
7+
8+
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.
9+
10+
**Full documentation:** https://github.com/NoriginMedia/Norigin-Spatial-Navigation/tree/main/docs
11+
12+
When you need details beyond this skill, read the relevant page directly:
13+
14+
- Concepts & hierarchy: `docs/guides/concepts.md`, `docs/guides/focus-hierarchy.md`
15+
- Quick start & install: `docs/guides/quick-start.md`, `docs/guides/installation.md`
16+
- `useFocusable` API: `docs/api-reference/useFocusable.md`
17+
- `SpatialNavigation` API (init, setFocus, pause, etc.): `docs/api-reference/SpatialNavigation.md`
18+
- Boundaries / modals: `docs/guides/focus-boundaries.md`
19+
- Programmatic focus: `docs/guides/programmatic-focus.md`
20+
- Distance calculation tuning: `docs/guides/distance-calculation.md`
21+
- Key mapping (custom remotes): `docs/guides/key-mapping.md`
22+
- Event callbacks: `docs/guides/event-callbacks.md`
23+
- Performance: `docs/guides/performance.md`
24+
- Debugging: `docs/guides/debugging.md`
25+
- Recipes (lists, grids, virtualization): `docs/guides/recipes.md`
26+
- Accessibility labels: `docs/guides/accessibility-labels.md`
27+
- RTL: `docs/guides/rtl-support.md`
28+
29+
## Mental model
30+
31+
Two component shapes exist:
32+
33+
1. **Leaf** — interactive element that actually receives focus. Reads `focused` from `useFocusable`.
34+
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`).
35+
36+
Every focusable component **must** attach the returned `ref` to a real DOM element. Containers **must** wrap children in `<FocusContext.Provider value={focusKey}>` — otherwise children are orphaned at the root and navigation across them breaks.
37+
38+
## Initialization
39+
40+
Call `init` once at app startup (e.g. in `App.tsx`):
41+
42+
```tsx
43+
import { init as initNavigation } from '@noriginmedia/norigin-spatial-navigation';
44+
45+
initNavigation({
46+
debug: false,
47+
visualDebug: false,
48+
throttle: 0,
49+
distanceCalculationMethod: 'corners' // 'center' | 'edges' | 'corners'
50+
});
51+
```
52+
53+
If you ship custom remote keys, also configure key mapping (see `docs/guides/key-mapping.md`).
54+
55+
## Patterns
56+
57+
### Leaf (button, menu item)
58+
59+
```tsx
60+
import { useFocusable } from '@noriginmedia/norigin-spatial-navigation';
61+
62+
function Button({ label, onPress }: Props) {
63+
const { ref, focused } = useFocusable({ onEnterPress: onPress });
64+
return (
65+
<div ref={ref} className={focused ? 'focused' : ''}>
66+
{label}
67+
</div>
68+
);
69+
}
70+
```
71+
72+
### Container
73+
74+
```tsx
75+
import {
76+
useFocusable,
77+
FocusContext
78+
} from '@noriginmedia/norigin-spatial-navigation';
79+
80+
function Menu({ items }: Props) {
81+
const { ref, focusKey, hasFocusedChild } = useFocusable({
82+
trackChildren: true,
83+
saveLastFocusedChild: true
84+
});
85+
86+
return (
87+
<FocusContext.Provider value={focusKey}>
88+
<div ref={ref} className={hasFocusedChild ? 'active' : ''}>
89+
{items.map((i) => (
90+
<MenuItem key={i.id} {...i} />
91+
))}
92+
</div>
93+
</FocusContext.Provider>
94+
);
95+
}
96+
```
97+
98+
### Modal / popup (trap focus + grab on mount)
99+
100+
```tsx
101+
function Popup() {
102+
const { ref, focusKey, focusSelf } = useFocusable({ isFocusBoundary: true });
103+
104+
useEffect(() => {
105+
focusSelf();
106+
}, [focusSelf]);
107+
108+
return (
109+
<FocusContext.Provider value={focusKey}>
110+
<div ref={ref}>{/* buttons */}</div>
111+
</FocusContext.Provider>
112+
);
113+
}
114+
```
115+
116+
For partial trapping (e.g. block only horizontal), use `focusBoundaryDirections`. See `docs/guides/focus-boundaries.md`.
117+
118+
### Programmatic focus from anywhere
119+
120+
```tsx
121+
import {
122+
setFocus,
123+
doesFocusableExist
124+
} from '@noriginmedia/norigin-spatial-navigation';
125+
126+
const KEY = 'primary-cta';
127+
useFocusable({ focusKey: KEY });
128+
129+
if (doesFocusableExist(KEY)) setFocus(KEY);
130+
```
131+
132+
### Conditional focusability
133+
134+
```tsx
135+
const { ref } = useFocusable({ focusable: !disabled });
136+
```
137+
138+
### Blocking / overriding directional input
139+
140+
`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.
141+
142+
## `useFocusable` — params worth knowing
143+
144+
| Param | Purpose |
145+
| --------------------------------------------------- | -------------------------------------------------------------------------- |
146+
| `focusable` | Toggle whether element participates in navigation. Default `true`. |
147+
| `trackChildren` | Enables `hasFocusedChild`. Has a perf cost — only enable when you read it. |
148+
| `saveLastFocusedChild` | Container restores last focused child when re-entered. Default `true`. |
149+
| `isFocusBoundary` / `focusBoundaryDirections` | Trap focus inside the container. |
150+
| `focusKey` | Stable key for `setFocus` / `doesFocusableExist`. |
151+
| `preferredChildFocusKey` | Which child gets initial focus when container is entered. |
152+
| `onEnterPress`, `onArrowPress`, `onFocus`, `onBlur` | Event hooks. See `docs/guides/event-callbacks.md`. |
153+
| `extraProps` | Arbitrary payload forwarded to callbacks (e.g. item id). |
154+
155+
Returned: `ref` (required), `focusKey`, `focused` (leaf), `hasFocusedChild` (container), `focusSelf()`.
156+
157+
## Top-level API surface
158+
159+
```tsx
160+
import {
161+
init,
162+
setFocus,
163+
getCurrentFocusKey,
164+
doesFocusableExist,
165+
navigateByDirection,
166+
pause,
167+
resume,
168+
setKeyMap,
169+
setThrottle,
170+
destroy
171+
} from '@noriginmedia/norigin-spatial-navigation';
172+
```
173+
174+
See `docs/api-reference/SpatialNavigation.md` for the full list and signatures.
175+
176+
## Best practices
177+
178+
**Do**
179+
180+
- Always attach `ref` to a DOM element.
181+
- Wrap container children in `<FocusContext.Provider value={focusKey}>`.
182+
- Enable `trackChildren` only when you actually consume `hasFocusedChild`.
183+
- Use stable `focusKey`s for elements you target programmatically.
184+
- Guard `setFocus` calls with `doesFocusableExist`, especially around route changes / async data.
185+
- Use `isFocusBoundary` for modals, drawers, and overlays.
186+
- Set `focusable: false` on disabled or hidden items rather than unmounting if they may toggle frequently.
187+
188+
**Don't**
189+
190+
- Don't read `focused` on a container — use `hasFocusedChild` instead.
191+
- Don't omit `FocusContext.Provider` on containers; children become unreachable from siblings.
192+
- Don't blindly call `setFocus` on keys that may not be mounted (causes silent focus loss).
193+
- Don't generate `focusKey`s that change on every render — focus state will be lost.
194+
- Don't enable `debug` / `visualDebug` in production builds.
195+
196+
## Debugging
197+
198+
Turn on diagnostics in `init`:
199+
200+
```tsx
201+
init({ debug: true, visualDebug: true });
202+
```
203+
204+
Common issues:
205+
206+
- **Element won't focus**`ref` not attached, or `focusable: false`, or ancestor missing `FocusContext.Provider`.
207+
- **Focus jumps to wrong neighbor** — try a different `distanceCalculationMethod` (`corners` / `center` / `edges`); enable `visualDebug` to see hitboxes.
208+
- **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.
209+
- **Modal leaks focus** — missing `isFocusBoundary` on the modal container.
210+
211+
More: `docs/guides/debugging.md`, `docs/guides/performance.md`.
212+
213+
## Quick reference
214+
215+
```tsx
216+
// Leaf
217+
const { ref, focused } = useFocusable();
218+
219+
// Container
220+
const { ref, focusKey } = useFocusable();
221+
<FocusContext.Provider value={focusKey}>
222+
<div ref={ref}>{children}</div>
223+
</FocusContext.Provider>;
224+
225+
// Focus on mount
226+
const { focusSelf } = useFocusable({ isFocusBoundary: true });
227+
useEffect(() => {
228+
focusSelf();
229+
}, [focusSelf]);
230+
231+
// Manual focus
232+
if (doesFocusableExist('my-key')) setFocus('my-key');
233+
```

0 commit comments

Comments
 (0)