@typed/ui 1.0.0-beta.3 → 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.
- package/README.md +53 -9
- package/dist/Alert.d.ts +78 -0
- package/dist/Alert.d.ts.map +1 -0
- package/dist/Alert.js +37 -0
- package/dist/Button.d.ts +144 -0
- package/dist/Button.d.ts.map +1 -0
- package/dist/Button.js +38 -0
- package/dist/Carousel.d.ts +547 -0
- package/dist/Carousel.d.ts.map +1 -0
- package/dist/Carousel.js +387 -0
- package/dist/Checkbox.d.ts +203 -0
- package/dist/Checkbox.d.ts.map +1 -0
- package/dist/Checkbox.js +140 -0
- package/dist/Collection.d.ts +254 -0
- package/dist/Collection.d.ts.map +1 -0
- package/dist/Collection.js +218 -0
- package/dist/Combobox.d.ts +516 -0
- package/dist/Combobox.d.ts.map +1 -0
- package/dist/Combobox.js +371 -0
- package/dist/Component.d.ts +127 -0
- package/dist/Component.d.ts.map +1 -0
- package/dist/Component.js +60 -0
- package/dist/Composite.d.ts +823 -0
- package/dist/Composite.d.ts.map +1 -0
- package/dist/Composite.js +615 -0
- package/dist/Dialog.d.ts +544 -0
- package/dist/Dialog.d.ts.map +1 -0
- package/dist/Dialog.js +355 -0
- package/dist/Disclosure.d.ts +219 -0
- package/dist/Disclosure.d.ts.map +1 -0
- package/dist/Disclosure.js +128 -0
- package/dist/Dom/Events.d.ts +122 -0
- package/dist/Dom/Events.d.ts.map +1 -0
- package/dist/Dom/Events.js +192 -0
- package/dist/Dom/Props.d.ts +161 -0
- package/dist/Dom/Props.d.ts.map +1 -0
- package/dist/Dom/Props.js +110 -0
- package/dist/Dom/Refs.d.ts +58 -0
- package/dist/Dom/Refs.d.ts.map +1 -0
- package/dist/Dom/Refs.js +61 -0
- package/dist/Dom/Render.d.ts +59 -0
- package/dist/Dom/Render.d.ts.map +1 -0
- package/dist/Dom/Render.js +71 -0
- package/dist/Dom/Types.d.ts +570 -0
- package/dist/Dom/Types.d.ts.map +1 -0
- package/dist/Dom/Types.js +1 -0
- package/dist/Dom/index.d.ts +20 -0
- package/dist/Dom/index.d.ts.map +1 -0
- package/dist/Dom/index.js +8 -0
- package/dist/Dom.d.ts +14 -0
- package/dist/Dom.d.ts.map +1 -0
- package/dist/Dom.js +13 -0
- package/dist/Focusable.d.ts +85 -0
- package/dist/Focusable.d.ts.map +1 -0
- package/dist/Focusable.js +35 -0
- package/dist/Form.d.ts +1695 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/Form.js +987 -0
- package/dist/Grid.d.ts +569 -0
- package/dist/Grid.d.ts.map +1 -0
- package/dist/Grid.js +379 -0
- package/dist/Group.d.ts +147 -0
- package/dist/Group.d.ts.map +1 -0
- package/dist/Group.js +63 -0
- package/dist/Heading.d.ts +86 -0
- package/dist/Heading.d.ts.map +1 -0
- package/dist/Heading.js +48 -0
- package/dist/Hovercard.d.ts +297 -0
- package/dist/Hovercard.d.ts.map +1 -0
- package/dist/Hovercard.js +188 -0
- package/dist/HttpRouter.d.ts +129 -6
- package/dist/HttpRouter.d.ts.map +1 -1
- package/dist/HttpRouter.js +197 -54
- package/dist/Link.d.ts +67 -28
- package/dist/Link.d.ts.map +1 -1
- package/dist/Link.js +91 -37
- package/dist/Listbox.d.ts +437 -0
- package/dist/Listbox.d.ts.map +1 -0
- package/dist/Listbox.js +316 -0
- package/dist/Menu.d.ts +972 -0
- package/dist/Menu.d.ts.map +1 -0
- package/dist/Menu.js +731 -0
- package/dist/Menubar.d.ts +367 -0
- package/dist/Menubar.d.ts.map +1 -0
- package/dist/Menubar.js +265 -0
- package/dist/Meter.d.ts +217 -0
- package/dist/Meter.d.ts.map +1 -0
- package/dist/Meter.js +94 -0
- package/dist/NativeDetails.d.ts +41 -0
- package/dist/NativeDetails.d.ts.map +1 -0
- package/dist/NativeDetails.js +40 -0
- package/dist/NativeDialog.d.ts +64 -0
- package/dist/NativeDialog.d.ts.map +1 -0
- package/dist/NativeDialog.js +50 -0
- package/dist/NativePopover.d.ts +41 -0
- package/dist/NativePopover.d.ts.map +1 -0
- package/dist/NativePopover.js +46 -0
- package/dist/Popover.d.ts +241 -0
- package/dist/Popover.d.ts.map +1 -0
- package/dist/Popover.js +141 -0
- package/dist/RadioGroup.d.ts +432 -0
- package/dist/RadioGroup.d.ts.map +1 -0
- package/dist/RadioGroup.js +291 -0
- package/dist/Role.d.ts +64 -0
- package/dist/Role.d.ts.map +1 -0
- package/dist/Role.js +27 -0
- package/dist/Select.d.ts +529 -0
- package/dist/Select.d.ts.map +1 -0
- package/dist/Select.js +407 -0
- package/dist/Separator.d.ts +54 -0
- package/dist/Separator.d.ts.map +1 -0
- package/dist/Separator.js +26 -0
- package/dist/Slider.d.ts +188 -0
- package/dist/Slider.d.ts.map +1 -0
- package/dist/Slider.js +96 -0
- package/dist/SpinButton.d.ts +188 -0
- package/dist/SpinButton.d.ts.map +1 -0
- package/dist/SpinButton.js +96 -0
- package/dist/Storybook.d.ts +76 -0
- package/dist/Storybook.d.ts.map +1 -0
- package/dist/Storybook.js +102 -0
- package/dist/Switch.d.ts +187 -0
- package/dist/Switch.d.ts.map +1 -0
- package/dist/Switch.js +116 -0
- package/dist/Tab.d.ts +26 -0
- package/dist/Tab.d.ts.map +1 -0
- package/dist/Tab.js +25 -0
- package/dist/Tabs.d.ts +591 -0
- package/dist/Tabs.d.ts.map +1 -0
- package/dist/Tabs.js +346 -0
- package/dist/Toolbar.d.ts +366 -0
- package/dist/Toolbar.d.ts.map +1 -0
- package/dist/Toolbar.js +245 -0
- package/dist/Tooltip.d.ts +296 -0
- package/dist/Tooltip.d.ts.map +1 -0
- package/dist/Tooltip.js +172 -0
- package/dist/Tree.d.ts +591 -0
- package/dist/Tree.d.ts.map +1 -0
- package/dist/Tree.js +434 -0
- package/dist/TreeGrid.d.ts +645 -0
- package/dist/TreeGrid.d.ts.map +1 -0
- package/dist/TreeGrid.js +420 -0
- package/dist/VisuallyHidden.d.ts +54 -0
- package/dist/VisuallyHidden.d.ts.map +1 -0
- package/dist/VisuallyHidden.js +27 -0
- package/dist/WindowSplitter.d.ts +376 -0
- package/dist/WindowSplitter.d.ts.map +1 -0
- package/dist/WindowSplitter.js +222 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -0
- package/package.json +42 -15
- package/src/HttpRouter.test.ts +0 -294
- package/src/HttpRouter.ts +0 -168
- package/src/Link.test.ts +0 -84
- package/src/Link.ts +0 -107
- 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
|
+
}
|