@expressive/ui 0.0.0 → 0.1.0
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 +47 -1
- package/dist/button/base.d.ts +63 -0
- package/dist/button/base.js +90 -0
- package/dist/button/base.js.map +1 -0
- package/dist/button/button.d.ts +56 -0
- package/dist/button/button.js +138 -0
- package/dist/button/button.js.map +1 -0
- package/dist/calendar/base.d.ts +112 -0
- package/dist/calendar/base.js +235 -0
- package/dist/calendar/base.js.map +1 -0
- package/dist/calendar/calendar.d.ts +78 -0
- package/dist/calendar/calendar.js +145 -0
- package/dist/calendar/calendar.js.map +1 -0
- package/dist/calendar/date.js +56 -0
- package/dist/calendar/date.js.map +1 -0
- package/dist/dialog/base.d.ts +74 -0
- package/dist/dialog/base.js +143 -0
- package/dist/dialog/base.js.map +1 -0
- package/dist/dialog/dialog.d.ts +69 -0
- package/dist/dialog/dialog.js +108 -0
- package/dist/dialog/dialog.js.map +1 -0
- package/dist/dom.d.ts +16 -0
- package/dist/dom.js +17 -0
- package/dist/dropzone/base.d.ts +77 -0
- package/dist/dropzone/base.js +159 -0
- package/dist/dropzone/base.js.map +1 -0
- package/dist/dropzone/dropzone.d.ts +38 -0
- package/dist/dropzone/dropzone.js +92 -0
- package/dist/dropzone/dropzone.js.map +1 -0
- package/dist/form/base.d.ts +157 -0
- package/dist/form/base.js +243 -0
- package/dist/form/base.js.map +1 -0
- package/dist/form/form.d.ts +123 -0
- package/dist/form/form.js +179 -0
- package/dist/form/form.js.map +1 -0
- package/dist/icons/caret.d.ts +8 -0
- package/dist/icons/caret.js +16 -0
- package/dist/icons/caret.js.map +1 -0
- package/dist/icons/check.d.ts +8 -0
- package/dist/icons/check.js +16 -0
- package/dist/icons/check.js.map +1 -0
- package/dist/icons/cross.d.ts +8 -0
- package/dist/icons/cross.js +16 -0
- package/dist/icons/cross.js.map +1 -0
- package/dist/icons/icon.js +16 -0
- package/dist/icons/icon.js.map +1 -0
- package/dist/icons/spinner.d.ts +8 -0
- package/dist/icons/spinner.js +27 -0
- package/dist/icons/spinner.js.map +1 -0
- package/dist/icons/upload.d.ts +8 -0
- package/dist/icons/upload.js +16 -0
- package/dist/icons/upload.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/layout/base.d.ts +88 -0
- package/dist/layout/base.js +158 -0
- package/dist/layout/base.js.map +1 -0
- package/dist/layout/layout.d.ts +53 -0
- package/dist/layout/layout.js +80 -0
- package/dist/layout/layout.js.map +1 -0
- package/dist/piece.js +13 -0
- package/dist/piece.js.map +1 -0
- package/dist/popover/base.d.ts +94 -0
- package/dist/popover/base.js +176 -0
- package/dist/popover/base.js.map +1 -0
- package/dist/popover/place.d.ts +12 -0
- package/dist/popover/place.js +32 -0
- package/dist/popover/place.js.map +1 -0
- package/dist/popover/popover.d.ts +52 -0
- package/dist/popover/popover.js +104 -0
- package/dist/popover/popover.js.map +1 -0
- package/dist/select/base.d.ts +196 -0
- package/dist/select/base.js +409 -0
- package/dist/select/base.js.map +1 -0
- package/dist/select/select.d.ts +119 -0
- package/dist/select/select.js +219 -0
- package/dist/select/select.js.map +1 -0
- package/dist/table/base.d.ts +96 -0
- package/dist/table/base.js +154 -0
- package/dist/table/base.js.map +1 -0
- package/dist/table/column.d.ts +53 -0
- package/dist/table/column.js +29 -0
- package/dist/table/column.js.map +1 -0
- package/dist/table/table.d.ts +41 -0
- package/dist/table/table.js +101 -0
- package/dist/table/table.js.map +1 -0
- package/package.json +68 -3
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { missing, none } from "../piece.js";
|
|
2
|
+
import { place } from "../popover/place.js";
|
|
3
|
+
import { Component, ref } from "@expressive/mvc";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "@expressive/mvc/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/select/base.tsx
|
|
7
|
+
let ID = 0;
|
|
8
|
+
const CONTROL = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
const MENU = /* @__PURE__ */ new WeakMap();
|
|
10
|
+
const Layer = ({ children }) => children;
|
|
11
|
+
/**
|
|
12
|
+
* Combobox behavior without a look: state, keyboard, announcements and ARIA, assembled from pieces.
|
|
13
|
+
*
|
|
14
|
+
* Every visible piece is a placeholder that throws when rendered - a skin assigns each one it renders.
|
|
15
|
+
* Pieces spread the matching `*Props` getter onto their element to stay accessible. `Layer` wraps the menu and renders
|
|
16
|
+
* in place by default; a skin replaces it to move the menu out of clipping containers, placed at `place`.
|
|
17
|
+
*/
|
|
18
|
+
var Select = class extends Component {
|
|
19
|
+
options = [];
|
|
20
|
+
value = null;
|
|
21
|
+
onChange = void 0;
|
|
22
|
+
multi = false;
|
|
23
|
+
/** Whether Backspace and `clear` remove values. Defaults to `multi`. */
|
|
24
|
+
clearable = void 0;
|
|
25
|
+
searchable = true;
|
|
26
|
+
disabled = false;
|
|
27
|
+
placeholder = "Select…";
|
|
28
|
+
/** The input's id, for a `<label htmlFor>`; prefixes the listbox and option ids. */
|
|
29
|
+
id = `select-${++ID}`;
|
|
30
|
+
query = "";
|
|
31
|
+
open = false;
|
|
32
|
+
focused = false;
|
|
33
|
+
highlighted = 0;
|
|
34
|
+
message = "";
|
|
35
|
+
input = ref();
|
|
36
|
+
/** Where the menu sits, once measured. */
|
|
37
|
+
place = void 0;
|
|
38
|
+
Root = missing("Select.Root");
|
|
39
|
+
Control = missing("Select.Control");
|
|
40
|
+
Values = missing("Select.Values");
|
|
41
|
+
Chip = missing("Select.Chip");
|
|
42
|
+
Value = missing("Select.Value");
|
|
43
|
+
Input = missing("Select.Input");
|
|
44
|
+
Clear = missing("Select.Clear");
|
|
45
|
+
Toggle = missing("Select.Toggle");
|
|
46
|
+
Layer = Layer;
|
|
47
|
+
Menu = missing("Select.Menu");
|
|
48
|
+
Option = missing("Select.Option");
|
|
49
|
+
Empty = none;
|
|
50
|
+
Live = Live;
|
|
51
|
+
constructor(...args) {
|
|
52
|
+
super(...args, () => {
|
|
53
|
+
this.get(({ open }) => {
|
|
54
|
+
if (!open) return;
|
|
55
|
+
const release = this.listen();
|
|
56
|
+
return () => {
|
|
57
|
+
release();
|
|
58
|
+
this.place = void 0;
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
get selection() {
|
|
64
|
+
const { value } = this;
|
|
65
|
+
return Array.isArray(value) ? value : value ? [value] : [];
|
|
66
|
+
}
|
|
67
|
+
/** Options matching `query`, accent- and case-insensitive; skips disabled and, when `multi`, chosen ones. */
|
|
68
|
+
get filtered() {
|
|
69
|
+
const query = fold(this.query);
|
|
70
|
+
const taken = this.multi ? this.selection : [];
|
|
71
|
+
return this.options.filter((option) => {
|
|
72
|
+
return !option.disabled && !taken.some((chosen) => chosen.value === option.value) && fold(`${option.label} ${String(option.value)}`).includes(query);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** The highlighted option while open. */
|
|
76
|
+
get active() {
|
|
77
|
+
return this.open ? this.filtered[this.highlighted] : void 0;
|
|
78
|
+
}
|
|
79
|
+
get canClear() {
|
|
80
|
+
return (this.clearable ?? this.multi) && this.selection.length > 0;
|
|
81
|
+
}
|
|
82
|
+
/** Text for the `aria-live` region; empty while unfocused. */
|
|
83
|
+
get announcement() {
|
|
84
|
+
if (!this.focused) return "";
|
|
85
|
+
const count = this.filtered.length;
|
|
86
|
+
const results = this.open ? `${count} result${count === 1 ? "" : "s"} available${this.query ? ` for ${this.query}` : ""}.` : "";
|
|
87
|
+
return [this.message, results].filter(Boolean).join(" ");
|
|
88
|
+
}
|
|
89
|
+
/** For the element wrapping values and buttons: a press anywhere focuses the input and opens. */
|
|
90
|
+
get controlProps() {
|
|
91
|
+
return {
|
|
92
|
+
ref: this.anchor,
|
|
93
|
+
onMouseDown: this.press
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
get inputProps() {
|
|
97
|
+
const { id, input, open, query, active, highlighted, disabled, searchable, selection, placeholder } = this;
|
|
98
|
+
return {
|
|
99
|
+
ref: input,
|
|
100
|
+
id,
|
|
101
|
+
role: "combobox",
|
|
102
|
+
"aria-autocomplete": "list",
|
|
103
|
+
"aria-expanded": open,
|
|
104
|
+
"aria-controls": `${id}-listbox`,
|
|
105
|
+
"aria-activedescendant": active && `${id}-option-${highlighted}`,
|
|
106
|
+
autoComplete: "off",
|
|
107
|
+
disabled,
|
|
108
|
+
readOnly: !searchable,
|
|
109
|
+
placeholder: selection.length ? "" : placeholder,
|
|
110
|
+
value: query,
|
|
111
|
+
onInput: (event) => this.search(event.currentTarget.value),
|
|
112
|
+
onFocus: this.focus,
|
|
113
|
+
onBlur: this.blur,
|
|
114
|
+
onKeyDown: this.keyDown
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
get clearProps() {
|
|
118
|
+
return {
|
|
119
|
+
type: "button",
|
|
120
|
+
tabIndex: -1,
|
|
121
|
+
"aria-label": "Clear selection",
|
|
122
|
+
onMouseDown: hold,
|
|
123
|
+
onClick: () => {
|
|
124
|
+
this.clear();
|
|
125
|
+
this.input.current?.focus();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
get toggleProps() {
|
|
130
|
+
return {
|
|
131
|
+
type: "button",
|
|
132
|
+
tabIndex: -1,
|
|
133
|
+
"aria-label": this.open ? "Hide options" : "Show options",
|
|
134
|
+
onMouseDown: hold,
|
|
135
|
+
onClick: () => {
|
|
136
|
+
this.input.current?.focus();
|
|
137
|
+
this.toggle();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** For a chip's remove button. */
|
|
142
|
+
get removeProps() {
|
|
143
|
+
return (option) => ({
|
|
144
|
+
type: "button",
|
|
145
|
+
tabIndex: -1,
|
|
146
|
+
"aria-label": `Remove ${option.label}`,
|
|
147
|
+
onMouseDown: hold,
|
|
148
|
+
onClick: () => this.remove(option)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** For the menu: hidden at the origin until measured, then at `place`, as wide as the control. */
|
|
152
|
+
get menuProps() {
|
|
153
|
+
const { id, multi, place } = this;
|
|
154
|
+
return {
|
|
155
|
+
ref: this.mount,
|
|
156
|
+
id: `${id}-listbox`,
|
|
157
|
+
role: "listbox",
|
|
158
|
+
"aria-multiselectable": multi,
|
|
159
|
+
onMouseDown: hold,
|
|
160
|
+
style: place ? {
|
|
161
|
+
left: `${place.left}px`,
|
|
162
|
+
top: `${place.top}px`,
|
|
163
|
+
width: `${place.width}px`
|
|
164
|
+
} : {
|
|
165
|
+
left: "0px",
|
|
166
|
+
top: "0px",
|
|
167
|
+
opacity: "0"
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** For an option's element, with `active` and `selected` for styling. */
|
|
172
|
+
get optionProps() {
|
|
173
|
+
const { id, highlighted, selection } = this;
|
|
174
|
+
return (option, index) => {
|
|
175
|
+
const active = index === highlighted;
|
|
176
|
+
const selected = selection.some((chosen) => chosen.value === option.value);
|
|
177
|
+
return {
|
|
178
|
+
active,
|
|
179
|
+
selected,
|
|
180
|
+
props: {
|
|
181
|
+
ref: active ? this.reveal : void 0,
|
|
182
|
+
id: `${id}-option-${index}`,
|
|
183
|
+
role: "option",
|
|
184
|
+
"aria-selected": selected,
|
|
185
|
+
onMouseMove: () => this.highlighted = index,
|
|
186
|
+
onClick: () => this.choose(option)
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
isSelected(option) {
|
|
192
|
+
return this.selection.some((chosen) => chosen.value === option.value);
|
|
193
|
+
}
|
|
194
|
+
choose(option) {
|
|
195
|
+
if (this.disabled || option.disabled) return;
|
|
196
|
+
this.update(this.multi ? [...this.selection, option] : option);
|
|
197
|
+
this.query = "";
|
|
198
|
+
this.open = false;
|
|
199
|
+
this.message = `Selected ${option.label}.`;
|
|
200
|
+
}
|
|
201
|
+
remove(option) {
|
|
202
|
+
if (this.disabled) return;
|
|
203
|
+
const rest = this.selection.filter((chosen) => chosen.value !== option.value);
|
|
204
|
+
this.update(this.multi ? rest : null);
|
|
205
|
+
this.query = "";
|
|
206
|
+
this.message = this.multi ? `Removed ${option.label}.` : "Selection cleared.";
|
|
207
|
+
}
|
|
208
|
+
clear() {
|
|
209
|
+
if (this.disabled) return;
|
|
210
|
+
this.update(this.multi ? [] : null);
|
|
211
|
+
this.query = "";
|
|
212
|
+
this.open = false;
|
|
213
|
+
this.message = "Selection cleared.";
|
|
214
|
+
}
|
|
215
|
+
search(query) {
|
|
216
|
+
if (!this.searchable) return;
|
|
217
|
+
this.query = query;
|
|
218
|
+
this.message = "";
|
|
219
|
+
this.highlighted = 0;
|
|
220
|
+
this.show();
|
|
221
|
+
}
|
|
222
|
+
/** Open, highlighting the current choice - else the first option, or the last when `last`. */
|
|
223
|
+
show(last = false) {
|
|
224
|
+
if (this.disabled || this.open) return;
|
|
225
|
+
const { filtered } = this;
|
|
226
|
+
const current = filtered.findIndex((option) => this.isSelected(option));
|
|
227
|
+
this.highlighted = current >= 0 ? current : last ? filtered.length - 1 : 0;
|
|
228
|
+
this.open = true;
|
|
229
|
+
}
|
|
230
|
+
hide() {
|
|
231
|
+
this.open = false;
|
|
232
|
+
this.query = "";
|
|
233
|
+
}
|
|
234
|
+
toggle() {
|
|
235
|
+
if (this.open) this.hide();
|
|
236
|
+
else this.show();
|
|
237
|
+
}
|
|
238
|
+
focus() {
|
|
239
|
+
this.focused = true;
|
|
240
|
+
this.message = this.selection.length ? `Selected ${this.selection.map((option) => option.label).join(", ")}.` : "";
|
|
241
|
+
}
|
|
242
|
+
blur() {
|
|
243
|
+
this.focused = false;
|
|
244
|
+
this.hide();
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Arrows, Home/End, PageUp/PageDown move the highlight; Enter, Space and Tab choose;
|
|
248
|
+
* Escape closes; Backspace removes the last value.
|
|
249
|
+
*
|
|
250
|
+
* Calls `preventDefault` and `stopPropagation` on keys it consumes. Tab and Backspace pass through.
|
|
251
|
+
*/
|
|
252
|
+
keyDown(event) {
|
|
253
|
+
if (this.disabled || event.isComposing) return;
|
|
254
|
+
const { open, filtered, highlighted, active } = this;
|
|
255
|
+
const last = filtered.length - 1;
|
|
256
|
+
switch (event.key) {
|
|
257
|
+
case "ArrowDown":
|
|
258
|
+
case "ArrowUp": {
|
|
259
|
+
const step = event.key === "ArrowDown" ? 1 : -1;
|
|
260
|
+
if (!open) this.show(step < 0);
|
|
261
|
+
else this.highlighted = filtered.length ? (highlighted + step + filtered.length) % filtered.length : 0;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case "Home":
|
|
265
|
+
case "End":
|
|
266
|
+
if (!open) return;
|
|
267
|
+
this.highlighted = event.key === "Home" ? 0 : last;
|
|
268
|
+
break;
|
|
269
|
+
case "PageDown":
|
|
270
|
+
case "PageUp":
|
|
271
|
+
if (!open) return;
|
|
272
|
+
this.highlighted = Math.max(0, Math.min(last, highlighted + (event.key === "PageDown" ? 5 : -5)));
|
|
273
|
+
break;
|
|
274
|
+
case "Enter":
|
|
275
|
+
if (!active) return;
|
|
276
|
+
this.choose(active);
|
|
277
|
+
break;
|
|
278
|
+
case "Escape":
|
|
279
|
+
if (!open) return;
|
|
280
|
+
this.hide();
|
|
281
|
+
break;
|
|
282
|
+
case " ":
|
|
283
|
+
if (this.query) return;
|
|
284
|
+
if (active) this.choose(active);
|
|
285
|
+
else this.show();
|
|
286
|
+
break;
|
|
287
|
+
case "Backspace":
|
|
288
|
+
if (!this.query && this.canClear) this.remove(this.selection[this.selection.length - 1]);
|
|
289
|
+
return;
|
|
290
|
+
case "Tab":
|
|
291
|
+
if (active && !event.shiftKey) this.choose(active);
|
|
292
|
+
this.open = false;
|
|
293
|
+
return;
|
|
294
|
+
default: return;
|
|
295
|
+
}
|
|
296
|
+
event.preventDefault();
|
|
297
|
+
event.stopPropagation();
|
|
298
|
+
}
|
|
299
|
+
/** Focus the input and open, from a press anywhere on the control. */
|
|
300
|
+
press(event) {
|
|
301
|
+
if (event.target !== this.input.current) event.preventDefault();
|
|
302
|
+
this.input.current?.focus();
|
|
303
|
+
this.show();
|
|
304
|
+
}
|
|
305
|
+
/** Follow scroll and resize while open, keeping the menu at the control; returns the release. Defaults to `window`. */
|
|
306
|
+
listen() {
|
|
307
|
+
const move = () => this.reposition();
|
|
308
|
+
window.addEventListener("resize", move);
|
|
309
|
+
window.addEventListener("scroll", move, true);
|
|
310
|
+
return () => {
|
|
311
|
+
window.removeEventListener("resize", move);
|
|
312
|
+
window.removeEventListener("scroll", move, true);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/** Where the menu fits by the control, or `undefined` before both mount. Defaults to the DOM's layout. */
|
|
316
|
+
measure() {
|
|
317
|
+
const control = CONTROL.get(this);
|
|
318
|
+
const menu = MENU.get(this);
|
|
319
|
+
if (!control || !menu) return;
|
|
320
|
+
const anchor = control.getBoundingClientRect();
|
|
321
|
+
const { left, top, side } = place({
|
|
322
|
+
anchor,
|
|
323
|
+
width: anchor.width,
|
|
324
|
+
height: menu.offsetHeight,
|
|
325
|
+
view: {
|
|
326
|
+
left: 0,
|
|
327
|
+
top: 0,
|
|
328
|
+
width: window.innerWidth,
|
|
329
|
+
height: window.innerHeight
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
left,
|
|
334
|
+
top,
|
|
335
|
+
width: anchor.width,
|
|
336
|
+
above: side == "above"
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/** Re-measure and move the menu when its place changed; for a `listen` that follows layout. */
|
|
340
|
+
reposition() {
|
|
341
|
+
const next = this.measure();
|
|
342
|
+
if (!next) return;
|
|
343
|
+
const { place: last } = this;
|
|
344
|
+
if (!last || Object.keys(next).some((key) => last[key] != next[key])) this.place = next;
|
|
345
|
+
}
|
|
346
|
+
/** Ref for the control, in `controlProps`; the menu anchors to it. */
|
|
347
|
+
anchor(element) {
|
|
348
|
+
if (element) CONTROL.set(this, element);
|
|
349
|
+
}
|
|
350
|
+
/** Ref for the menu, in `menuProps`; places it once mounted. */
|
|
351
|
+
mount(element) {
|
|
352
|
+
if (!element) return;
|
|
353
|
+
MENU.set(this, element);
|
|
354
|
+
this.reposition();
|
|
355
|
+
}
|
|
356
|
+
/** Ref for the highlighted option, in `optionProps`; keeps it in view. Defaults to `scrollIntoView`. */
|
|
357
|
+
reveal(element) {
|
|
358
|
+
element?.scrollIntoView({ block: "nearest" });
|
|
359
|
+
}
|
|
360
|
+
update(value) {
|
|
361
|
+
this.value = value;
|
|
362
|
+
if (this.onChange) this.onChange(value);
|
|
363
|
+
}
|
|
364
|
+
render() {
|
|
365
|
+
const { open, multi, query, selection, filtered, canClear } = this;
|
|
366
|
+
const value = !multi && !query && selection[0];
|
|
367
|
+
const empty = filtered.length == 0;
|
|
368
|
+
return /* @__PURE__ */ jsxs(this.Root, { children: [
|
|
369
|
+
/* @__PURE__ */ jsxs(this.Control, { children: [
|
|
370
|
+
/* @__PURE__ */ jsxs(this.Values, { children: [/* @__PURE__ */ jsxs(Fragment, { children: [multi && selection.map((option) => /* @__PURE__ */ jsx(this.Chip, { option }, String(option.value))), value && /* @__PURE__ */ jsx(this.Value, { option: value })] }), /* @__PURE__ */ jsx(this.Input, {}, "input")] }),
|
|
371
|
+
canClear && /* @__PURE__ */ jsx(this.Clear, {}),
|
|
372
|
+
/* @__PURE__ */ jsx(this.Toggle, {})
|
|
373
|
+
] }),
|
|
374
|
+
open && /* @__PURE__ */ jsx(this.Layer, { children: /* @__PURE__ */ jsxs(this.Menu, { children: [filtered.map((option, index) => /* @__PURE__ */ jsx(this.Option, {
|
|
375
|
+
option,
|
|
376
|
+
index
|
|
377
|
+
}, String(option.value))), empty && /* @__PURE__ */ jsx(this.Empty, {})] }) }),
|
|
378
|
+
/* @__PURE__ */ jsx(this.Live, {})
|
|
379
|
+
] });
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
const hidden = {
|
|
383
|
+
position: "absolute",
|
|
384
|
+
width: "1px",
|
|
385
|
+
height: "1px",
|
|
386
|
+
overflow: "hidden",
|
|
387
|
+
clip: "rect(0 0 0 0)",
|
|
388
|
+
whiteSpace: "nowrap"
|
|
389
|
+
};
|
|
390
|
+
const Live = () => {
|
|
391
|
+
const { announcement } = Select.get();
|
|
392
|
+
return /* @__PURE__ */ jsx("span", {
|
|
393
|
+
"aria-live": "polite",
|
|
394
|
+
"aria-atomic": "true",
|
|
395
|
+
style: hidden,
|
|
396
|
+
children: announcement
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
function hold(event) {
|
|
400
|
+
event.preventDefault();
|
|
401
|
+
event.stopPropagation();
|
|
402
|
+
}
|
|
403
|
+
function fold(text) {
|
|
404
|
+
return text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
//#endregion
|
|
408
|
+
export { Select };
|
|
409
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.js","names":[],"sources":["../../src/select/base.tsx"],"sourcesContent":["import { Component, ref, type Model } from '@expressive/mvc';\n\nimport { missing, none } from '../piece';\nimport { place } from '../popover/place';\n\ndeclare namespace Select {\n interface Option<T = unknown> {\n value: T;\n label: string;\n disabled?: boolean;\n }\n\n /** One option, or an array when `multi`. */\n type Value = Option | Option[] | null;\n\n type Key = Pick<\n KeyboardEvent,\n 'key' | 'shiftKey' | 'isComposing' | 'preventDefault' | 'stopPropagation'\n >;\n\n /** Where the menu sits, in viewport pixels; `above` when it flipped over the control. */\n interface Place {\n left: number;\n top: number;\n width: number;\n above: boolean;\n }\n\n type Wrap = (props: { children: Component.Node }) => Component.Node;\n type Each = (props: { option: Option }) => Component.Node;\n type Item = (props: { option: Option; index: number }) => Component.Node;\n type Part = () => Component.Node;\n}\n\nlet ID = 0;\n\nconst CONTROL = new WeakMap<Select, HTMLElement>();\n\nconst MENU = new WeakMap<Select, HTMLElement>();\n\nconst Layer = ({ children }: { children: Component.Node }): Component.Node => children;\n\n/**\n * Combobox behavior without a look: state, keyboard, announcements and ARIA, assembled from pieces.\n *\n * Every visible piece is a placeholder that throws when rendered - a skin assigns each one it renders.\n * Pieces spread the matching `*Props` getter onto their element to stay accessible. `Layer` wraps the menu and renders\n * in place by default; a skin replaces it to move the menu out of clipping containers, placed at `place`.\n */\nclass Select extends Component {\n options: readonly Select.Option[] = [];\n value: Select.Value = null;\n onChange?: (value: Select.Value) => void = undefined;\n\n multi = false;\n\n /** Whether Backspace and `clear` remove values. Defaults to `multi`. */\n clearable?: boolean = undefined;\n\n searchable = true;\n disabled = false;\n placeholder = 'Select…';\n\n /** The input's id, for a `<label htmlFor>`; prefixes the listbox and option ids. */\n id = `select-${++ID}`;\n\n query = '';\n open = false;\n focused = false;\n highlighted = 0;\n message = '';\n\n input = ref<HTMLInputElement>();\n\n /** Where the menu sits, once measured. */\n place?: Select.Place = undefined;\n\n Root: Select.Wrap = missing('Select.Root');\n Control: Select.Wrap = missing('Select.Control');\n Values: Select.Wrap = missing('Select.Values');\n Chip: Select.Each = missing('Select.Chip');\n Value: Select.Each = missing('Select.Value');\n Input: Select.Part = missing('Select.Input');\n Clear: Select.Part = missing('Select.Clear');\n Toggle: Select.Part = missing('Select.Toggle');\n Layer: Select.Wrap = Layer;\n Menu: Select.Wrap = missing('Select.Menu');\n Option: Select.Item = missing('Select.Option');\n Empty: Select.Part = none;\n Live: Select.Part = Live;\n\n constructor(...args: Model.Args) {\n super(...args, () => {\n this.get(({ open }) => {\n if (!open) return;\n\n const release = this.listen();\n\n return () => {\n release();\n this.place = undefined;\n };\n });\n });\n }\n\n get selection(): Select.Option[] {\n const { value } = this;\n\n return Array.isArray(value) ? value : value ? [value] : [];\n }\n\n /** Options matching `query`, accent- and case-insensitive; skips disabled and, when `multi`, chosen ones. */\n get filtered(): Select.Option[] {\n const query = fold(this.query);\n const taken = this.multi ? this.selection : [];\n\n return this.options.filter((option) => {\n const available = !option.disabled && !taken.some((chosen) => chosen.value === option.value);\n\n return available && fold(`${option.label} ${String(option.value)}`).includes(query);\n });\n }\n\n /** The highlighted option while open. */\n get active(): Select.Option | undefined {\n return this.open ? this.filtered[this.highlighted] : undefined;\n }\n\n get canClear(): boolean {\n return (this.clearable ?? this.multi) && this.selection.length > 0;\n }\n\n /** Text for the `aria-live` region; empty while unfocused. */\n get announcement(): string {\n if (!this.focused) return '';\n\n const count = this.filtered.length;\n const results = this.open\n ? `${count} result${count === 1 ? '' : 's'} available${this.query ? ` for ${this.query}` : ''}.`\n : '';\n\n return [this.message, results].filter(Boolean).join(' ');\n }\n\n /** For the element wrapping values and buttons: a press anywhere focuses the input and opens. */\n get controlProps() {\n return { ref: this.anchor, onMouseDown: this.press };\n }\n\n get inputProps() {\n const { id, input, open, query, active, highlighted, disabled, searchable, selection, placeholder } = this;\n\n return {\n ref: input,\n id,\n role: 'combobox',\n 'aria-autocomplete': 'list',\n 'aria-expanded': open,\n 'aria-controls': `${id}-listbox`,\n 'aria-activedescendant': active && `${id}-option-${highlighted}`,\n autoComplete: 'off',\n disabled,\n readOnly: !searchable,\n placeholder: selection.length ? '' : placeholder,\n value: query,\n onInput: (event: Event) => this.search((event.currentTarget as HTMLInputElement).value),\n onFocus: this.focus,\n onBlur: this.blur,\n onKeyDown: this.keyDown\n } as const;\n }\n\n get clearProps() {\n return {\n type: 'button',\n tabIndex: -1,\n 'aria-label': 'Clear selection',\n onMouseDown: hold,\n onClick: () => {\n this.clear();\n this.input.current?.focus();\n }\n } as const;\n }\n\n get toggleProps() {\n return {\n type: 'button',\n tabIndex: -1,\n 'aria-label': this.open ? 'Hide options' : 'Show options',\n onMouseDown: hold,\n onClick: () => {\n this.input.current?.focus();\n this.toggle();\n }\n } as const;\n }\n\n /** For a chip's remove button. */\n get removeProps() {\n return (option: Select.Option) =>\n ({\n type: 'button',\n tabIndex: -1,\n 'aria-label': `Remove ${option.label}`,\n onMouseDown: hold,\n onClick: () => this.remove(option)\n }) as const;\n }\n\n /** For the menu: hidden at the origin until measured, then at `place`, as wide as the control. */\n get menuProps() {\n const { id, multi, place } = this;\n\n return {\n ref: this.mount,\n id: `${id}-listbox`,\n role: 'listbox',\n 'aria-multiselectable': multi,\n onMouseDown: hold,\n style: place\n ? { left: `${place.left}px`, top: `${place.top}px`, width: `${place.width}px` }\n : { left: '0px', top: '0px', opacity: '0' }\n } as const;\n }\n\n /** For an option's element, with `active` and `selected` for styling. */\n get optionProps() {\n const { id, highlighted, selection } = this;\n\n return (option: Select.Option, index: number) => {\n const active = index === highlighted;\n const selected = selection.some((chosen) => chosen.value === option.value);\n\n return {\n active,\n selected,\n props: {\n ref: active ? this.reveal : undefined,\n id: `${id}-option-${index}`,\n role: 'option',\n 'aria-selected': selected,\n onMouseMove: () => (this.highlighted = index),\n onClick: () => this.choose(option)\n } as const\n };\n };\n }\n\n isSelected(option: Select.Option) {\n return this.selection.some((chosen) => chosen.value === option.value);\n }\n\n choose(option: Select.Option) {\n if (this.disabled || option.disabled) return;\n\n this.update(this.multi ? [...this.selection, option] : option);\n this.query = '';\n this.open = false;\n this.message = `Selected ${option.label}.`;\n }\n\n remove(option: Select.Option) {\n if (this.disabled) return;\n\n const rest = this.selection.filter((chosen) => chosen.value !== option.value);\n\n this.update(this.multi ? rest : null);\n this.query = '';\n this.message = this.multi ? `Removed ${option.label}.` : 'Selection cleared.';\n }\n\n clear() {\n if (this.disabled) return;\n\n this.update(this.multi ? [] : null);\n this.query = '';\n this.open = false;\n this.message = 'Selection cleared.';\n }\n\n search(query: string) {\n if (!this.searchable) return;\n\n this.query = query;\n this.message = '';\n this.highlighted = 0;\n this.show();\n }\n\n /** Open, highlighting the current choice - else the first option, or the last when `last`. */\n show(last = false) {\n if (this.disabled || this.open) return;\n\n const { filtered } = this;\n const current = filtered.findIndex((option) => this.isSelected(option));\n\n this.highlighted = current >= 0 ? current : last ? filtered.length - 1 : 0;\n this.open = true;\n }\n\n hide() {\n this.open = false;\n this.query = '';\n }\n\n toggle() {\n if (this.open) this.hide();\n else this.show();\n }\n\n focus() {\n this.focused = true;\n this.message = this.selection.length\n ? `Selected ${this.selection.map((option) => option.label).join(', ')}.`\n : '';\n }\n\n blur() {\n this.focused = false;\n this.hide();\n }\n\n /**\n * Arrows, Home/End, PageUp/PageDown move the highlight; Enter, Space and Tab choose;\n * Escape closes; Backspace removes the last value.\n *\n * Calls `preventDefault` and `stopPropagation` on keys it consumes. Tab and Backspace pass through.\n */\n keyDown(event: Select.Key) {\n if (this.disabled || event.isComposing) return;\n\n const { open, filtered, highlighted, active } = this;\n const last = filtered.length - 1;\n\n switch (event.key) {\n case 'ArrowDown':\n case 'ArrowUp': {\n const step = event.key === 'ArrowDown' ? 1 : -1;\n\n if (!open) this.show(step < 0);\n else this.highlighted = filtered.length ? (highlighted + step + filtered.length) % filtered.length : 0;\n break;\n }\n\n case 'Home':\n case 'End':\n if (!open) return;\n\n this.highlighted = event.key === 'Home' ? 0 : last;\n break;\n\n case 'PageDown':\n case 'PageUp':\n if (!open) return;\n\n this.highlighted = Math.max(0, Math.min(last, highlighted + (event.key === 'PageDown' ? 5 : -5)));\n break;\n\n case 'Enter':\n if (!active) return;\n\n this.choose(active);\n break;\n\n case 'Escape':\n if (!open) return;\n\n this.hide();\n break;\n\n case ' ':\n if (this.query) return;\n\n if (active) this.choose(active);\n else this.show();\n break;\n\n case 'Backspace':\n if (!this.query && this.canClear) this.remove(this.selection[this.selection.length - 1]);\n return;\n\n case 'Tab':\n if (active && !event.shiftKey) this.choose(active);\n\n this.open = false;\n return;\n\n default:\n return;\n }\n\n event.preventDefault();\n event.stopPropagation();\n }\n\n /** Focus the input and open, from a press anywhere on the control. */\n press(event: MouseEvent) {\n if (event.target !== this.input.current) event.preventDefault();\n\n this.input.current?.focus();\n this.show();\n }\n\n /** Follow scroll and resize while open, keeping the menu at the control; returns the release. Defaults to `window`. */\n protected listen(): () => void {\n const move = () => this.reposition();\n\n window.addEventListener('resize', move);\n window.addEventListener('scroll', move, true);\n\n return () => {\n window.removeEventListener('resize', move);\n window.removeEventListener('scroll', move, true);\n };\n }\n\n /** Where the menu fits by the control, or `undefined` before both mount. Defaults to the DOM's layout. */\n protected measure(): Select.Place | undefined {\n const control = CONTROL.get(this);\n const menu = MENU.get(this);\n\n if (!control || !menu) return;\n\n const anchor = control.getBoundingClientRect();\n const { left, top, side } = place({\n anchor,\n width: anchor.width,\n height: menu.offsetHeight,\n view: { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight }\n });\n\n return { left, top, width: anchor.width, above: side == 'above' };\n }\n\n /** Re-measure and move the menu when its place changed; for a `listen` that follows layout. */\n protected reposition() {\n const next = this.measure();\n\n if (!next) return;\n\n const { place: last } = this;\n const moved = !last || (Object.keys(next) as (keyof Select.Place)[]).some((key) => last[key] != next[key]);\n\n if (moved) this.place = next;\n }\n\n /** Ref for the control, in `controlProps`; the menu anchors to it. */\n protected anchor(element: HTMLElement | null) {\n if (element) CONTROL.set(this, element);\n }\n\n /** Ref for the menu, in `menuProps`; places it once mounted. */\n protected mount(element: HTMLElement | null) {\n if (!element) return;\n\n MENU.set(this, element);\n this.reposition();\n }\n\n /** Ref for the highlighted option, in `optionProps`; keeps it in view. Defaults to `scrollIntoView`. */\n protected reveal(element: HTMLElement | null) {\n element?.scrollIntoView({ block: 'nearest' });\n }\n\n private update(value: Select.Value) {\n this.value = value;\n\n if (this.onChange) this.onChange(value);\n }\n\n render(): Component.Node {\n const { open, multi, query, selection, filtered, canClear } = this;\n const value = !multi && !query && selection[0];\n const empty = filtered.length == 0;\n\n return (\n <this.Root>\n <this.Control>\n <this.Values>\n <>\n {multi && selection.map((option) => <this.Chip key={String(option.value)} option={option} />)}\n {value && <this.Value option={value} />}\n </>\n <this.Input key=\"input\" />\n </this.Values>\n {canClear && <this.Clear />}\n <this.Toggle />\n </this.Control>\n {open && (\n <this.Layer>\n <this.Menu>\n {filtered.map((option, index) => (\n <this.Option key={String(option.value)} option={option} index={index} />\n ))}\n {empty && <this.Empty />}\n </this.Menu>\n </this.Layer>\n )}\n <this.Live />\n </this.Root>\n );\n }\n}\n\nconst hidden = {\n position: 'absolute',\n width: '1px',\n height: '1px',\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap'\n} as const;\n\nconst Live = (): Component.Node => {\n const { announcement } = Select.get();\n\n return (\n <span aria-live=\"polite\" aria-atomic=\"true\" style={hidden}>\n {announcement}\n </span>\n );\n};\n\nfunction hold(event: MouseEvent) {\n event.preventDefault();\n event.stopPropagation();\n}\n\nfunction fold(text: string) {\n return text\n .normalize('NFD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .trim();\n}\n\nexport { Live, Select };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Select as Select$1 } from "./base.js";
|
|
2
|
+
import { Component } from "@expressive/mvc";
|
|
3
|
+
|
|
4
|
+
//#region src/select/select.d.ts
|
|
5
|
+
declare namespace Select {
|
|
6
|
+
type Option<T = unknown> = Select$1.Option<T>;
|
|
7
|
+
type Value = Select$1.Value;
|
|
8
|
+
type Key = Select$1.Key;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Combobox with search, single or multiple selection, keyboard navigation and live announcements.
|
|
12
|
+
*
|
|
13
|
+
* Pieces - `Root`, `Control`, `Values`, `Chip`, `Value`, `Input`, `Clear`, `Toggle`, `Layer`, `Menu`, `Option`,
|
|
14
|
+
* `Empty` - are function components reading `Select.get()`, each styled on its own; pass one as a prop or assign it
|
|
15
|
+
* in a subclass to replace it. Stock pieces are statics. `Layer` portals the menu to the body, so a dialog or a
|
|
16
|
+
* scrolling container does not clip it; the menu sits under the control, flips above when only that fits, and
|
|
17
|
+
* follows scroll and resize.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* <label htmlFor="role">Role</label>
|
|
21
|
+
* <Select id="role" options={roles} value={this.role} onChange={(role) => (this.role = role)} />
|
|
22
|
+
*/
|
|
23
|
+
declare class Select extends Select$1 {
|
|
24
|
+
static Root: ({
|
|
25
|
+
children
|
|
26
|
+
}: {
|
|
27
|
+
children: Component.Node;
|
|
28
|
+
}) => Component.Node;
|
|
29
|
+
static Control: ({
|
|
30
|
+
children
|
|
31
|
+
}: {
|
|
32
|
+
children: Component.Node;
|
|
33
|
+
}) => Component.Node;
|
|
34
|
+
static Values: ({
|
|
35
|
+
children
|
|
36
|
+
}: {
|
|
37
|
+
children: Component.Node;
|
|
38
|
+
}) => Component.Node;
|
|
39
|
+
static Chip: ({
|
|
40
|
+
option
|
|
41
|
+
}: {
|
|
42
|
+
option: Select.Option;
|
|
43
|
+
}) => Component.Node;
|
|
44
|
+
static Value: ({
|
|
45
|
+
option
|
|
46
|
+
}: {
|
|
47
|
+
option: Select.Option;
|
|
48
|
+
}) => Component.Node;
|
|
49
|
+
static Input: () => Component.Node;
|
|
50
|
+
static Clear: () => Component.Node;
|
|
51
|
+
static Toggle: () => Component.Node;
|
|
52
|
+
static Layer: ({
|
|
53
|
+
children
|
|
54
|
+
}: {
|
|
55
|
+
children: Component.Node;
|
|
56
|
+
}) => Component.Node;
|
|
57
|
+
static Menu: ({
|
|
58
|
+
children
|
|
59
|
+
}: {
|
|
60
|
+
children: Component.Node;
|
|
61
|
+
}) => Component.Node;
|
|
62
|
+
static Option: ({
|
|
63
|
+
option,
|
|
64
|
+
index
|
|
65
|
+
}: {
|
|
66
|
+
option: Select.Option;
|
|
67
|
+
index: number;
|
|
68
|
+
}) => Component.Node;
|
|
69
|
+
static Empty: () => Component.Node;
|
|
70
|
+
Root: ({
|
|
71
|
+
children
|
|
72
|
+
}: {
|
|
73
|
+
children: Component.Node;
|
|
74
|
+
}) => Component.Node;
|
|
75
|
+
Control: ({
|
|
76
|
+
children
|
|
77
|
+
}: {
|
|
78
|
+
children: Component.Node;
|
|
79
|
+
}) => Component.Node;
|
|
80
|
+
Values: ({
|
|
81
|
+
children
|
|
82
|
+
}: {
|
|
83
|
+
children: Component.Node;
|
|
84
|
+
}) => Component.Node;
|
|
85
|
+
Chip: ({
|
|
86
|
+
option
|
|
87
|
+
}: {
|
|
88
|
+
option: Select.Option;
|
|
89
|
+
}) => Component.Node;
|
|
90
|
+
Value: ({
|
|
91
|
+
option
|
|
92
|
+
}: {
|
|
93
|
+
option: Select.Option;
|
|
94
|
+
}) => Component.Node;
|
|
95
|
+
Input: () => Component.Node;
|
|
96
|
+
Clear: () => Component.Node;
|
|
97
|
+
Toggle: () => Component.Node;
|
|
98
|
+
Layer: ({
|
|
99
|
+
children
|
|
100
|
+
}: {
|
|
101
|
+
children: Component.Node;
|
|
102
|
+
}) => Component.Node;
|
|
103
|
+
Menu: ({
|
|
104
|
+
children
|
|
105
|
+
}: {
|
|
106
|
+
children: Component.Node;
|
|
107
|
+
}) => Component.Node;
|
|
108
|
+
Option: ({
|
|
109
|
+
option,
|
|
110
|
+
index
|
|
111
|
+
}: {
|
|
112
|
+
option: Select.Option;
|
|
113
|
+
index: number;
|
|
114
|
+
}) => Component.Node;
|
|
115
|
+
Empty: () => Component.Node;
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { Select };
|
|
119
|
+
//# sourceMappingURL=select.d.ts.map
|