@typed/ui 1.0.0-beta.4 → 1.0.0-beta.5

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 (157) hide show
  1. package/README.md +53 -9
  2. package/dist/Alert.d.ts +78 -0
  3. package/dist/Alert.d.ts.map +1 -0
  4. package/dist/Alert.js +37 -0
  5. package/dist/Button.d.ts +144 -0
  6. package/dist/Button.d.ts.map +1 -0
  7. package/dist/Button.js +38 -0
  8. package/dist/Carousel.d.ts +547 -0
  9. package/dist/Carousel.d.ts.map +1 -0
  10. package/dist/Carousel.js +387 -0
  11. package/dist/Checkbox.d.ts +203 -0
  12. package/dist/Checkbox.d.ts.map +1 -0
  13. package/dist/Checkbox.js +140 -0
  14. package/dist/Collection.d.ts +254 -0
  15. package/dist/Collection.d.ts.map +1 -0
  16. package/dist/Collection.js +218 -0
  17. package/dist/Combobox.d.ts +516 -0
  18. package/dist/Combobox.d.ts.map +1 -0
  19. package/dist/Combobox.js +371 -0
  20. package/dist/Component.d.ts +127 -0
  21. package/dist/Component.d.ts.map +1 -0
  22. package/dist/Component.js +60 -0
  23. package/dist/Composite.d.ts +823 -0
  24. package/dist/Composite.d.ts.map +1 -0
  25. package/dist/Composite.js +615 -0
  26. package/dist/Dialog.d.ts +544 -0
  27. package/dist/Dialog.d.ts.map +1 -0
  28. package/dist/Dialog.js +355 -0
  29. package/dist/Disclosure.d.ts +219 -0
  30. package/dist/Disclosure.d.ts.map +1 -0
  31. package/dist/Disclosure.js +128 -0
  32. package/dist/Dom/Events.d.ts +122 -0
  33. package/dist/Dom/Events.d.ts.map +1 -0
  34. package/dist/Dom/Events.js +192 -0
  35. package/dist/Dom/Props.d.ts +161 -0
  36. package/dist/Dom/Props.d.ts.map +1 -0
  37. package/dist/Dom/Props.js +110 -0
  38. package/dist/Dom/Refs.d.ts +58 -0
  39. package/dist/Dom/Refs.d.ts.map +1 -0
  40. package/dist/Dom/Refs.js +61 -0
  41. package/dist/Dom/Render.d.ts +59 -0
  42. package/dist/Dom/Render.d.ts.map +1 -0
  43. package/dist/Dom/Render.js +71 -0
  44. package/dist/Dom/Types.d.ts +570 -0
  45. package/dist/Dom/Types.d.ts.map +1 -0
  46. package/dist/Dom/Types.js +1 -0
  47. package/dist/Dom/index.d.ts +20 -0
  48. package/dist/Dom/index.d.ts.map +1 -0
  49. package/dist/Dom/index.js +8 -0
  50. package/dist/Dom.d.ts +14 -0
  51. package/dist/Dom.d.ts.map +1 -0
  52. package/dist/Dom.js +13 -0
  53. package/dist/Focusable.d.ts +85 -0
  54. package/dist/Focusable.d.ts.map +1 -0
  55. package/dist/Focusable.js +35 -0
  56. package/dist/Form.d.ts +1695 -0
  57. package/dist/Form.d.ts.map +1 -0
  58. package/dist/Form.js +987 -0
  59. package/dist/Grid.d.ts +569 -0
  60. package/dist/Grid.d.ts.map +1 -0
  61. package/dist/Grid.js +379 -0
  62. package/dist/Group.d.ts +147 -0
  63. package/dist/Group.d.ts.map +1 -0
  64. package/dist/Group.js +63 -0
  65. package/dist/Heading.d.ts +86 -0
  66. package/dist/Heading.d.ts.map +1 -0
  67. package/dist/Heading.js +48 -0
  68. package/dist/Hovercard.d.ts +297 -0
  69. package/dist/Hovercard.d.ts.map +1 -0
  70. package/dist/Hovercard.js +188 -0
  71. package/dist/HttpRouter.d.ts +129 -6
  72. package/dist/HttpRouter.d.ts.map +1 -1
  73. package/dist/HttpRouter.js +196 -53
  74. package/dist/Link.d.ts +67 -28
  75. package/dist/Link.d.ts.map +1 -1
  76. package/dist/Link.js +91 -37
  77. package/dist/Listbox.d.ts +437 -0
  78. package/dist/Listbox.d.ts.map +1 -0
  79. package/dist/Listbox.js +316 -0
  80. package/dist/Menu.d.ts +972 -0
  81. package/dist/Menu.d.ts.map +1 -0
  82. package/dist/Menu.js +731 -0
  83. package/dist/Menubar.d.ts +367 -0
  84. package/dist/Menubar.d.ts.map +1 -0
  85. package/dist/Menubar.js +265 -0
  86. package/dist/Meter.d.ts +217 -0
  87. package/dist/Meter.d.ts.map +1 -0
  88. package/dist/Meter.js +94 -0
  89. package/dist/NativeDetails.d.ts +41 -0
  90. package/dist/NativeDetails.d.ts.map +1 -0
  91. package/dist/NativeDetails.js +40 -0
  92. package/dist/NativeDialog.d.ts +64 -0
  93. package/dist/NativeDialog.d.ts.map +1 -0
  94. package/dist/NativeDialog.js +50 -0
  95. package/dist/NativePopover.d.ts +41 -0
  96. package/dist/NativePopover.d.ts.map +1 -0
  97. package/dist/NativePopover.js +46 -0
  98. package/dist/Popover.d.ts +241 -0
  99. package/dist/Popover.d.ts.map +1 -0
  100. package/dist/Popover.js +141 -0
  101. package/dist/RadioGroup.d.ts +432 -0
  102. package/dist/RadioGroup.d.ts.map +1 -0
  103. package/dist/RadioGroup.js +291 -0
  104. package/dist/Role.d.ts +64 -0
  105. package/dist/Role.d.ts.map +1 -0
  106. package/dist/Role.js +27 -0
  107. package/dist/Select.d.ts +529 -0
  108. package/dist/Select.d.ts.map +1 -0
  109. package/dist/Select.js +407 -0
  110. package/dist/Separator.d.ts +54 -0
  111. package/dist/Separator.d.ts.map +1 -0
  112. package/dist/Separator.js +26 -0
  113. package/dist/Slider.d.ts +188 -0
  114. package/dist/Slider.d.ts.map +1 -0
  115. package/dist/Slider.js +96 -0
  116. package/dist/SpinButton.d.ts +188 -0
  117. package/dist/SpinButton.d.ts.map +1 -0
  118. package/dist/SpinButton.js +96 -0
  119. package/dist/Storybook.d.ts +76 -0
  120. package/dist/Storybook.d.ts.map +1 -0
  121. package/dist/Storybook.js +102 -0
  122. package/dist/Switch.d.ts +187 -0
  123. package/dist/Switch.d.ts.map +1 -0
  124. package/dist/Switch.js +116 -0
  125. package/dist/Tab.d.ts +26 -0
  126. package/dist/Tab.d.ts.map +1 -0
  127. package/dist/Tab.js +25 -0
  128. package/dist/Tabs.d.ts +591 -0
  129. package/dist/Tabs.d.ts.map +1 -0
  130. package/dist/Tabs.js +346 -0
  131. package/dist/Toolbar.d.ts +366 -0
  132. package/dist/Toolbar.d.ts.map +1 -0
  133. package/dist/Toolbar.js +245 -0
  134. package/dist/Tooltip.d.ts +296 -0
  135. package/dist/Tooltip.d.ts.map +1 -0
  136. package/dist/Tooltip.js +172 -0
  137. package/dist/Tree.d.ts +591 -0
  138. package/dist/Tree.d.ts.map +1 -0
  139. package/dist/Tree.js +434 -0
  140. package/dist/TreeGrid.d.ts +645 -0
  141. package/dist/TreeGrid.d.ts.map +1 -0
  142. package/dist/TreeGrid.js +420 -0
  143. package/dist/VisuallyHidden.d.ts +54 -0
  144. package/dist/VisuallyHidden.d.ts.map +1 -0
  145. package/dist/VisuallyHidden.js +27 -0
  146. package/dist/WindowSplitter.d.ts +376 -0
  147. package/dist/WindowSplitter.d.ts.map +1 -0
  148. package/dist/WindowSplitter.js +222 -0
  149. package/dist/index.d.ts +49 -0
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +49 -0
  152. package/package.json +45 -18
  153. package/src/HttpRouter.test.ts +0 -256
  154. package/src/HttpRouter.ts +0 -165
  155. package/src/Link.test.ts +0 -84
  156. package/src/Link.ts +0 -107
  157. package/src/index.ts +0 -2
