@typed/ui 1.0.0-beta.1 → 1.0.0-beta.11
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/LICENSE +21 -0
- package/README.md +54 -10
- package/dist/Alert.d.ts +72 -0
- package/dist/Alert.d.ts.map +1 -0
- package/dist/Alert.js +40 -0
- package/dist/Button.d.ts +100 -0
- package/dist/Button.d.ts.map +1 -0
- package/dist/Button.js +42 -0
- package/dist/Carousel.d.ts +346 -0
- package/dist/Carousel.d.ts.map +1 -0
- package/dist/Carousel.js +264 -0
- package/dist/Checkbox.d.ts +168 -0
- package/dist/Checkbox.d.ts.map +1 -0
- package/dist/Checkbox.js +146 -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 +367 -0
- package/dist/Combobox.d.ts.map +1 -0
- package/dist/Combobox.js +300 -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 +357 -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 +1727 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/Form.js +1142 -0
- package/dist/Grid.d.ts +388 -0
- package/dist/Grid.d.ts.map +1 -0
- package/dist/Grid.js +284 -0
- package/dist/Group.d.ts +128 -0
- package/dist/Group.d.ts.map +1 -0
- package/dist/Group.js +71 -0
- package/dist/Heading.d.ts +87 -0
- package/dist/Heading.d.ts.map +1 -0
- package/dist/Heading.js +58 -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 +198 -55
- package/dist/Link.d.ts +63 -28
- package/dist/Link.d.ts.map +1 -1
- package/dist/Link.js +84 -37
- package/dist/Listbox.d.ts +305 -0
- package/dist/Listbox.d.ts.map +1 -0
- package/dist/Listbox.js +245 -0
- package/dist/Menu.d.ts +663 -0
- package/dist/Menu.d.ts.map +1 -0
- package/dist/Menu.js +569 -0
- package/dist/Menubar.d.ts +249 -0
- package/dist/Menubar.d.ts.map +1 -0
- package/dist/Menubar.js +207 -0
- package/dist/Meter.d.ts +157 -0
- package/dist/Meter.d.ts.map +1 -0
- package/dist/Meter.js +87 -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 +66 -0
- package/dist/NativeDialog.d.ts.map +1 -0
- package/dist/NativeDialog.js +88 -0
- package/dist/NativePopover.d.ts +43 -0
- package/dist/NativePopover.d.ts.map +1 -0
- package/dist/NativePopover.js +84 -0
- package/dist/Popover.d.ts +240 -0
- package/dist/Popover.d.ts.map +1 -0
- package/dist/Popover.js +140 -0
- package/dist/RadioGroup.d.ts +330 -0
- package/dist/RadioGroup.d.ts.map +1 -0
- package/dist/RadioGroup.js +241 -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 +418 -0
- package/dist/Select.d.ts.map +1 -0
- package/dist/Select.js +357 -0
- package/dist/Separator.d.ts +58 -0
- package/dist/Separator.d.ts.map +1 -0
- package/dist/Separator.js +32 -0
- package/dist/Slider.d.ts +141 -0
- package/dist/Slider.d.ts.map +1 -0
- package/dist/Slider.js +101 -0
- package/dist/SpinButton.d.ts +141 -0
- package/dist/SpinButton.d.ts.map +1 -0
- package/dist/SpinButton.js +101 -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 +148 -0
- package/dist/Switch.d.ts.map +1 -0
- package/dist/Switch.js +110 -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 +411 -0
- package/dist/Tabs.d.ts.map +1 -0
- package/dist/Tabs.js +262 -0
- package/dist/Toolbar.d.ts +248 -0
- package/dist/Toolbar.d.ts.map +1 -0
- package/dist/Toolbar.js +187 -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 +405 -0
- package/dist/Tree.d.ts.map +1 -0
- package/dist/Tree.js +333 -0
- package/dist/TreeGrid.d.ts +426 -0
- package/dist/TreeGrid.d.ts.map +1 -0
- package/dist/TreeGrid.js +308 -0
- package/dist/VisuallyHidden.d.ts +68 -0
- package/dist/VisuallyHidden.d.ts.map +1 -0
- package/dist/VisuallyHidden.js +44 -0
- package/dist/WindowSplitter.d.ts +336 -0
- package/dist/WindowSplitter.d.ts.map +1 -0
- package/dist/WindowSplitter.js +305 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -0
- package/package.json +48 -20
- 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/Combobox.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combobox keeps focus on its native input and exposes the active option through
|
|
3
|
+
* aria-activedescendant. Input, a manual native popover, and options share explicit value, open,
|
|
4
|
+
* and active state.
|
|
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
|
+
* Learn the interaction in the [Combobox guide](/explore/ui-combobox).
|
|
12
|
+
*
|
|
13
|
+
* @since 1.0.0
|
|
14
|
+
* @category modules
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
import * as Effect from "effect/Effect";
|
|
18
|
+
import * as Schema from "effect/Schema";
|
|
19
|
+
import { RefSubject } from "@typed/fx";
|
|
20
|
+
import { EventHandler, html, } from "@typed/template";
|
|
21
|
+
import * as Collection from "./Collection.js";
|
|
22
|
+
import * as Composite from "./Composite.js";
|
|
23
|
+
import * as Dom from "./Dom.js";
|
|
24
|
+
import * as NativePopover from "./NativePopover.js";
|
|
25
|
+
/**
|
|
26
|
+
* Effect Schema used by makeState to encode, decode, and hydrate Combobox state.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import * as Schema from "effect/Schema";
|
|
32
|
+
* import * as Combobox from "@typed/ui/Combobox";
|
|
33
|
+
*
|
|
34
|
+
* const decodeState = Schema.decodeUnknownEffect(Combobox.StateSchema);
|
|
35
|
+
* ```
|
|
36
|
+
* @since 1.0.0
|
|
37
|
+
* @category Query and popup state
|
|
38
|
+
*/
|
|
39
|
+
export const StateSchema = Schema.Struct({
|
|
40
|
+
id: Schema.String,
|
|
41
|
+
value: Schema.String,
|
|
42
|
+
open: Schema.Boolean,
|
|
43
|
+
activeId: Schema.NullOr(Schema.String),
|
|
44
|
+
orientation: Schema.Literals(["vertical"]),
|
|
45
|
+
loop: Schema.Boolean,
|
|
46
|
+
rtl: Schema.Boolean,
|
|
47
|
+
virtualFocus: Schema.Boolean,
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* Creates hydrated Combobox state. value defaults to an empty string, open to false, activeId to
|
|
51
|
+
* null, loop to true, and virtual focus is enabled.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* The returned Effect creates the RefSubject when run. That state is renderer-independent;
|
|
55
|
+
* collection registrations belong to the separate Scope that runs register or ref, not to state
|
|
56
|
+
* creation.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import * as Effect from "effect/Effect";
|
|
61
|
+
* import * as Combobox from "@typed/ui/Combobox";
|
|
62
|
+
*
|
|
63
|
+
* const program = Effect.scoped(
|
|
64
|
+
* Effect.gen(function* () {
|
|
65
|
+
* const state = yield* Combobox.makeState({ id: "city" });
|
|
66
|
+
* const collection = yield* Combobox.makeCollection();
|
|
67
|
+
* return { state: yield* state, collection: yield* collection };
|
|
68
|
+
* }),
|
|
69
|
+
* );
|
|
70
|
+
* ```
|
|
71
|
+
* @since 1.0.0
|
|
72
|
+
* @category Query and popup state
|
|
73
|
+
*/
|
|
74
|
+
export function makeState(initial) {
|
|
75
|
+
return RefSubject.hydrate(StateSchema, {
|
|
76
|
+
id: initial.id,
|
|
77
|
+
value: initial.value ?? "",
|
|
78
|
+
open: initial.open ?? false,
|
|
79
|
+
activeId: initial.activeId ?? null,
|
|
80
|
+
orientation: "vertical",
|
|
81
|
+
loop: initial.loop ?? true,
|
|
82
|
+
rtl: false,
|
|
83
|
+
virtualFocus: true,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Creates a scoped Collection for Combobox items.
|
|
88
|
+
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* The returned Effect allocates the RefSubject in the caller's Scope. Each later registration is
|
|
91
|
+
* owned by the Scope that runs register, independently of this construction Effect.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* import * as Effect from "effect/Effect";
|
|
96
|
+
* import * as Combobox from "@typed/ui/Combobox";
|
|
97
|
+
*
|
|
98
|
+
* const program = Effect.scoped(
|
|
99
|
+
* Effect.gen(function* () {
|
|
100
|
+
* const collection = yield* Combobox.makeCollection();
|
|
101
|
+
* return yield* collection;
|
|
102
|
+
* }),
|
|
103
|
+
* );
|
|
104
|
+
* ```
|
|
105
|
+
* @since 1.0.0
|
|
106
|
+
* @category Suggestion registration
|
|
107
|
+
*/
|
|
108
|
+
export const makeCollection = Collection.makeState;
|
|
109
|
+
/**
|
|
110
|
+
* Stores the input value, clears activeId, and opens the popup in one state update.
|
|
111
|
+
*
|
|
112
|
+
* @remarks
|
|
113
|
+
* The operation exposes Combobox's transition directly so callers can compose it in Effect
|
|
114
|
+
* programs and native event handlers.
|
|
115
|
+
*
|
|
116
|
+
* @since 1.0.0
|
|
117
|
+
* @category Query and popup state
|
|
118
|
+
*/
|
|
119
|
+
export function setValue(state, value) {
|
|
120
|
+
return RefSubject.update(state, (current) => ({ ...current, value, activeId: null, open: true }));
|
|
121
|
+
}
|
|
122
|
+
function openPopover(state, collection) {
|
|
123
|
+
return Effect.gen(function* () {
|
|
124
|
+
const current = yield* state;
|
|
125
|
+
const item = collection === undefined
|
|
126
|
+
? undefined
|
|
127
|
+
: visibleItems(yield* collection).find((item) => item.value === current.value);
|
|
128
|
+
const next = yield* RefSubject.update(state, (value) => ({
|
|
129
|
+
...value,
|
|
130
|
+
activeId: item?.id ?? value.activeId,
|
|
131
|
+
open: true,
|
|
132
|
+
}));
|
|
133
|
+
if (item !== undefined && collection !== undefined) {
|
|
134
|
+
yield* Composite.scrollActive({ state, collection });
|
|
135
|
+
}
|
|
136
|
+
return next;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function move(state, collection, direction) {
|
|
140
|
+
return Effect.gen(function* () {
|
|
141
|
+
const next = Composite.moveActiveId(visibleItems(yield* collection), yield* state, direction);
|
|
142
|
+
if (next === null)
|
|
143
|
+
return yield* state;
|
|
144
|
+
return yield* RefSubject.update(state, (current) => ({ ...current, activeId: next }));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function select(state, id, value) {
|
|
148
|
+
return RefSubject.update(state, (current) => ({ ...current, activeId: id, value, open: false }));
|
|
149
|
+
}
|
|
150
|
+
function selectActive(state, collection) {
|
|
151
|
+
return Effect.gen(function* () {
|
|
152
|
+
const activeId = (yield* state).activeId;
|
|
153
|
+
const item = activeId === null ? undefined : (yield* collection).find((item) => item.id === activeId);
|
|
154
|
+
return item?.value === undefined ? yield* state : yield* select(state, item.id, item.value);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function inputProps(options) {
|
|
158
|
+
const id = RefSubject.map(options.state, (state) => state.id);
|
|
159
|
+
const inputId = RefSubject.map(options.state, (state) => `${state.id}-input`);
|
|
160
|
+
const open = RefSubject.map(options.state, (state) => state.open);
|
|
161
|
+
const onkeydown = options.collection === undefined
|
|
162
|
+
? undefined
|
|
163
|
+
: EventHandler.make(Effect.fn(function* (event) {
|
|
164
|
+
const direction = event.key === "ArrowDown" ? "next" : event.key === "ArrowUp" ? "previous" : undefined;
|
|
165
|
+
if (direction !== undefined) {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
if (!(yield* options.state).open) {
|
|
168
|
+
yield* openPopover(options.state, options.collection);
|
|
169
|
+
}
|
|
170
|
+
yield* move(options.state, options.collection, direction);
|
|
171
|
+
yield* Composite.scrollActive({
|
|
172
|
+
state: options.state,
|
|
173
|
+
collection: options.collection,
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (event.key === "Enter") {
|
|
178
|
+
if ((yield* options.state).activeId === null)
|
|
179
|
+
return;
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
yield* selectActive(options.state, options.collection);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (event.key === "Escape") {
|
|
185
|
+
yield* RefSubject.update(options.state, (state) => ({ ...state, open: false }));
|
|
186
|
+
}
|
|
187
|
+
}));
|
|
188
|
+
return () => ({
|
|
189
|
+
id: inputId,
|
|
190
|
+
role: "combobox",
|
|
191
|
+
"aria-autocomplete": "list",
|
|
192
|
+
"aria-controls": id,
|
|
193
|
+
"aria-expanded": open,
|
|
194
|
+
"aria-activedescendant": Composite.activeDescendant(options.state),
|
|
195
|
+
placeholder: options.placeholder,
|
|
196
|
+
".value": RefSubject.map(options.state, (state) => state.value),
|
|
197
|
+
oninput: EventHandler.make(Effect.fn((event) => setValue(options.state, Dom.currentTarget(event).value))),
|
|
198
|
+
onfocus: openPopover(options.state, options.collection),
|
|
199
|
+
onkeydown,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function visibleItems(collection) {
|
|
203
|
+
return Collection.byDomOrder(collection).filter((item) => item.element === undefined || item.element.closest("[hidden]") === null);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Renders the native combobox input, keeps its value property synchronized, opens on focus, and
|
|
207
|
+
* handles Arrow, Enter, and Escape when a collection is supplied.
|
|
208
|
+
*
|
|
209
|
+
* @remarks
|
|
210
|
+
* The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
|
|
211
|
+
* collection registrations only when rendered. The rendering Scope removes those resources;
|
|
212
|
+
* unrelated nodes and attributes remain caller-owned.
|
|
213
|
+
*
|
|
214
|
+
* @since 1.0.0
|
|
215
|
+
* @category Editable input
|
|
216
|
+
*/
|
|
217
|
+
export function Input(options, host) {
|
|
218
|
+
return Dom.renderHost()(options, host, inputProps(options), "", (props) => html `<input ...${props} />`);
|
|
219
|
+
}
|
|
220
|
+
function popoverProps(options) {
|
|
221
|
+
const id = RefSubject.map(options.state, (state) => state.id);
|
|
222
|
+
const inputId = RefSubject.map(options.state, (state) => `${state.id}-input`);
|
|
223
|
+
return () => ({
|
|
224
|
+
id,
|
|
225
|
+
role: "listbox",
|
|
226
|
+
"aria-labelledby": inputId,
|
|
227
|
+
popover: "manual",
|
|
228
|
+
ontoggle: EventHandler.make(Effect.fn(function* (event) {
|
|
229
|
+
const open = Dom.toggleState(event) === "open";
|
|
230
|
+
const current = yield* options.state;
|
|
231
|
+
if (current.open === open)
|
|
232
|
+
return current;
|
|
233
|
+
const next = yield* RefSubject.update(options.state, (state) => ({ ...state, open }));
|
|
234
|
+
if (!open || options.collection === undefined)
|
|
235
|
+
return next;
|
|
236
|
+
const item = (yield* options.collection).find((item) => item.value === next.value);
|
|
237
|
+
if (item === undefined)
|
|
238
|
+
return next;
|
|
239
|
+
const selected = yield* RefSubject.update(options.state, (state) => ({
|
|
240
|
+
...state,
|
|
241
|
+
activeId: item.id,
|
|
242
|
+
}));
|
|
243
|
+
yield* Composite.scrollActive({ state: options.state, collection: options.collection });
|
|
244
|
+
return selected;
|
|
245
|
+
})),
|
|
246
|
+
ref: Dom.composeRefs(options.state, NativePopover.ref(options.state)),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Renders manual-popover listbox content, mirrors native toggle events into open state, and
|
|
251
|
+
* scrolls the selected option when opened.
|
|
252
|
+
*
|
|
253
|
+
* @remarks
|
|
254
|
+
* The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
|
|
255
|
+
* collection registrations only when rendered. The rendering Scope removes those resources;
|
|
256
|
+
* unrelated nodes and attributes remain caller-owned.
|
|
257
|
+
*
|
|
258
|
+
* @since 1.0.0
|
|
259
|
+
* @category Suggestion popup
|
|
260
|
+
*/
|
|
261
|
+
export function Popover(options, host) {
|
|
262
|
+
return Dom.renderHost()(options, host, popoverProps(options), options.content, (props, content) => {
|
|
263
|
+
return html `<div ...${props}>${content}</div>`;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function itemProps(options) {
|
|
267
|
+
const selected = RefSubject.map(options.state, (state) => state.value === options.value);
|
|
268
|
+
const register = options.collection === undefined
|
|
269
|
+
? undefined
|
|
270
|
+
: Collection.ref(options.collection, {
|
|
271
|
+
id: options.id,
|
|
272
|
+
value: options.value,
|
|
273
|
+
textValue: options.textValue ?? options.value,
|
|
274
|
+
disabled: options.disabled,
|
|
275
|
+
});
|
|
276
|
+
return () => ({
|
|
277
|
+
id: options.id,
|
|
278
|
+
role: "option",
|
|
279
|
+
"aria-selected": selected,
|
|
280
|
+
"aria-disabled": options.disabled ?? false,
|
|
281
|
+
tabindex: Composite.tabIndex(options.state, options.id),
|
|
282
|
+
onclick: options.disabled === true ? Effect.void : select(options.state, options.id, options.value),
|
|
283
|
+
ref: Dom.composeRefs(register, options.ref),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Renders and optionally registers one option; disabled options expose aria-disabled and do not
|
|
288
|
+
* select on click.
|
|
289
|
+
*
|
|
290
|
+
* @remarks
|
|
291
|
+
* The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
|
|
292
|
+
* collection registrations only when rendered. The rendering Scope removes those resources;
|
|
293
|
+
* unrelated nodes and attributes remain caller-owned.
|
|
294
|
+
*
|
|
295
|
+
* @since 1.0.0
|
|
296
|
+
* @category Suggestions
|
|
297
|
+
*/
|
|
298
|
+
export function Item(options, host) {
|
|
299
|
+
return Dom.renderHost()(options, host, itemProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
|
|
300
|
+
}
|