aidos-ui 4.0.0 → 4.0.2

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.
Files changed (99) hide show
  1. package/.claude/settings.local.json +14 -1
  2. package/CLAUDE.md +72 -0
  3. package/dist/BaseButton.js.map +1 -1
  4. package/dist/BaseLink.js.map +1 -1
  5. package/dist/BaseListItem.d.ts +1 -1
  6. package/dist/Calendar.d.ts +2 -2
  7. package/dist/Card.js +1 -1
  8. package/dist/Card.js.map +1 -1
  9. package/dist/DarkMode.js.map +1 -1
  10. package/dist/DatePicker.js.map +1 -1
  11. package/dist/Dialog.d.ts +3 -3
  12. package/dist/Dialog.js +7 -1
  13. package/dist/Dialog.js.map +1 -1
  14. package/dist/Dropdown.js.map +1 -1
  15. package/dist/Icon.d.ts +3 -3
  16. package/dist/Icon.js +2 -21
  17. package/dist/Icon.js.map +1 -1
  18. package/dist/Interactable.js.map +1 -1
  19. package/dist/NextJSRouterState.js.map +1 -1
  20. package/dist/OnVisible.d.ts +1 -1
  21. package/dist/OnVisible.js.map +1 -1
  22. package/dist/Popover.d.ts +3 -3
  23. package/dist/Popover.js +3 -1
  24. package/dist/Popover.js.map +1 -1
  25. package/dist/Providers.d.ts +2 -2
  26. package/dist/RangeInput.d.ts +1 -1
  27. package/dist/RootView.js.map +1 -1
  28. package/dist/Select.d.ts +0 -1
  29. package/dist/Storage.d.ts +2 -1
  30. package/dist/Storage.js.map +1 -1
  31. package/dist/Stream.js.map +1 -1
  32. package/dist/Text.js.map +1 -1
  33. package/dist/TextInput.d.ts +3 -2
  34. package/dist/TextInput.js +13 -4
  35. package/dist/TextInput.js.map +1 -1
  36. package/dist/TextPairing.js.map +1 -1
  37. package/dist/Tooltip.d.ts +2 -2
  38. package/dist/Tooltip.js +3 -2
  39. package/dist/Tooltip.js.map +1 -1
  40. package/dist/aria.js.map +1 -1
  41. package/dist/hash.js.map +1 -1
  42. package/dist/jss.d.ts +11 -6
  43. package/dist/jss.js +47 -2
  44. package/dist/jss.js.map +1 -1
  45. package/dist/numberToBase.js.map +1 -1
  46. package/dist/stateMachineContextFactory.js.map +1 -1
  47. package/dist/useCookie.js.map +1 -1
  48. package/dist/useKeyboard.js.map +1 -1
  49. package/dist/useNavigation.js.map +1 -1
  50. package/dist/useRefEffect.js.map +1 -1
  51. package/next-env.d.ts +2 -1
  52. package/next.config.js +2 -1
  53. package/package.json +27 -20
  54. package/src/BaseListItem.tsx +1 -1
  55. package/src/Calendar.tsx +2 -2
  56. package/src/Card.tsx +1 -1
  57. package/src/Dialog.tsx +10 -4
  58. package/src/Icon.tsx +4 -30
  59. package/src/Popover.tsx +6 -4
  60. package/src/Providers.tsx +1 -1
  61. package/src/RangeInput.tsx +1 -1
  62. package/src/Storage.tsx +1 -1
  63. package/src/TextInput.tsx +17 -5
  64. package/src/Tooltip.tsx +5 -4
  65. package/src/docs/mdx.tsx +40 -0
  66. package/src/docs/searchIndex.ts +1 -1
  67. package/src/jss.tsx +55 -3
  68. package/src/pages/Badge.mdx +136 -14
  69. package/src/pages/Button.mdx +121 -63
  70. package/src/pages/Calendar.mdx +196 -2
  71. package/src/pages/Card.mdx +123 -47
  72. package/src/pages/Checkbox.mdx +103 -28
  73. package/src/pages/Dialog.mdx +172 -7
  74. package/src/pages/FlexLayout.mdx +185 -51
  75. package/src/pages/Hashing.mdx +199 -8
  76. package/src/pages/Icon.mdx +150 -42
  77. package/src/pages/IconButton.mdx +127 -116
  78. package/src/pages/Link.mdx +118 -47
  79. package/src/pages/List.mdx +167 -283
  80. package/src/pages/ListHeader.mdx +157 -27
  81. package/src/pages/Popover.mdx +177 -55
  82. package/src/pages/ProgressBar.mdx +186 -6
  83. package/src/pages/ProgressCircle.mdx +196 -13
  84. package/src/pages/RangeInput.mdx +189 -61
  85. package/src/pages/Tabs.mdx +202 -346
  86. package/src/pages/Text.mdx +102 -81
  87. package/src/pages/TextInput.mdx +169 -44
  88. package/src/pages/TextPairing.mdx +164 -115
  89. package/src/pages/TimeInput.mdx +142 -37
  90. package/src/pages/Tooltip.mdx +99 -12
  91. package/src/pages/_app.tsx +13 -3
  92. package/src/pages/css-in-js.mdx +196 -83
  93. package/src/pages/navigation.mdx +122 -20
  94. package/src/pages/useCookie.mdx +168 -7
  95. package/src/pages/useKeyboard.mdx +242 -5
  96. package/src/pages/useNavigation.mdx +244 -16
  97. package/src/pages/usePromise.mdx +271 -6
  98. package/src/pages/useRefEffect.mdx +230 -28
  99. package/tsconfig.json +13 -4
