@typed/ui 1.0.0-beta.0 → 1.0.0-beta.10
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 +78 -26
- 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 +199 -56
- 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 +47 -14
- package/src/HttpRouter.test.ts +0 -294
- package/src/HttpRouter.ts +0 -168
- package/src/Link.test.ts +0 -85
- package/src/Link.ts +0 -107
- package/src/index.ts +0 -2
- package/tsconfig.json +0 -6
package/dist/Select.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A native popover-backed listbox with separate selection, focus, and visibility.
|
|
3
|
+
* Unlike Form.Select, these options are divs rather than native select options.
|
|
4
|
+
* Pass one collection to Content and its Options for registered keyboard navigation.
|
|
5
|
+
*
|
|
6
|
+
* Read the [Select guide](/explore/ui-select) for a complete example.
|
|
7
|
+
*
|
|
8
|
+
* [APG interaction reference](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/).
|
|
9
|
+
* @since 1.0.0
|
|
10
|
+
* @category Overview
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
import * as Effect from "effect/Effect";
|
|
14
|
+
import * as Scope from "effect/Scope";
|
|
15
|
+
import * as Schema from "effect/Schema";
|
|
16
|
+
import { RefSubject } from "@typed/fx";
|
|
17
|
+
import { EventHandler, html, } from "@typed/template";
|
|
18
|
+
import * as Collection from "./Collection.js";
|
|
19
|
+
import * as Composite from "./Composite.js";
|
|
20
|
+
import * as Dom from "./Dom.js";
|
|
21
|
+
import * as NativePopover from "./NativePopover.js";
|
|
22
|
+
const invokers = new WeakMap();
|
|
23
|
+
/**
|
|
24
|
+
* Effect Schema used by makeState to encode, decode, and hydrate Select state.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* A public schema makes hydration and serialized state use the same runtime validation as direct
|
|
28
|
+
* construction.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* import * as Schema from "effect/Schema";
|
|
33
|
+
* import * as Select from "@typed/ui/Select";
|
|
34
|
+
*
|
|
35
|
+
* const decodeState = Schema.decodeUnknownEffect(Select.StateSchema);
|
|
36
|
+
* ```
|
|
37
|
+
* @since 1.0.0
|
|
38
|
+
* @category Hydration schemas
|
|
39
|
+
*/
|
|
40
|
+
export const StateSchema = Schema.Struct({
|
|
41
|
+
id: Schema.String,
|
|
42
|
+
value: Schema.NullOr(Schema.String),
|
|
43
|
+
open: Schema.Boolean,
|
|
44
|
+
activeId: Schema.NullOr(Schema.String),
|
|
45
|
+
orientation: Schema.Literals(["vertical"]),
|
|
46
|
+
loop: Schema.Boolean,
|
|
47
|
+
rtl: Schema.Boolean,
|
|
48
|
+
virtualFocus: Schema.Boolean,
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* Creates hydrated Select state. The caller supplies id; value and activeId default null, open
|
|
52
|
+
* false, and loop true.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* State and collection ownership can be composed and tested independently from any renderer.
|
|
56
|
+
*
|
|
57
|
+
* The returned Effect creates the RefSubject when run. That state is renderer-independent;
|
|
58
|
+
* collection registrations belong to the separate Scope that runs register or ref, not to state
|
|
59
|
+
* creation.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* import * as Effect from "effect/Effect";
|
|
64
|
+
* import * as Select from "@typed/ui/Select";
|
|
65
|
+
*
|
|
66
|
+
* const program = Effect.scoped(
|
|
67
|
+
* Effect.gen(function* () {
|
|
68
|
+
* const state = yield* Select.makeState({ id: "timezone" });
|
|
69
|
+
* const collection = yield* Select.makeCollection();
|
|
70
|
+
* return { state: yield* state, collection: yield* collection };
|
|
71
|
+
* }),
|
|
72
|
+
* );
|
|
73
|
+
* ```
|
|
74
|
+
* @since 1.0.0
|
|
75
|
+
* @category State construction
|
|
76
|
+
*/
|
|
77
|
+
export function makeState(initial) {
|
|
78
|
+
return RefSubject.hydrate(StateSchema, {
|
|
79
|
+
id: initial.id,
|
|
80
|
+
value: initial.value ?? null,
|
|
81
|
+
open: initial.open ?? false,
|
|
82
|
+
activeId: initial.activeId ?? null,
|
|
83
|
+
orientation: "vertical",
|
|
84
|
+
loop: initial.loop ?? true,
|
|
85
|
+
rtl: false,
|
|
86
|
+
virtualFocus: false,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Creates a scoped Collection for Select items.
|
|
91
|
+
*
|
|
92
|
+
* @remarks
|
|
93
|
+
* State and collection ownership can be composed and tested independently from any renderer.
|
|
94
|
+
*
|
|
95
|
+
* The returned Effect allocates the RefSubject in the caller's Scope. Each later registration is
|
|
96
|
+
* owned by the Scope that runs register, independently of this construction Effect.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* import * as Effect from "effect/Effect";
|
|
101
|
+
* import * as Select from "@typed/ui/Select";
|
|
102
|
+
*
|
|
103
|
+
* const program = Effect.scoped(
|
|
104
|
+
* Effect.gen(function* () {
|
|
105
|
+
* const collection = yield* Select.makeCollection();
|
|
106
|
+
* return yield* collection;
|
|
107
|
+
* }),
|
|
108
|
+
* );
|
|
109
|
+
* ```
|
|
110
|
+
* @since 1.0.0
|
|
111
|
+
* @category State construction
|
|
112
|
+
*/
|
|
113
|
+
export const makeCollection = Collection.makeState;
|
|
114
|
+
/**
|
|
115
|
+
* Commits an option ID and value, then marks the popup closed.
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* This Effect changes state only. It does not check collection membership or move focus itself;
|
|
119
|
+
* the mounted popover observes the visibility change. Keep id and value consistent with the
|
|
120
|
+
* option being selected.
|
|
121
|
+
* @since 1.0.0
|
|
122
|
+
* @category State transitions
|
|
123
|
+
*/
|
|
124
|
+
export function select(state, id, value) {
|
|
125
|
+
return RefSubject.update(state, (current) => ({ ...current, activeId: id, value, open: false }));
|
|
126
|
+
}
|
|
127
|
+
function move(state, collection, direction) {
|
|
128
|
+
return Composite.moveAndFocus({ state, collection }, direction);
|
|
129
|
+
}
|
|
130
|
+
function selectActive(state, collection) {
|
|
131
|
+
return Effect.gen(function* () {
|
|
132
|
+
const activeId = (yield* state).activeId;
|
|
133
|
+
const item = activeId === null ? undefined : (yield* collection).find((item) => item.id === activeId);
|
|
134
|
+
return item?.value === undefined ? yield* state : yield* select(state, item.id, item.value);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function triggerProps(options) {
|
|
138
|
+
const id = RefSubject.map(options.state, (state) => state.id);
|
|
139
|
+
const triggerId = RefSubject.map(options.state, (state) => `${state.id}-trigger`);
|
|
140
|
+
const open = RefSubject.map(options.state, (state) => state.open);
|
|
141
|
+
return () => ({
|
|
142
|
+
id: triggerId,
|
|
143
|
+
type: "button",
|
|
144
|
+
popovertarget: id,
|
|
145
|
+
popovertargetaction: "toggle",
|
|
146
|
+
"aria-haspopup": "listbox",
|
|
147
|
+
"aria-expanded": open,
|
|
148
|
+
onkeydown: EventHandler.make((event) => {
|
|
149
|
+
if (event.key !== "ArrowDown")
|
|
150
|
+
return;
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
Dom.currentTarget(event).click();
|
|
153
|
+
}),
|
|
154
|
+
ref: Dom.composeRefs(options.state, invokerRef(options.state)),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function invokerRef(state) {
|
|
158
|
+
return Effect.fn(function* (element) {
|
|
159
|
+
invokers.set(state, element);
|
|
160
|
+
const scope = yield* Effect.scope;
|
|
161
|
+
yield* Scope.addFinalizer(scope, Effect.sync(() => {
|
|
162
|
+
if (invokers.get(state) === element)
|
|
163
|
+
invokers.delete(state);
|
|
164
|
+
}));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Renders the native button that toggles the select listbox popover.
|
|
169
|
+
*
|
|
170
|
+
* @remarks
|
|
171
|
+
* The generated button ID labels Content; popovertarget points to state.id. ArrowDown invokes
|
|
172
|
+
* the native button click. Content synchronizes native toggle events with open state. Render a
|
|
173
|
+
* useful name containing the current choice; the trigger does not format selected values for
|
|
174
|
+
* you.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* import { RefSubject } from "@typed/fx";
|
|
179
|
+
* import { html } from "@typed/template";
|
|
180
|
+
* import { component } from "@typed/template";
|
|
181
|
+
* import * as Select from "@typed/ui/Select";
|
|
182
|
+
*
|
|
183
|
+
* export const DensityPicker = component(function* () {
|
|
184
|
+
* const state = yield* Select.makeState({ id: "density-options", value: "comfortable" });
|
|
185
|
+
* const collection = yield* Select.makeCollection();
|
|
186
|
+
* const label = RefSubject.map(state, ({ value }) =>
|
|
187
|
+
* value === "compact" ? "Density: compact" : "Density: comfortable",
|
|
188
|
+
* );
|
|
189
|
+
* return html`<div class="density-picker">
|
|
190
|
+
* ${Select.Trigger({ state, content: label })}
|
|
191
|
+
* ${Select.Content({
|
|
192
|
+
* state, collection,
|
|
193
|
+
* content: [
|
|
194
|
+
* Select.Option({
|
|
195
|
+
* state, collection, id: "density-comfortable", value: "comfortable",
|
|
196
|
+
* textValue: "Comfortable", content: "Comfortable",
|
|
197
|
+
* }),
|
|
198
|
+
* Select.Option({
|
|
199
|
+
* state, collection, id: "density-compact", value: "compact",
|
|
200
|
+
* textValue: "Compact", content: "Compact",
|
|
201
|
+
* }),
|
|
202
|
+
* ],
|
|
203
|
+
* })}
|
|
204
|
+
* </div>`;
|
|
205
|
+
* });
|
|
206
|
+
* ```
|
|
207
|
+
* @since 1.0.0
|
|
208
|
+
* @category Controls
|
|
209
|
+
*/
|
|
210
|
+
export function Trigger(options, host) {
|
|
211
|
+
return Dom.renderHost()(options, host, triggerProps(options), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Alias of Trigger.
|
|
215
|
+
*
|
|
216
|
+
* @remarks
|
|
217
|
+
* Calling Select alone creates only the invoker button. Compose Content and Option for the popup
|
|
218
|
+
* choice surface; use Form.Select for a native select element.
|
|
219
|
+
* @since 1.0.0
|
|
220
|
+
* @category Controls
|
|
221
|
+
*/
|
|
222
|
+
export const Select = Trigger;
|
|
223
|
+
function contentProps(options) {
|
|
224
|
+
const id = RefSubject.map(options.state, (state) => state.id);
|
|
225
|
+
const triggerId = RefSubject.map(options.state, (state) => `${state.id}-trigger`);
|
|
226
|
+
let typeahead = { value: "", updatedAt: 0 };
|
|
227
|
+
let restoreInvokerFocus = false;
|
|
228
|
+
const restoreFocus = Effect.gen(function* () {
|
|
229
|
+
if (!restoreInvokerFocus)
|
|
230
|
+
return;
|
|
231
|
+
restoreInvokerFocus = false;
|
|
232
|
+
yield* Composite.focusElement(invokers.get(options.state));
|
|
233
|
+
});
|
|
234
|
+
const onkeydown = options.collection === undefined
|
|
235
|
+
? undefined
|
|
236
|
+
: EventHandler.make(Effect.fn(function* (event) {
|
|
237
|
+
const direction = Composite.keyMove(event, { orientation: "vertical" });
|
|
238
|
+
if (direction !== undefined) {
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
yield* move(options.state, options.collection, direction);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
244
|
+
event.preventDefault();
|
|
245
|
+
yield* selectActive(options.state, options.collection);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (event.key === "Escape") {
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
restoreInvokerFocus = true;
|
|
251
|
+
yield* setOpen(options.state, false);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const key = Composite.typeaheadKey(event);
|
|
255
|
+
if (key === null)
|
|
256
|
+
return;
|
|
257
|
+
typeahead = Composite.updateTypeaheadBuffer(typeahead, key, Date.now());
|
|
258
|
+
const activeId = Composite.typeaheadFrom(yield* options.collection, typeahead.value, (yield* options.state).activeId);
|
|
259
|
+
if (activeId === null)
|
|
260
|
+
return;
|
|
261
|
+
event.preventDefault();
|
|
262
|
+
yield* RefSubject.update(options.state, (state) => ({ ...state, activeId }));
|
|
263
|
+
yield* Composite.focusActive({ state: options.state, collection: options.collection });
|
|
264
|
+
}));
|
|
265
|
+
return () => ({
|
|
266
|
+
id,
|
|
267
|
+
role: "listbox",
|
|
268
|
+
"aria-labelledby": triggerId,
|
|
269
|
+
popover: "manual",
|
|
270
|
+
"aria-activedescendant": Composite.activeDescendant(options.state),
|
|
271
|
+
onkeydown,
|
|
272
|
+
ontoggle: EventHandler.make(Effect.fn(function* (event) {
|
|
273
|
+
const open = Dom.toggleState(event) === "open";
|
|
274
|
+
const current = yield* options.state;
|
|
275
|
+
if (current.open === open) {
|
|
276
|
+
if (!open)
|
|
277
|
+
yield* restoreFocus;
|
|
278
|
+
return current;
|
|
279
|
+
}
|
|
280
|
+
const next = yield* RefSubject.update(options.state, (state) => ({ ...state, open }));
|
|
281
|
+
if (!open) {
|
|
282
|
+
yield* restoreFocus;
|
|
283
|
+
return next;
|
|
284
|
+
}
|
|
285
|
+
if (options.collection === undefined)
|
|
286
|
+
return next;
|
|
287
|
+
const item = (yield* options.collection).find((item) => item.value === next.value);
|
|
288
|
+
if (item === undefined)
|
|
289
|
+
return next;
|
|
290
|
+
const selected = yield* RefSubject.update(options.state, (state) => ({
|
|
291
|
+
...state,
|
|
292
|
+
activeId: item.id,
|
|
293
|
+
}));
|
|
294
|
+
yield* Composite.focusActive({ state: options.state, collection: options.collection });
|
|
295
|
+
yield* Composite.scrollActive({ state: options.state, collection: options.collection });
|
|
296
|
+
return selected;
|
|
297
|
+
})),
|
|
298
|
+
ref: Dom.composeRefs(options.state, NativePopover.ref(options.state)),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Renders the manual-popover listbox and coordinates registered keyboard movement.
|
|
303
|
+
*
|
|
304
|
+
* @remarks
|
|
305
|
+
* With a collection, arrows and Home/End move focus, buffered typeahead locates options,
|
|
306
|
+
* Enter/Space commits the active choice, and Escape closes with invoker focus restoration.
|
|
307
|
+
* Opening focuses a registered option matching the selected value; an unmatched or null value
|
|
308
|
+
* does not choose the first option. Manual popover mode does not imply auto-mode outside-click
|
|
309
|
+
* dismissal.
|
|
310
|
+
* @since 1.0.0
|
|
311
|
+
* @category Controls
|
|
312
|
+
*/
|
|
313
|
+
export function Content(options, host) {
|
|
314
|
+
return Dom.renderHost()(options, host, contentProps(options), options.content, (props, content) => {
|
|
315
|
+
return html `<div ...${props}>${content}</div>`;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function optionProps(options) {
|
|
319
|
+
const selected = RefSubject.map(options.state, (state) => state.value === options.value);
|
|
320
|
+
const register = options.collection === undefined
|
|
321
|
+
? undefined
|
|
322
|
+
: Collection.ref(options.collection, {
|
|
323
|
+
id: options.id,
|
|
324
|
+
value: options.value,
|
|
325
|
+
textValue: options.textValue ?? options.value,
|
|
326
|
+
disabled: options.disabled,
|
|
327
|
+
});
|
|
328
|
+
return () => ({
|
|
329
|
+
id: options.id,
|
|
330
|
+
role: "option",
|
|
331
|
+
"aria-selected": selected,
|
|
332
|
+
"aria-disabled": options.disabled ?? false,
|
|
333
|
+
tabindex: Composite.tabIndex(options.state, options.id),
|
|
334
|
+
onclick: options.disabled === true ? Effect.void : select(options.state, options.id, options.value),
|
|
335
|
+
onfocus: options.disabled === true
|
|
336
|
+
? Effect.void
|
|
337
|
+
: RefSubject.update(options.state, (state) => ({ ...state, activeId: options.id })),
|
|
338
|
+
ref: Dom.composeRefs(register, options.ref),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Renders a selectable listbox option with distinct active and selected state.
|
|
343
|
+
*
|
|
344
|
+
* @remarks
|
|
345
|
+
* Click commits value and closes the popup; focus updates activeId without committing a new
|
|
346
|
+
* value. Register every option with the same collection used by Content for keyboard navigation.
|
|
347
|
+
* Supply textValue for readable typeahead when value is an internal code. This div is not a
|
|
348
|
+
* native form field.
|
|
349
|
+
* @since 1.0.0
|
|
350
|
+
* @category Controls
|
|
351
|
+
*/
|
|
352
|
+
export function Option(options, host) {
|
|
353
|
+
return Dom.renderHost()(options, host, optionProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
|
|
354
|
+
}
|
|
355
|
+
function setOpen(state, open) {
|
|
356
|
+
return RefSubject.update(state, (current) => ({ ...current, open }));
|
|
357
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A non-focusable semantic boundary with an orientation attribute.
|
|
3
|
+
* CSS draws the line; WindowSplitter supplies the separate adjustable-pane interaction.
|
|
4
|
+
*
|
|
5
|
+
* Read the [Separator guide](/explore/ui-separator) for a complete example.
|
|
6
|
+
*
|
|
7
|
+
* [Platform reference](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/separator_role).
|
|
8
|
+
* @since 1.0.0
|
|
9
|
+
* @category Overview
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
import type * as Scope from "effect/Scope";
|
|
13
|
+
import type { Fx } from "@typed/fx/Fx";
|
|
14
|
+
import { type Renderable, type RenderEvent, type RenderTemplate } from "@typed/template";
|
|
15
|
+
import * as Dom from "./Dom.js";
|
|
16
|
+
import type { HostResult } from "./Dom/Types.js";
|
|
17
|
+
/**
|
|
18
|
+
*/
|
|
19
|
+
export interface SeparatorOptions extends Dom.HostOptions<HTMLDivElement> {
|
|
20
|
+
/**
|
|
21
|
+
* Separator orientation, defaulting to horizontal.
|
|
22
|
+
* @since 1.0.0
|
|
23
|
+
* @category Keyboard navigation
|
|
24
|
+
*/
|
|
25
|
+
readonly orientation?: Renderable.Any<"horizontal" | "vertical" | null | undefined>;
|
|
26
|
+
}
|
|
27
|
+
declare function internalProps<const Options extends SeparatorOptions>({ property, }: Dom.InternalPropsHelpers<Options>): {
|
|
28
|
+
role: string;
|
|
29
|
+
"aria-orientation": "horizontal" | Exclude<Dom.Property<Options, "orientation">, Dom.Nullish>;
|
|
30
|
+
};
|
|
31
|
+
type SeparatorInternalProps<Options extends SeparatorOptions> = ReturnType<typeof internalProps<Options>>;
|
|
32
|
+
/**
|
|
33
|
+
* Renders a non-interactive semantic division.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* orientation defaults to horizontal and changes aria-orientation, not layout or dimensions.
|
|
37
|
+
* Supply the visible line through CSS. Do not make this host focusable to imitate a resizer;
|
|
38
|
+
* WindowSplitter owns that separate interaction.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { html } from "@typed/template";
|
|
43
|
+
* import { Heading } from "@typed/ui/Heading";
|
|
44
|
+
* import { Separator } from "@typed/ui/Separator";
|
|
45
|
+
*
|
|
46
|
+
* export const AccountSummary = html`<section>
|
|
47
|
+
* ${Heading({ level: 2, content: "Account summary" })}
|
|
48
|
+
* <p>Personal account</p>
|
|
49
|
+
* ${Separator({ props: { class: "summary-divider" } })}
|
|
50
|
+
* <p>Next renewal: September 30</p>
|
|
51
|
+
* </section>`;
|
|
52
|
+
* ```
|
|
53
|
+
* @since 1.0.0
|
|
54
|
+
* @category Structure and naming
|
|
55
|
+
*/
|
|
56
|
+
export declare function Separator<const Options extends SeparatorOptions, const Host extends HostResult = never>(options: Options, host?: Dom.HostOverride<Dom.RenderHostProps<Options, SeparatorInternalProps<Options>>, "", Host>): Fx<RenderEvent, Renderable.Error<Options | Host>, Renderable.Services<Options | Host> | Scope.Scope | RenderTemplate>;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=Separator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Separator.d.ts","sourceRoot":"","sources":["../src/Separator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAQ,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC/F,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD;GACG;AACH,MAAM,WAAW,gBAAiB,SAAQ,GAAG,CAAC,WAAW,CAAC,cAAc,CAAC;IACvE;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,YAAY,GAAG,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACrF;AAED,iBAAS,aAAa,CAAC,KAAK,CAAC,OAAO,SAAS,gBAAgB,EAAE,EAC7D,QAAQ,GACT,EAAE,GAAG,CAAC,oBAAoB,CAAC,OAAO,CAAC;IACzB,IAAI;IAAe,kBAAkB;EAC/C;AAED,KAAK,sBAAsB,CAAC,OAAO,SAAS,gBAAgB,IAAI,UAAU,CACxE,OAAO,aAAa,CAAC,OAAO,CAAC,CAC9B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,SAAS,CACvB,KAAK,CAAC,OAAO,SAAS,gBAAgB,EACtC,KAAK,CAAC,IAAI,SAAS,UAAU,GAAG,KAAK,EAErC,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,OAAO,EAAE,sBAAsB,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,GAC/F,EAAE,CACH,WAAW,EACX,UAAU,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,EAChC,UAAU,CAAC,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,GAAG,cAAc,CACnE,CAQA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { html } from "@typed/template";
|
|
2
|
+
import * as Dom from "./Dom.js";
|
|
3
|
+
function internalProps({ property, }) {
|
|
4
|
+
return { role: "separator", "aria-orientation": property("orientation", "horizontal") };
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Renders a non-interactive semantic division.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* orientation defaults to horizontal and changes aria-orientation, not layout or dimensions.
|
|
11
|
+
* Supply the visible line through CSS. Do not make this host focusable to imitate a resizer;
|
|
12
|
+
* WindowSplitter owns that separate interaction.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { html } from "@typed/template";
|
|
17
|
+
* import { Heading } from "@typed/ui/Heading";
|
|
18
|
+
* import { Separator } from "@typed/ui/Separator";
|
|
19
|
+
*
|
|
20
|
+
* export const AccountSummary = html`<section>
|
|
21
|
+
* ${Heading({ level: 2, content: "Account summary" })}
|
|
22
|
+
* <p>Personal account</p>
|
|
23
|
+
* ${Separator({ props: { class: "summary-divider" } })}
|
|
24
|
+
* <p>Next renewal: September 30</p>
|
|
25
|
+
* </section>`;
|
|
26
|
+
* ```
|
|
27
|
+
* @since 1.0.0
|
|
28
|
+
* @category Structure and naming
|
|
29
|
+
*/
|
|
30
|
+
export function Separator(options, host) {
|
|
31
|
+
return Dom.renderHost()(options, host, internalProps, "", (props) => html `<div ...${props}></div>`);
|
|
32
|
+
}
|
package/dist/Slider.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native range input publishing numeric updates during input.
|
|
3
|
+
* Keep programmatic values coherent with bounds and control expensive downstream work separately.
|
|
4
|
+
*
|
|
5
|
+
* Read the [Slider guide](/explore/ui-slider) for a complete example.
|
|
6
|
+
*
|
|
7
|
+
* [APG interaction reference](https://www.w3.org/WAI/ARIA/apg/patterns/slider/).
|
|
8
|
+
* @since 1.0.0
|
|
9
|
+
* @category Overview
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Schema from "effect/Schema";
|
|
14
|
+
import { RefSubject } from "@typed/fx";
|
|
15
|
+
import { EventHandler, type Renderable } from "@typed/template";
|
|
16
|
+
import * as Dom from "./Dom.js";
|
|
17
|
+
import type { HostResult } from "./Dom/Types.js";
|
|
18
|
+
/**
|
|
19
|
+
*/
|
|
20
|
+
export interface State {
|
|
21
|
+
/**
|
|
22
|
+
*/
|
|
23
|
+
readonly value: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
*/
|
|
27
|
+
export interface InitialState {
|
|
28
|
+
/**
|
|
29
|
+
*/
|
|
30
|
+
readonly value: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
*/
|
|
34
|
+
export declare const StateSchema: Schema.Struct<{
|
|
35
|
+
readonly value: Schema.Finite;
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* Creates hydrated slider state.
|
|
39
|
+
* @remarks
|
|
40
|
+
* State can be composed and tested independently of the renderer.
|
|
41
|
+
* The calling Effect Scope owns the returned RefSubject.
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { Effect } from "effect"
|
|
45
|
+
* import * as Slider from "@typed/ui/Slider"
|
|
46
|
+
*
|
|
47
|
+
* const program = Effect.gen(function* () {
|
|
48
|
+
* return yield* Slider.makeState({ value: 50 })
|
|
49
|
+
* })
|
|
50
|
+
* ```
|
|
51
|
+
* @since 1.0.0
|
|
52
|
+
* @category State construction
|
|
53
|
+
*/
|
|
54
|
+
export declare function makeState(initial: InitialState): Effect.Effect<RefSubject.HydratedRefSubject<{
|
|
55
|
+
readonly value: number;
|
|
56
|
+
}, Schema.SchemaError, never, never>, never, import("effect/Scope").Scope>;
|
|
57
|
+
/**
|
|
58
|
+
* Assigns a range value without clamping or snapping it.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* min, max, and step belong to the native input options and are not consulted here. Validate or
|
|
62
|
+
* normalize programmatic values before assignment so the subject and browser-sanitized range
|
|
63
|
+
* value stay consistent.
|
|
64
|
+
* @since 1.0.0
|
|
65
|
+
* @category State transitions
|
|
66
|
+
*/
|
|
67
|
+
export declare function setValue<E, R>(state: RefSubject.RefSubject<State, E, R>, value: number): Effect.Effect<State, E, R>;
|
|
68
|
+
/**
|
|
69
|
+
*/
|
|
70
|
+
export interface SliderOptions extends Dom.HostOptions<HTMLInputElement> {
|
|
71
|
+
/**
|
|
72
|
+
* Hydrated state synchronized to the input.
|
|
73
|
+
* @since 1.0.0
|
|
74
|
+
* @category State connection
|
|
75
|
+
*/
|
|
76
|
+
readonly state: RefSubject.HydratedRefSubject<State, Schema.SchemaError>;
|
|
77
|
+
/**
|
|
78
|
+
* Native minimum value.
|
|
79
|
+
* @since 1.0.0
|
|
80
|
+
* @category Numeric bounds
|
|
81
|
+
*/
|
|
82
|
+
readonly min?: Renderable.Any<number | null | undefined>;
|
|
83
|
+
/**
|
|
84
|
+
* Native maximum value.
|
|
85
|
+
* @since 1.0.0
|
|
86
|
+
* @category Numeric bounds
|
|
87
|
+
*/
|
|
88
|
+
readonly max?: Renderable.Any<number | null | undefined>;
|
|
89
|
+
/**
|
|
90
|
+
* Native step value or `"any"`.
|
|
91
|
+
* @since 1.0.0
|
|
92
|
+
* @category Numeric bounds
|
|
93
|
+
*/
|
|
94
|
+
readonly step?: Renderable.Any<number | "any" | null | undefined>;
|
|
95
|
+
}
|
|
96
|
+
declare function internalProps<const Options extends SliderOptions>(options: Options): ({ property }: Dom.InternalPropsHelpers<Options>) => {
|
|
97
|
+
readonly type: "range";
|
|
98
|
+
readonly value: RefSubject.Computed<number, Schema.SchemaError, never>;
|
|
99
|
+
readonly ".value": RefSubject.Computed<string, Schema.SchemaError, never>;
|
|
100
|
+
readonly min: Exclude<Dom.Property<Options, "min">, Dom.Nullish> | undefined;
|
|
101
|
+
readonly max: Exclude<Dom.Property<Options, "max">, Dom.Nullish> | undefined;
|
|
102
|
+
readonly step: Exclude<Dom.Property<Options, "step">, Dom.Nullish> | undefined;
|
|
103
|
+
readonly oninput: EventHandler.EventHandler<Event, Schema.SchemaError, never>;
|
|
104
|
+
readonly ref: RefSubject.HydratedRefSubject<State, Schema.SchemaError, never, never>;
|
|
105
|
+
};
|
|
106
|
+
type SliderInternalProps<Options extends SliderOptions> = ReturnType<ReturnType<typeof internalProps<Options>>>;
|
|
107
|
+
/**
|
|
108
|
+
* Renders a native range input whose input events update numeric state.
|
|
109
|
+
*
|
|
110
|
+
* @remarks
|
|
111
|
+
* Dragging publishes through input, so expensive downstream work should control its own update
|
|
112
|
+
* rate. Native min/max/step, keyboard interaction, and pointer behavior stay with the browser.
|
|
113
|
+
* Supply a label and a unit-bearing readout; a custom non-input host must implement the
|
|
114
|
+
* interaction itself.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* import { RefSubject } from "@typed/fx";
|
|
119
|
+
* import { html } from "@typed/template";
|
|
120
|
+
* import { component } from "@typed/template";
|
|
121
|
+
* import * as Slider from "@typed/ui/Slider";
|
|
122
|
+
*
|
|
123
|
+
* export const ZoomControl = component(function* () {
|
|
124
|
+
* const state = yield* Slider.makeState({ value: 100 });
|
|
125
|
+
* const percentage = RefSubject.map(state, ({ value }) => `${value}%`);
|
|
126
|
+
* return html`<div class="zoom-control">
|
|
127
|
+
* <label for="preview-zoom">Preview zoom</label>
|
|
128
|
+
* ${Slider.Slider({
|
|
129
|
+
* state, min: 50, max: 200, step: 10,
|
|
130
|
+
* props: { id: "preview-zoom", name: "zoom", "aria-valuetext": percentage },
|
|
131
|
+
* })}
|
|
132
|
+
* <output for="preview-zoom">${percentage}</output>
|
|
133
|
+
* </div>`;
|
|
134
|
+
* });
|
|
135
|
+
* ```
|
|
136
|
+
* @since 1.0.0
|
|
137
|
+
* @category Native controls
|
|
138
|
+
*/
|
|
139
|
+
export declare function Slider<const Options extends SliderOptions, const Host extends HostResult = never>(options: Options, host?: Dom.HostOverride<Dom.RenderHostProps<Options, SliderInternalProps<Options>>, "", Host>): import("./Dom/Types.js").HostComponent<Host | Options>;
|
|
140
|
+
export {};
|
|
141
|
+
//# sourceMappingURL=Slider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Slider.d.ts","sourceRoot":"","sources":["../src/Slider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,YAAY,EAAQ,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD;GACG;AACH,MAAM,WAAW,KAAK;IACpB;OACG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;GACG;AACH,MAAM,WAAW,YAAY;IAC3B;OACG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;GACG;AACH,eAAO,MAAM,WAAW;;EAA0C,CAAC;AAEnE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,YAAY;;2EAE9C;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,CAAC,EAC3B,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,EACzC,KAAK,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAE5B;AAED;GACG;AACH,MAAM,WAAW,aAAc,SAAQ,GAAG,CAAC,WAAW,CAAC,gBAAgB,CAAC;IACtE;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACzE;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACzD;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACzD;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,MAAM,GAAG,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACnE;AAED,iBAAS,aAAa,CAAC,KAAK,CAAC,OAAO,SAAS,aAAa,EAAE,OAAO,EAAE,OAAO,kBACpD,GAAG,CAAC,oBAAoB,CAAC,OAAO,CAAC;aAEnD,IAAI,EAAE,OAAO;aACb,KAAK;aACL,QAAQ;aACR,GAAG;aACH,GAAG;aACH,IAAI;aACJ,OAAO;aAKP,GAAG;EAER;AACD,KAAK,mBAAmB,CAAC,OAAO,SAAS,aAAa,IAAI,UAAU,CAClE,UAAU,CAAC,OAAO,aAAa,CAAC,OAAO,CAAC,CAAC,CAC1C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,MAAM,CAAC,KAAK,CAAC,OAAO,SAAS,aAAa,EAAE,KAAK,CAAC,IAAI,SAAS,UAAU,GAAG,KAAK,EAC/F,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,OAAO,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,0DAW9F"}
|