package/dist/Tree.js ADDED
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Tree stores active item and expanded ids independently. Its key policy follows hierarchical tree
3
+ * navigation, while Item and Group expose explicit levels, expansion, and visibility without
4
+ * reconstructing nodes.
5
+ *
6
+ * @remarks
7
+ * The module keeps policy, state transitions, and DOM rendering separable so applications can use
8
+ * the state and pure operations without mounting UI, or supply custom hosts without replacing native
9
+ * events and browser-owned focus.
10
+ *
11
+ * @since 1.0.0
12
+ * @category modules
13
+ * @packageDocumentation
14
+ */
15
+ import * as Effect from "effect/Effect";
16
+ import * as Schema from "effect/Schema";
17
+ import { RefSubject } from "@typed/fx";
18
+ import { EventHandler, html, } from "@typed/template";
19
+ import * as Collection from "./Collection.js";
20
+ import * as Composite from "./Composite.js";
21
+ import * as Dom from "./Dom.js";
22
+ /**
23
+ * Effect Schema used by makeState to encode, decode, and hydrate Tree state.
24
+ *
25
+ * @remarks
26
+ * ## Why
27
+ *
28
+ * A public schema makes hydration and serialized state use the same runtime validation as direct
29
+ * construction.
30
+ *
31
+ * ## Ownership and lifetime
32
+ *
33
+ * This declaration is data or schema metadata and acquires no resources.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import * as Schema from "effect/Schema";
38
+ * import * as Tree from "@typed/ui/Tree";
39
+ *
40
+ * const decodeState = Schema.decodeUnknownEffect(Tree.StateSchema);
41
+ * ```
42
+ * @since 1.0.0
43
+ * @category schemas
44
+ */
45
+ export const StateSchema = Schema.Struct({
46
+ activeId: Schema.NullOr(Schema.String),
47
+ expandedIds: Schema.Array(Schema.String),
48
+ loop: Schema.Boolean,
49
+ });
50
+ /**
51
+ * Creates hydrated Tree state. activeId defaults null and expandedIds defaults empty.
52
+ *
53
+ * @remarks
54
+ * ## Why
55
+ *
56
+ * State and collection ownership can be composed and tested independently from any renderer.
57
+ *
58
+ * ## Ownership and lifetime
59
+ *
60
+ * The returned Effect creates the RefSubject when run. That state is renderer-independent;
61
+ * collection registrations belong to the separate Scope that runs register or ref, not to state
62
+ * creation.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * import * as Effect from "effect/Effect";
67
+ * import * as Tree from "@typed/ui/Tree";
68
+ *
69
+ * const program = Effect.scoped(
70
+ * Effect.gen(function* () {
71
+ * const state = yield* Tree.makeState({});
72
+ * const collection = yield* Tree.makeCollection();
73
+ * return { state: yield* state, collection: yield* collection };
74
+ * }),
75
+ * );
76
+ * ```
77
+ * @since 1.0.0
78
+ * @category constructors
79
+ */
80
+ export function makeState(initial = {}) {
81
+ return RefSubject.hydrate(StateSchema, {
82
+ activeId: initial.activeId ?? null,
83
+ expandedIds: [...(initial.expandedIds ?? [])],
84
+ loop: initial.loop ?? true,
85
+ });
86
+ }
87
+ /**
88
+ * Creates a scoped Collection for Tree items.
89
+ *
90
+ * @remarks
91
+ * ## Why
92
+ *
93
+ * State and collection ownership can be composed and tested independently from any renderer.
94
+ *
95
+ * ## Ownership and lifetime
96
+ *
97
+ * The returned Effect allocates the RefSubject in the caller's Scope. Each later registration is
98
+ * owned by the Scope that runs register, independently of this construction Effect.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * import * as Effect from "effect/Effect";
103
+ * import * as Tree from "@typed/ui/Tree";
104
+ *
105
+ * const program = Effect.scoped(
106
+ * Effect.gen(function* () {
107
+ * const collection = yield* Tree.makeCollection();
108
+ * return yield* collection;
109
+ * }),
110
+ * );
111
+ * ```
112
+ * @since 1.0.0
113
+ * @category constructors
114
+ */
115
+ export const makeCollection = Collection.makeState;
116
+ /**
117
+ * Tests membership in expandedIds without touching the DOM.
118
+ *
119
+ * @remarks
120
+ * ## Why
121
+ *
122
+ * Separating this deterministic policy from event wiring lets applications test it directly and
123
+ * reuse it in custom composites.
124
+ *
125
+ * ## Ownership and lifetime
126
+ *
127
+ * This is a synchronous calculation. It acquires no resources and does not mutate the input array,
128
+ * state, event, or DOM.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * import * as Tree from "@typed/ui/Tree";
133
+ *
134
+ * const expanded = Tree.isExpanded({ activeId: null, expandedIds: ["parent"], loop: true }, "parent");
135
+ * ```
136
+ * @since 1.0.0
137
+ * @category combinators
138
+ */
139
+ export function isExpanded(state, id) {
140
+ return state.expandedIds.includes(id);
141
+ }
142
+ /**
143
+ * Adds an id once to expandedIds.
144
+ *
145
+ * @remarks
146
+ * ## Why
147
+ *
148
+ * The operation exposes Tree's transition directly so callers can compose it in Effect programs
149
+ * and native event handlers.
150
+ *
151
+ * ## Ownership and lifetime
152
+ *
153
+ * The returned Effect performs the update or DOM side effect only when run, preserves the declared
154
+ * error and service channels, and retains no resources after completion.
155
+ *
156
+ * ## Example
157
+ *
158
+ * Import with `import { expand } from "@typed/ui/Tree";` Extend the [Tree.makeState runnable
159
+ * setup](/reference/%40typed%2Fui%2FTree%23makeState). Inside the linked Effect program invoke
160
+ * `yield* expand(state, "item-2")`, then read the state snapshot to observe the transition
161
+ * described above.
162
+ * @since 1.0.0
163
+ * @category combinators
164
+ */
165
+ export function expand(state, id) {
166
+ return RefSubject.update(state, (current) => isExpanded(current, id) ? current : { ...current, expandedIds: [...current.expandedIds, id] });
167
+ }
168
+ /**
169
+ * Removes an id from expandedIds.
170
+ *
171
+ * @remarks
172
+ * ## Why
173
+ *
174
+ * The operation exposes Tree's transition directly so callers can compose it in Effect programs
175
+ * and native event handlers.
176
+ *
177
+ * ## Ownership and lifetime
178
+ *
179
+ * The returned Effect performs the update or DOM side effect only when run, preserves the declared
180
+ * error and service channels, and retains no resources after completion.
181
+ *
182
+ * ## Example
183
+ *
184
+ * Import with `import { collapse } from "@typed/ui/Tree";` Extend the [Tree.makeState runnable
185
+ * setup](/reference/%40typed%2Fui%2FTree%23makeState). Inside the linked Effect program invoke
186
+ * `yield* collapse(state, "item-2")`, then read the state snapshot to observe the transition
187
+ * described above.
188
+ * @since 1.0.0
189
+ * @category combinators
190
+ */
191
+ export function collapse(state, id) {
192
+ return RefSubject.update(state, (current) => ({
193
+ ...current,
194
+ expandedIds: current.expandedIds.filter((expanded) => expanded !== id),
195
+ }));
196
+ }
197
+ /**
198
+ * Sets the active tree item id without changing expansion.
199
+ *
200
+ * @remarks
201
+ * ## Why
202
+ *
203
+ * The operation exposes Tree's transition directly so callers can compose it in Effect programs
204
+ * and native event handlers.
205
+ *
206
+ * ## Ownership and lifetime
207
+ *
208
+ * The returned Effect performs the update or DOM side effect only when run, preserves the declared
209
+ * error and service channels, and retains no resources after completion.
210
+ *
211
+ * ## Example
212
+ *
213
+ * Import with `import { activate } from "@typed/ui/Tree";` Extend the [Tree.makeState runnable
214
+ * setup](/reference/%40typed%2Fui%2FTree%23makeState). Inside the linked Effect program invoke
215
+ * `yield* activate(state, "item-2")`, then read the state snapshot to observe the transition
216
+ * described above.
217
+ * @since 1.0.0
218
+ * @category combinators
219
+ */
220
+ export function activate(state, activeId) {
221
+ return RefSubject.update(state, (current) => ({ ...current, activeId }));
222
+ }
223
+ function rootInternalProps(options) {
224
+ const onfocus = options.collection === undefined
225
+ ? undefined
226
+ : Effect.gen(function* () {
227
+ const current = yield* options.state;
228
+ if (current.activeId !== null)
229
+ return;
230
+ const first = Composite.moveActiveId(visibleItems(yield* options.collection, current), current, "first");
231
+ if (first === null)
232
+ return;
233
+ yield* activate(options.state, first);
234
+ yield* focusItem(options.collection, first);
235
+ });
236
+ const onkeydown = options.collection === undefined
237
+ ? undefined
238
+ : EventHandler.make(Effect.fn((event) => onKeyDown(options.state, options.collection, event)));
239
+ return ({ property }) => ({
240
+ role: "tree",
241
+ "aria-label": property("label", undefined),
242
+ tabindex: RefSubject.map(options.state, (state) => (state.activeId === null ? 0 : -1)),
243
+ onfocus,
244
+ onkeydown,
245
+ ref: options.state,
246
+ });
247
+ }
248
+ /**
249
+ * Renders the tree root and applies hierarchy-aware keyboard navigation to registered items.
250
+ *
251
+ * @remarks
252
+ * ## Why
253
+ *
254
+ * The component applies the family behavior while leaving callers free to supply a custom host
255
+ * through the shared DOM boundary.
256
+ *
257
+ * ## Ownership and lifetime
258
+ *
259
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
260
+ * collection registrations only when rendered. The rendering Scope removes those resources;
261
+ * unrelated nodes and attributes remain caller-owned.
262
+ *
263
+ * ## Example
264
+ *
265
+ * Import with `import { Root } from "@typed/ui/Tree";` Extend the [Tree.makeState runnable
266
+ * setup](/reference/%40typed%2Fui%2FTree%23makeState). Replace the linked program's final snapshot
267
+ * read with `Root({ state, label: "Files", content: "Items" })`; render that Fx before the same
268
+ * Scope closes.
269
+ * @since 1.0.0
270
+ * @category components
271
+ */
272
+ export function Root(options, host) {
273
+ return Dom.renderHost()(options, host, rootInternalProps(options), options.content, (props, content) => {
274
+ return html `<div ...${props}>${content}</div>`;
275
+ });
276
+ }
277
+ function itemInternalProps(options) {
278
+ const hasChildren = options.hasChildren ?? false;
279
+ const register = options.collection === undefined
280
+ ? undefined
281
+ : Collection.ref(options.collection, {
282
+ id: options.id,
283
+ value: { parentId: options.parentId, hasChildren },
284
+ textValue: options.id,
285
+ disabled: options.disabled,
286
+ });
287
+ return ({ property }) => ({
288
+ id: options.id,
289
+ role: "treeitem",
290
+ "aria-level": property("level", 1),
291
+ "aria-expanded": hasChildren
292
+ ? RefSubject.map(options.state, (state) => isExpanded(state, options.id))
293
+ : undefined,
294
+ "aria-disabled": options.disabled ?? false,
295
+ tabindex: RefSubject.map(options.state, (state) => (state.activeId === options.id ? 0 : -1)),
296
+ onfocus: activate(options.state, options.id),
297
+ onclick: options.disabled === true ? Effect.void : activate(options.state, options.id),
298
+ ref: Dom.composeRefs(register, options.ref),
299
+ });
300
+ }
301
+ /**
302
+ * Renders and optionally registers a treeitem with level, expanded, disabled, and roving-focus
303
+ * state.
304
+ *
305
+ * @remarks
306
+ * ## Why
307
+ *
308
+ * The component applies the family behavior while leaving callers free to supply a custom host
309
+ * through the shared DOM boundary.
310
+ *
311
+ * ## Ownership and lifetime
312
+ *
313
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
314
+ * collection registrations only when rendered. The rendering Scope removes those resources;
315
+ * unrelated nodes and attributes remain caller-owned.
316
+ *
317
+ * ## Example
318
+ *
319
+ * Import with `import { Item } from "@typed/ui/Tree";` Extend the [Tree.makeState runnable
320
+ * setup](/reference/%40typed%2Fui%2FTree%23makeState). Replace the linked program's final snapshot
321
+ * read with `Item({ state, id: "src", hasChildren: true, content: "src" })`; render that Fx before
322
+ * the same Scope closes.
323
+ * @since 1.0.0
324
+ * @category components
325
+ */
326
+ export function Item(options, host) {
327
+ return Dom.renderHost()(options, host, itemInternalProps(options), options.content, (props, content) => {
328
+ return html `<div ...${props}>${content}</div>`;
329
+ });
330
+ }
331
+ function groupInternalProps(options) {
332
+ return () => ({
333
+ role: "group",
334
+ "?hidden": RefSubject.map(options.state, (state) => !isExpanded(state, options.parentId)),
335
+ });
336
+ }
337
+ /**
338
+ * Renders a child group and hides it while its parent id is not expanded.
339
+ *
340
+ * @remarks
341
+ * ## Why
342
+ *
343
+ * The component applies the family behavior while leaving callers free to supply a custom host
344
+ * through the shared DOM boundary.
345
+ *
346
+ * ## Ownership and lifetime
347
+ *
348
+ * The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
349
+ * collection registrations only when rendered. The rendering Scope removes those resources;
350
+ * unrelated nodes and attributes remain caller-owned.
351
+ *
352
+ * ## Example
353
+ *
354
+ * Import with `import { Group } from "@typed/ui/Tree";` Extend the [Tree.makeState runnable
355
+ * setup](/reference/%40typed%2Fui%2FTree%23makeState). Replace the linked program's final snapshot
356
+ * read with `Group({ state, parentId: "src", content: "Children" })`; render that Fx before the
357
+ * same Scope closes.
358
+ * @since 1.0.0
359
+ * @category components
360
+ */
361
+ export function Group(options, host) {
362
+ return Dom.renderHost()(options, host, groupInternalProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
363
+ }
364
+ function onKeyDown(state, collection, event) {
365
+ return Effect.gen(function* () {
366
+ const current = yield* state;
367
+ const items = visibleItems(yield* collection, current);
368
+ if (event.key === "Enter" || event.key === " ") {
369
+ const active = current.activeId === null
370
+ ? undefined
371
+ : items.find((item) => item.id === current.activeId && item.disabled !== true);
372
+ const element = active?.element;
373
+ const click = element === undefined ? undefined : Reflect.get(element, "click");
374
+ if (typeof click === "function") {
375
+ event.preventDefault();
376
+ yield* Effect.sync(() => click.call(element));
377
+ }
378
+ return yield* state;
379
+ }
380
+ const direction = Composite.keyMove(event, { orientation: "vertical" });
381
+ if (direction !== undefined) {
382
+ event.preventDefault();
383
+ const next = Composite.moveActiveId(items, current, direction);
384
+ if (next === null)
385
+ return current;
386
+ yield* activate(state, next);
387
+ yield* focusItem(collection, next);
388
+ return yield* state;
389
+ }
390
+ const active = current.activeId === null
391
+ ? undefined
392
+ : (yield* collection).find((item) => item.id === current.activeId);
393
+ if (active === undefined)
394
+ return current;
395
+ const value = active.value;
396
+ if (event.key === "ArrowRight" && value?.hasChildren) {
397
+ event.preventDefault();
398
+ if (!isExpanded(current, active.id))
399
+ return yield* expand(state, active.id);
400
+ const child = items.find((item) => item.value?.parentId === active.id);
401
+ if (child === undefined)
402
+ return current;
403
+ yield* activate(state, child.id);
404
+ yield* focusItem(collection, child.id);
405
+ return yield* state;
406
+ }
407
+ if (event.key === "ArrowLeft") {
408
+ event.preventDefault();
409
+ if (value?.hasChildren && isExpanded(current, active.id))
410
+ return yield* collapse(state, active.id);
411
+ if (value?.parentId === undefined)
412
+ return current;
413
+ yield* activate(state, value.parentId);
414
+ yield* focusItem(collection, value.parentId);
415
+ return yield* state;
416
+ }
417
+ return current;
418
+ });
419
+ }
420
+ function visibleItems(items, state) {
421
+ const ordered = Collection.byDomOrder(items);
422
+ return ordered.filter((item) => {
423
+ let parentId = item.value?.parentId;
424
+ while (parentId !== undefined) {
425
+ if (!isExpanded(state, parentId))
426
+ return false;
427
+ parentId = ordered.find((candidate) => candidate.id === parentId)?.value?.parentId;
428
+ }
429
+ return true;
430
+ });
431
+ }
432
+ function focusItem(collection, id) {
433
+ return Effect.flatMap(collection, (items) => Composite.focusElement(items.find((item) => item.id === id)?.element));
434
+ }