@@ -1,15 +1,176 @@
1
1
  # useCookie
2
2
 
3
- `useCookie` is a hook that can be used to get acceess to cookies by name, and use them as if they were React component state.
3
+ A React hook for reading and writing browser cookies with React state-like API. Handles SSR gracefully and persists values across page reloads.
4
+
5
+ ---
6
+
7
+ ## Signature
8
+
9
+ ```ts
10
+ function useCookie<T, L>(
11
+ key: string,
12
+ options: {
13
+ initialValue: T;
14
+ loadingValue: L;
15
+ serialize?: (value: T) => string;
16
+ deserialize?: (value: string) => T;
17
+ maxAge?: number;
18
+ }
19
+ ): [T | L, (value: T | ((prev: T | L) => T)) => void]
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Parameters
25
+
26
+ | Parameter | Type | Default | Description |
27
+ |-----------|------|---------|-------------|
28
+ | `key` | `string` | Required | Cookie name |
29
+ | `options.initialValue` | `T` | Required | Value to set if cookie doesn't exist |
30
+ | `options.loadingValue` | `L` | Required | Value to use during SSR or initial load |
31
+ | `options.serialize` | `(T) => string` | `JSON.stringify` | Custom serialization function |
32
+ | `options.deserialize` | `(string) => T` | `JSON.parse` | Custom deserialization function |
33
+ | `options.maxAge` | `number` | 1 year | Cookie expiration in seconds |
34
+
35
+ ---
36
+
37
+ ## Returns
38
+
39
+ Returns a tuple similar to `useState`:
40
+
41
+ 1. **Current value** - The cookie value (or `loadingValue` during SSR)
42
+ 2. **Setter function** - Updates the cookie (accepts value or callback)
43
+
44
+ ---
45
+
46
+ ## Basic usage
47
+
48
+ ```js
49
+ import { useCookie } from "aidos-ui";
50
+
51
+ function Settings() {
52
+ const [theme, setTheme] = useCookie("theme", {
53
+ initialValue: "light",
54
+ loadingValue: "light",
55
+ });
56
+
57
+ return (
58
+ <Button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
59
+ Current: {theme}
60
+ </Button>
61
+ );
62
+ }
63
+ ```
64
+
65
+ ---
66
+
67
+ ## With callback setter
68
+
69
+ Like `useState`, the setter accepts a callback function:
70
+
71
+ ```js
72
+ const [count, setCount] = useCookie("visit-count", {
73
+ initialValue: 0,
74
+ loadingValue: 0,
75
+ });
76
+
77
+ // Increment based on previous value
78
+ setCount((prev) => prev + 1);
79
+ ```
80
+
81
+ ---
82
+
83
+ ## SSR handling
84
+
85
+ The `loadingValue` is used during server-side rendering when cookies aren't available:
4
86
 
5
87
  ```js
6
- const [darkModeEnabled, setDarkModeEnabled] = useCookie("dark-mode", {
7
- initialValue: false,
8
- // loadingValue could be different to initial value for example if we are server rendering,
9
- // in which case we do not have access to cookies and you may decide to assign another
10
- // value, like "pending".
11
- loadingValue: false,
88
+ const [user, setUser] = useCookie("user-preferences", {
89
+ initialValue: { notifications: true },
90
+ loadingValue: null, // Renders nothing during SSR
91
+ });
92
+
93
+ if (user === null) {
94
+ return <Spinner />;
95
+ }
96
+
97
+ return <Settings preferences={user} />;
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Custom serialization
103
+
104
+ For complex data or non-JSON formats:
105
+
106
+ ```js
107
+ const [date, setDate] = useCookie("last-visit", {
108
+ initialValue: new Date(),
109
+ loadingValue: null,
110
+ serialize: (d) => d.toISOString(),
111
+ deserialize: (s) => new Date(s),
12
112
  });
13
113
  ```
14
114
 
15
115
  ---
116
+
117
+ ## Custom expiration
118
+
119
+ Set cookie expiration with `maxAge` (in seconds):
120
+
121
+ ```js
122
+ // Expire in 1 hour
123
+ const [session, setSession] = useCookie("session", {
124
+ initialValue: null,
125
+ loadingValue: null,
126
+ maxAge: 60 * 60, // 1 hour
127
+ });
128
+
129
+ // Expire in 30 days
130
+ const [preferences, setPreferences] = useCookie("prefs", {
131
+ initialValue: {},
132
+ loadingValue: {},
133
+ maxAge: 60 * 60 * 24 * 30, // 30 days
134
+ });
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Helper functions
140
+
141
+ The module also exports utility functions:
142
+
143
+ ```js
144
+ import { hasCookie, getCookie } from "aidos-ui";
145
+
146
+ // Check if cookie exists
147
+ if (hasCookie("user-token")) {
148
+ // ...
149
+ }
150
+
151
+ // Get cookie value directly
152
+ const token = getCookie("user-token");
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Dark mode example
158
+
159
+ ```js
160
+ function App() {
161
+ const [darkMode, setDarkMode] = useCookie("dark-mode", {
162
+ initialValue: false,
163
+ loadingValue: false,
164
+ });
165
+
166
+ useEffect(() => {
167
+ document.body.classList.toggle("dark-mode", darkMode);
168
+ }, [darkMode]);
169
+
170
+ return (
171
+ <Button onClick={() => setDarkMode(!darkMode)}>
172
+ {darkMode ? "Light mode" : "Dark mode"}
173
+ </Button>
174
+ );
175
+ }
176
+ ```
@@ -1,20 +1,257 @@
1
1
  # useKeyboard
2
2
 
3
- This hook can be used to create keyboard shortcuts to make using a website easier and more accessible.
3
+ A React hook for creating keyboard shortcuts. Useful for improving accessibility and power-user workflows.
4
+
5
+ ---
6
+
7
+ ## Signature
8
+
9
+ ```ts
10
+ function useKeyboard<T extends HTMLElement>(
11
+ shortcuts: Array<KeyboardShortcut<T>>
12
+ ): (root: T) => void
13
+ ```
14
+
15
+ ---
16
+
17
+ ## KeyboardShortcut type
18
+
19
+ | Property | Type | Default | Description |
20
+ |----------|------|---------|-------------|
21
+ | `key` | `string` | Required | The key to listen for (case-insensitive) |
22
+ | `metaKey` | `boolean` | - | Require Cmd/Meta key (macOS) |
23
+ | `ctrlKey` | `boolean` | - | Require Ctrl key |
24
+ | `action` | `(root: T) => void` | Required | Function to execute when shortcut triggers |
25
+ | `onlyWhenFocused` | `boolean` | - | Only trigger when the element is focused |
26
+
27
+ ---
28
+
29
+ ## Basic usage
30
+
31
+ ```js
32
+ import { useKeyboard } from "aidos-ui";
33
+
34
+ function SearchInput() {
35
+ const [query, setQuery] = useState("");
36
+
37
+ const ref = useKeyboard([
38
+ {
39
+ key: "Escape",
40
+ onlyWhenFocused: true,
41
+ action: () => setQuery(""),
42
+ },
43
+ ]);
44
+
45
+ return (
46
+ <input
47
+ ref={ref}
48
+ value={query}
49
+ onChange={(e) => setQuery(e.target.value)}
50
+ placeholder="Search..."
51
+ />
52
+ );
53
+ }
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Global shortcuts
59
+
60
+ Create app-wide keyboard shortcuts by attaching to a root element:
61
+
62
+ ```js
63
+ function App() {
64
+ const searchRef = useRef(null);
65
+
66
+ const rootRef = useKeyboard([
67
+ {
68
+ key: "K",
69
+ metaKey: true,
70
+ action: () => searchRef.current?.focus(),
71
+ },
72
+ {
73
+ key: "/",
74
+ action: () => searchRef.current?.focus(),
75
+ },
76
+ ]);
77
+
78
+ return (
79
+ <div ref={rootRef}>
80
+ <SearchInput ref={searchRef} />
81
+ {/* Rest of app */}
82
+ </div>
83
+ );
84
+ }
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Focus management
90
+
91
+ Combine with `onlyWhenFocused` to create context-aware shortcuts:
92
+
93
+ ```js
94
+ function CommandPalette() {
95
+ const [open, setOpen] = useState(false);
96
+ const [query, setQuery] = useState("");
97
+
98
+ const ref = useKeyboard([
99
+ {
100
+ key: "K",
101
+ metaKey: true,
102
+ action: () => setOpen(true),
103
+ },
104
+ {
105
+ key: "Escape",
106
+ onlyWhenFocused: true,
107
+ action: () => {
108
+ if (query) {
109
+ setQuery("");
110
+ } else {
111
+ setOpen(false);
112
+ }
113
+ },
114
+ },
115
+ ]);
116
+
117
+ return (
118
+ <div ref={ref}>
119
+ {open && (
120
+ <Dialog>
121
+ <TextInput
122
+ value={query}
123
+ onValueChange={setQuery}
124
+ placeholder="Type a command..."
125
+ />
126
+ </Dialog>
127
+ )}
128
+ </div>
129
+ );
130
+ }
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Modifier keys
136
+
137
+ Use `metaKey` and `ctrlKey` for modifier-based shortcuts:
4
138
 
5
139
  ```js
6
140
  const ref = useKeyboard([
141
+ // Cmd+S (macOS) / Ctrl+S (Windows/Linux)
142
+ {
143
+ key: "S",
144
+ metaKey: true,
145
+ action: () => saveDocument(),
146
+ },
147
+ // Ctrl+Enter
148
+ {
149
+ key: "Enter",
150
+ ctrlKey: true,
151
+ action: () => submitForm(),
152
+ },
153
+ // Cmd+Shift doesn't have a dedicated prop, but you can handle it:
7
154
  {
8
- key: "K",
155
+ key: "Z",
9
156
  metaKey: true,
10
- action: (root) => root.focus(),
157
+ action: (root) => {
158
+ // Check for Shift in the action if needed
159
+ if (window.event?.shiftKey) {
160
+ redo();
161
+ } else {
162
+ undo();
163
+ }
164
+ },
11
165
  },
166
+ ]);
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Multiple shortcuts
172
+
173
+ Register multiple shortcuts at once:
174
+
175
+ ```js
176
+ const ref = useKeyboard([
177
+ { key: "ArrowUp", action: () => selectPrevious() },
178
+ { key: "ArrowDown", action: () => selectNext() },
179
+ { key: "Enter", action: () => confirm() },
180
+ { key: "Escape", action: () => cancel() },
181
+ { key: "Delete", action: () => remove() },
182
+ { key: "Backspace", action: () => remove() },
183
+ ]);
184
+ ```
185
+
186
+ ---
187
+
188
+ ## With TypeScript
189
+
190
+ The hook is generic and preserves the element type:
191
+
192
+ ```ts
193
+ // HTMLInputElement
194
+ const inputRef = useKeyboard<HTMLInputElement>([
12
195
  {
13
196
  key: "Escape",
14
197
  onlyWhenFocused: true,
15
- action: () => {
16
- setQuery("");
198
+ action: (input) => {
199
+ input.value = ""; // TypeScript knows this is HTMLInputElement
200
+ input.blur();
17
201
  },
18
202
  },
19
203
  ]);
204
+
205
+ // HTMLDivElement
206
+ const divRef = useKeyboard<HTMLDivElement>([
207
+ {
208
+ key: "Space",
209
+ action: (div) => div.click(),
210
+ },
211
+ ]);
20
212
  ```
213
+
214
+ ---
215
+
216
+ ## Common patterns
217
+
218
+ ### Text editor shortcuts
219
+
220
+ ```js
221
+ const ref = useKeyboard([
222
+ { key: "B", metaKey: true, action: () => toggleBold() },
223
+ { key: "I", metaKey: true, action: () => toggleItalic() },
224
+ { key: "U", metaKey: true, action: () => toggleUnderline() },
225
+ { key: "Z", metaKey: true, action: () => undo() },
226
+ ]);
227
+ ```
228
+
229
+ ### Navigation shortcuts
230
+
231
+ ```js
232
+ const ref = useKeyboard([
233
+ { key: "H", action: () => navigate("/") },
234
+ { key: "S", action: () => navigate("/settings") },
235
+ { key: "?", action: () => showHelp() },
236
+ ]);
237
+ ```
238
+
239
+ ### List navigation
240
+
241
+ ```js
242
+ const ref = useKeyboard([
243
+ { key: "J", action: () => selectNext() },
244
+ { key: "K", action: () => selectPrevious() },
245
+ { key: "G", action: () => selectFirst() },
246
+ { key: "G", ctrlKey: true, action: () => selectLast() },
247
+ ]);
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Implementation details
253
+
254
+ - Shortcuts are registered globally via `window.addEventListener`
255
+ - Key matching is case-insensitive
256
+ - The hook cleans up listeners when the component unmounts
257
+ - Built on top of `useRefEffect` for reliable effect management
@@ -1,30 +1,258 @@
1
1
  # useNavigation
2
2
 
3
- The `useNavigation` hook is easy to use but complicated under the hood, so I expect there will be situations in which it will break, especially if the HTML markup is not perfectly correct. The gist of it is that it enables keyboard navigation by manipulating the `tabInde` property for all the elements under a certain element (the root). To the 'active' element we assign the `tabIndex` 0, while every other elements gets assigned `tabIndex` -1. This means that the whole area will be a single tab stop. Then, when we detect the arrow down or right we push change the active element by changing the `tabInde` and calling `element.focus()`.
3
+ A React hook that enables keyboard navigation within a container element. Implements the [WAI-ARIA grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/layout-grids/) for accessible keyboard navigation.
4
4
 
5
- This is following the [grid layout pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/layout-grids/).
5
+ ---
6
+
7
+ ## Signature
8
+
9
+ ```ts
10
+ function useNavigation(options?: {
11
+ autofocus?: boolean;
12
+ rowLength?: number;
13
+ enabled?: boolean;
14
+ }): (root: HTMLElement) => void
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Options
20
+
21
+ | Option | Type | Default | Description |
22
+ |--------|------|---------|-------------|
23
+ | `autofocus` | `boolean` | `false` | Automatically focus the first item on mount |
24
+ | `rowLength` | `number` | `1` | Number of items per row (for grid layouts) |
25
+ | `enabled` | `boolean` | `true` | Enable/disable navigation |
26
+
27
+ ---
28
+
29
+ ## Basic usage
6
30
 
7
31
  ```js
8
- export function List() {
32
+ import { useNavigation } from "aidos-ui";
33
+
34
+ function NavigableList() {
9
35
  const rootRef = useNavigation();
10
36
 
11
37
  return (
12
- <ul aria-label={ariaLabel} ref={rootRef}>
13
- <li>
14
- <button>Button 1</button>
15
- </li>
16
- <li>
17
- <button>Button 2</button>
18
- </li>
19
- <li>
20
- <button>Button 3</button>
21
- </li>
22
- <li>
23
- <button>Button 4</button>
24
- </li>
38
+ <ul ref={rootRef} aria-label="Options">
39
+ <li><button>Option 1</button></li>
40
+ <li><button>Option 2</button></li>
41
+ <li><button>Option 3</button></li>
25
42
  </ul>
26
43
  );
27
44
  }
28
45
  ```
29
46
 
30
47
  ---
48
+
49
+ ## How it works
50
+
51
+ The hook manages `tabIndex` on focusable elements within the container:
52
+
53
+ 1. The "active" element gets `tabIndex="0"`
54
+ 2. All other elements get `tabIndex="-1"`
55
+ 3. This makes the entire area a single Tab stop
56
+ 4. Arrow keys move focus between elements
57
+
58
+ **Keyboard controls:**
59
+ - **Arrow Down / Arrow Right**: Move to next item
60
+ - **Arrow Up / Arrow Left**: Move to previous item
61
+ - Navigation wraps around at the ends
62
+
63
+ ---
64
+
65
+ ## List navigation
66
+
67
+ import { List } from "../List";
68
+ import { ListButtonItem } from "../ListButtonItem";
69
+
70
+ <List ariaLabel="example" navigation={true}>
71
+ <ListButtonItem color="secondary" headline="Headline 1" onClick={() => {}} />
72
+ <ListButtonItem color="secondary" headline="Headline 2" onClick={() => {}} />
73
+ <ListButtonItem
74
+ withDivider={false}
75
+ color="secondary"
76
+ headline="Headline 3"
77
+ onClick={() => {}}
78
+ />
79
+ </List>
80
+
81
+ ```js
82
+ <List ariaLabel="example" navigation={true}>
83
+ <ListButtonItem headline="Headline 1" onClick={() => {}} />
84
+ <ListButtonItem headline="Headline 2" onClick={() => {}} />
85
+ <ListButtonItem headline="Headline 3" onClick={() => {}} />
86
+ </List>
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Disabled navigation
92
+
93
+ <List ariaLabel="example" navigation={false}>
94
+ <ListButtonItem color="secondary" headline="Headline 1" onClick={() => {}} />
95
+ <ListButtonItem color="secondary" headline="Headline 2" onClick={() => {}} />
96
+ <ListButtonItem
97
+ withDivider={false}
98
+ color="secondary"
99
+ headline="Headline 3"
100
+ onClick={() => {}}
101
+ />
102
+ </List>
103
+
104
+ ```js
105
+ <List ariaLabel="example" navigation={false}>
106
+ <ListButtonItem headline="Headline 1" onClick={() => {}} />
107
+ <ListButtonItem headline="Headline 2" onClick={() => {}} />
108
+ <ListButtonItem headline="Headline 3" onClick={() => {}} />
109
+ </List>
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Grid navigation
115
+
116
+ For grid layouts, set `rowLength` to enable proper up/down navigation:
117
+
118
+ ```js
119
+ function IconGrid() {
120
+ const rootRef = useNavigation({ rowLength: 4 });
121
+
122
+ return (
123
+ <div
124
+ ref={rootRef}
125
+ style={{
126
+ display: "grid",
127
+ gridTemplateColumns: "repeat(4, 1fr)",
128
+ }}
129
+ >
130
+ {icons.map((icon) => (
131
+ <button key={icon.name} onClick={() => selectIcon(icon)}>
132
+ <Icon icon={icon.name} />
133
+ </button>
134
+ ))}
135
+ </div>
136
+ );
137
+ }
138
+ ```
139
+
140
+ With `rowLength: 4`:
141
+ - Arrow Left/Right moves horizontally
142
+ - Arrow Up/Down moves vertically (jumps 4 items)
143
+
144
+ ---
145
+
146
+ ## Autofocus
147
+
148
+ Focus the first item automatically when the component mounts:
149
+
150
+ ```js
151
+ function DropdownMenu({ items }) {
152
+ const rootRef = useNavigation({ autofocus: true });
153
+
154
+ return (
155
+ <ul ref={rootRef} role="menu">
156
+ {items.map((item) => (
157
+ <li key={item.id} role="menuitem">
158
+ <button onClick={item.action}>{item.label}</button>
159
+ </li>
160
+ ))}
161
+ </ul>
162
+ );
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Conditional navigation
169
+
170
+ Enable or disable navigation dynamically:
171
+
172
+ ```js
173
+ function EditableList({ isEditing }) {
174
+ const rootRef = useNavigation({
175
+ enabled: !isEditing, // Disable during edit mode
176
+ });
177
+
178
+ return (
179
+ <ul ref={rootRef}>
180
+ {items.map((item) => (
181
+ <li key={item.id}>
182
+ {isEditing ? (
183
+ <TextInput value={item.name} />
184
+ ) : (
185
+ <button>{item.name}</button>
186
+ )}
187
+ </li>
188
+ ))}
189
+ </ul>
190
+ );
191
+ }
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Dynamic lists
197
+
198
+ The hook uses a MutationObserver to handle items being added or removed:
199
+
200
+ ```js
201
+ function DynamicList() {
202
+ const [items, setItems] = useState(initialItems);
203
+ const rootRef = useNavigation();
204
+
205
+ const addItem = () => {
206
+ setItems([...items, { id: Date.now(), name: "New Item" }]);
207
+ };
208
+
209
+ return (
210
+ <div>
211
+ <button onClick={addItem}>Add Item</button>
212
+ <ul ref={rootRef}>
213
+ {items.map((item) => (
214
+ <li key={item.id}>
215
+ <button>{item.name}</button>
216
+ </li>
217
+ ))}
218
+ </ul>
219
+ </div>
220
+ );
221
+ }
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Implementation notes
227
+
228
+ - Uses `queryFocusables()` to find focusable elements (buttons, links, inputs, etc.)
229
+ - Manages focus state via `tabIndex` manipulation
230
+ - Handles wrap-around navigation at list boundaries
231
+ - Observes DOM mutations to handle dynamic content
232
+ - Built on `useRefEffect` for cleanup management
233
+
234
+ ---
235
+
236
+ ## Accessibility
237
+
238
+ This hook helps implement accessible keyboard navigation:
239
+
240
+ - Makes complex widgets keyboard-accessible
241
+ - Follows WAI-ARIA grid pattern recommendations
242
+ - Reduces Tab stops for better keyboard efficiency
243
+ - Works with screen readers
244
+
245
+ For best results, combine with proper ARIA attributes:
246
+
247
+ ```js
248
+ <ul
249
+ ref={rootRef}
250
+ role="listbox"
251
+ aria-label="Select an option"
252
+ >
253
+ <li role="option" aria-selected={selected === 1}>
254
+ <button>Option 1</button>
255
+ </li>
256
+ {/* ... */}
257
+ </ul>
258
+ ```