@oscarpalmer/oui 0.30.0 → 0.31.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/dist/css/badge.css +4 -4
- package/dist/css/combobox.css +139 -0
- package/dist/css/form.css +18 -14
- package/dist/js/combobox.d.mts +65 -1
- package/dist/js/combobox.mjs +428 -105
- package/dist/js/focus-trap/focus-trap.embedded.mjs +3 -4
- package/dist/js/internal/draggable.mjs +7 -15
- package/dist/js/menu.mjs +5 -2
- package/dist/js/movable/movable.embedded.mjs +6 -10
- package/dist/js/resizable/resizable.embedded.mjs +9 -16
- package/dist/js/sortable/sortable.embedded.mjs +21 -26
- package/package.json +3 -3
- package/src/css/badge.scss +1 -18
- package/src/css/combobox.scss +113 -2
- package/src/css/form.scss +18 -80
- package/src/css/mixins.scss +86 -0
- package/src/js/combobox.ts +714 -152
- package/src/js/focus-trap/focus-trap.embedded.ts +3 -7
- package/src/js/internal/draggable.ts +4 -10
- package/src/js/menu.ts +5 -2
- package/src/js/movable/movable.embedded.ts +3 -6
- package/src/js/resizable/resizable.embedded.ts +6 -19
- package/src/js/sortable/sortable.embedded.ts +17 -14
package/dist/js/combobox.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createEmbeddedFloatable } from "./floatable/floatable.embedded.mjs";
|
|
2
2
|
import { clamp } from "@oscarpalmer/atoms/number";
|
|
3
|
-
import { getString } from "@oscarpalmer/atoms/string";
|
|
4
|
-
import { getAria, setAria } from "@oscarpalmer/toretto/aria";
|
|
5
|
-
import { getAttribute, setAttribute
|
|
3
|
+
import { getString, join } from "@oscarpalmer/atoms/string";
|
|
4
|
+
import { getAria, setAria, setRole } from "@oscarpalmer/toretto/aria";
|
|
5
|
+
import { getAttribute, setAttribute } from "@oscarpalmer/toretto/attribute";
|
|
6
6
|
import { createElement } from "@oscarpalmer/toretto/create";
|
|
7
7
|
import { dispatch, on } from "@oscarpalmer/toretto/event";
|
|
8
8
|
import { findAncestor } from "@oscarpalmer/toretto/find";
|
|
@@ -13,41 +13,122 @@ var OuiComboboxElement = class extends HTMLElement {
|
|
|
13
13
|
static formAssociated = true;
|
|
14
14
|
static observedAttributes = [
|
|
15
15
|
"disabled",
|
|
16
|
+
"max",
|
|
17
|
+
"min",
|
|
18
|
+
"readonly",
|
|
16
19
|
"required",
|
|
17
20
|
"value"
|
|
18
21
|
];
|
|
19
22
|
#internals;
|
|
20
23
|
#state;
|
|
24
|
+
/**
|
|
25
|
+
* Is the combobox disabled?
|
|
26
|
+
*/
|
|
21
27
|
get disabled() {
|
|
22
|
-
return this
|
|
28
|
+
return this.hasAttribute("disabled");
|
|
23
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Should the combobox be disabled?
|
|
32
|
+
*/
|
|
24
33
|
set disabled(value) {
|
|
25
34
|
if (typeof value === "boolean" && value !== this.#state.disabled) setDisabled(this, this.#state, value);
|
|
26
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Maximum number of selected options
|
|
38
|
+
*/
|
|
39
|
+
get max() {
|
|
40
|
+
return this.#state.max;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Set the maximum number of selected options
|
|
44
|
+
*/
|
|
45
|
+
set max(value) {
|
|
46
|
+
setLimit("max", this, this.#state, value);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Minimum number of selected options
|
|
50
|
+
*/
|
|
51
|
+
get min() {
|
|
52
|
+
return this.#state.min;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Set the minimum number of selected options
|
|
56
|
+
*/
|
|
57
|
+
set min(value) {
|
|
58
|
+
setLimit("min", this, this.#state, value);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Does the combobox allow multiple values?
|
|
62
|
+
*/
|
|
27
63
|
get multiple() {
|
|
28
64
|
return this.hasAttribute("multiple");
|
|
29
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Name of the combobox
|
|
68
|
+
*/
|
|
30
69
|
get name() {
|
|
31
|
-
return
|
|
70
|
+
return getAttribute(this, "name") ?? "";
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Is the combobox readonly?
|
|
74
|
+
*/
|
|
75
|
+
get readonly() {
|
|
76
|
+
return this.hasAttribute("readonly");
|
|
32
77
|
}
|
|
33
|
-
|
|
34
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Should the combobox be readonly?
|
|
80
|
+
*/
|
|
81
|
+
set readonly(value) {
|
|
82
|
+
if (typeof value === "boolean" && value !== this.#state.readonly) setReadonly(this, this.#state, value);
|
|
35
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Is the combobox required?
|
|
86
|
+
*/
|
|
36
87
|
get required() {
|
|
37
88
|
return this.hasAttribute("required");
|
|
38
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Should the combobox be required?
|
|
92
|
+
*/
|
|
39
93
|
set required(value) {
|
|
40
94
|
if (typeof value === "boolean" && value !== this.#state.required) setRequired(this, this.#state, value);
|
|
41
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Selected options
|
|
98
|
+
*/
|
|
99
|
+
get selected() {
|
|
100
|
+
return this.#state.selected.slice();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Type of combobox
|
|
104
|
+
*/
|
|
42
105
|
get type() {
|
|
43
106
|
return this.#state.type;
|
|
44
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Current value
|
|
110
|
+
*/
|
|
45
111
|
get value() {
|
|
46
|
-
return this.#state.
|
|
112
|
+
return this.#state.values[0];
|
|
47
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Set the value
|
|
116
|
+
*/
|
|
48
117
|
set value(value) {
|
|
49
118
|
const [option, all] = getOption(this.#state, value);
|
|
50
|
-
if (option != null) setNext(this, this.#state, all,
|
|
119
|
+
if (option != null) setNext("set", this, this.#state, all, option);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Current values
|
|
123
|
+
*/
|
|
124
|
+
get values() {
|
|
125
|
+
return this.#state.values.slice();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Set the values
|
|
129
|
+
*/
|
|
130
|
+
set values(value) {
|
|
131
|
+
setValues(this, this.#state, value);
|
|
51
132
|
}
|
|
52
133
|
constructor() {
|
|
53
134
|
super();
|
|
@@ -60,16 +141,25 @@ var OuiComboboxElement = class extends HTMLElement {
|
|
|
60
141
|
attributeChangedCallback(name, _, value) {
|
|
61
142
|
switch (name) {
|
|
62
143
|
case "disabled":
|
|
63
|
-
|
|
64
|
-
break;
|
|
144
|
+
case "readonly":
|
|
65
145
|
case "required":
|
|
66
|
-
this.required = this.hasAttribute("required");
|
|
67
|
-
break;
|
|
68
146
|
case "value":
|
|
69
|
-
this
|
|
147
|
+
this[name] = this.hasAttribute(name);
|
|
148
|
+
break;
|
|
149
|
+
case "max":
|
|
150
|
+
case "min":
|
|
151
|
+
setLimit(name, this, this.#state, value == null ? void 0 : Number.parseInt(value, 10));
|
|
70
152
|
break;
|
|
153
|
+
default: break;
|
|
71
154
|
}
|
|
72
155
|
}
|
|
156
|
+
clear() {
|
|
157
|
+
setValues(this, this.#state, []);
|
|
158
|
+
}
|
|
159
|
+
formResetCallback() {
|
|
160
|
+
this.#state.touched = false;
|
|
161
|
+
this.clear();
|
|
162
|
+
}
|
|
73
163
|
};
|
|
74
164
|
function filterOptions(state, options, clear) {
|
|
75
165
|
if (clear || state.autocomplete.none) state.filter = "";
|
|
@@ -82,28 +172,38 @@ function filterOptions(state, options, clear) {
|
|
|
82
172
|
option.hidden = option.textContent?.toLocaleLowerCase().includes(filter) === false;
|
|
83
173
|
if (!option.hidden) visible.push(option);
|
|
84
174
|
}
|
|
85
|
-
|
|
175
|
+
state.empty.hidden = visible.length > 0;
|
|
176
|
+
if (state.empty.hidden) return state.active != null && visible.includes(state.active) ? state.active : visible[0];
|
|
86
177
|
}
|
|
87
|
-
function
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
[
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
178
|
+
function generateIds(select, prefix) {
|
|
179
|
+
const options = [...select.options];
|
|
180
|
+
const { length } = options;
|
|
181
|
+
for (let index = 0; index < length; index += 1) {
|
|
182
|
+
const option = options[index];
|
|
183
|
+
if (option.id === "") option.id = `${prefix}_option_${index}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function getAnchor(type, id, label, select, autocomplete, multiple) {
|
|
187
|
+
const anchor = type === "autocomplete" ? getInput(id, select) : getBox(id, select, multiple);
|
|
188
|
+
setAria(anchor, {
|
|
189
|
+
autocomplete: type === "autocomplete" ? autocomplete.value : void 0,
|
|
190
|
+
controls: `${id}_content`,
|
|
191
|
+
expanded: false,
|
|
192
|
+
haspopup: "listbox",
|
|
193
|
+
labelledby: label.id
|
|
97
194
|
});
|
|
195
|
+
setAttribute(anchor, ATTRIBUTE_ANCHOR, "");
|
|
196
|
+
setRole(anchor, "combobox");
|
|
98
197
|
return anchor;
|
|
99
198
|
}
|
|
199
|
+
function getAttributeValue(element, attribute, possible, defaultValue) {
|
|
200
|
+
const value = getAttribute(element, attribute);
|
|
201
|
+
return possible.has(value) ? value : defaultValue;
|
|
202
|
+
}
|
|
100
203
|
function getAutoComplete(element, select) {
|
|
101
204
|
let value;
|
|
102
205
|
if (select) value = "list";
|
|
103
|
-
else
|
|
104
|
-
const attribute = getAttribute(element, "autocomplete");
|
|
105
|
-
value = AUTOCOMPLETES.has(attribute) ? attribute : "none";
|
|
106
|
-
}
|
|
206
|
+
else value = getAttributeValue(element, "autocomplete", AUTOCOMPLETES, "none");
|
|
107
207
|
return {
|
|
108
208
|
value,
|
|
109
209
|
both: value === "both",
|
|
@@ -112,32 +212,41 @@ function getAutoComplete(element, select) {
|
|
|
112
212
|
none: value === "none"
|
|
113
213
|
};
|
|
114
214
|
}
|
|
115
|
-
function getBox(id,
|
|
116
|
-
|
|
215
|
+
function getBox(id, select, multiple) {
|
|
216
|
+
const box = createElement("div", {
|
|
117
217
|
id,
|
|
118
|
-
|
|
119
|
-
|
|
218
|
+
tabIndex: 0,
|
|
219
|
+
textContent: multiple ? "" : select.selectedOptions[0]?.textContent ?? ""
|
|
120
220
|
});
|
|
221
|
+
if (multiple) {
|
|
222
|
+
const selection = createElement("ul", {}, { [ATTRIBUTE_SELECTION]: "" });
|
|
223
|
+
for (const option of select.selectedOptions) selection.append(getSelectionItem(option));
|
|
224
|
+
box.append(selection);
|
|
225
|
+
}
|
|
226
|
+
return box;
|
|
121
227
|
}
|
|
122
|
-
function getContent(
|
|
228
|
+
function getContent(id, label, select, autocomplete, multiple) {
|
|
123
229
|
const content = createElement("div", { id: `${id}_content` }, {
|
|
124
230
|
[ATTRIBUTE_CONTENT]: "",
|
|
125
231
|
popover: "",
|
|
126
|
-
role: "listbox",
|
|
127
232
|
tabindex: -1,
|
|
128
233
|
"aria-labelledby": label.id
|
|
129
234
|
});
|
|
130
|
-
const list = createElement("ul", {
|
|
235
|
+
const list = createElement("ul", { role: "listbox" }, {
|
|
236
|
+
[ATTRIBUTE_LIST]: "",
|
|
237
|
+
"aria-labelledby": label.id,
|
|
238
|
+
"aria-multiselectable": multiple
|
|
239
|
+
});
|
|
131
240
|
const options = [...select.options];
|
|
132
241
|
const selected = [];
|
|
133
242
|
for (let index = 0; index < options.length; index += 1) {
|
|
134
243
|
const option = options[index];
|
|
135
244
|
const item = createElement("li", {
|
|
136
|
-
id:
|
|
245
|
+
id: option.id,
|
|
137
246
|
textContent: option.textContent
|
|
138
247
|
}, {
|
|
139
248
|
[ATTRIBUTE_OPTION]: "",
|
|
140
|
-
[
|
|
249
|
+
[ATTRIBUTE_OPTION_ACTIVE]: index === 0 ? "" : void 0,
|
|
141
250
|
role: "option",
|
|
142
251
|
value: option.value,
|
|
143
252
|
"aria-selected": !autocomplete.none && option.selected
|
|
@@ -145,14 +254,40 @@ function getContent(type, id, element, anchor, label, select, autocomplete) {
|
|
|
145
254
|
if (option.selected) selected.push(item);
|
|
146
255
|
list.append(item);
|
|
147
256
|
}
|
|
148
|
-
|
|
149
|
-
|
|
257
|
+
const empty = createElement("div", {
|
|
258
|
+
hidden: true,
|
|
259
|
+
role: "status",
|
|
260
|
+
textContent: "No results found"
|
|
261
|
+
}, {
|
|
262
|
+
[ATTRIBUTE_EMPTY]: "",
|
|
263
|
+
"aria-live": "polite"
|
|
264
|
+
});
|
|
265
|
+
content.append(list, empty);
|
|
266
|
+
return [
|
|
267
|
+
content,
|
|
268
|
+
selected,
|
|
269
|
+
empty
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
function getFormValue(state, values) {
|
|
273
|
+
if (values.length === 0 || !state.multiple) return values[0];
|
|
274
|
+
let formValues = [];
|
|
275
|
+
for (const value of values) formValues.push(`${state.name}=${value}`);
|
|
276
|
+
return join(formValues, "&");
|
|
277
|
+
}
|
|
278
|
+
function getFormValues(state, added) {
|
|
279
|
+
let values;
|
|
280
|
+
if (state.multiple) values = typeof added === "boolean" ? state.selected.map((option) => getAttribute(option, "value") ?? "") : state.values;
|
|
281
|
+
else values = [getAttribute(state.selected[0], "value") ?? ""];
|
|
282
|
+
const previous = getFormValue(state, state.values);
|
|
283
|
+
const next = getFormValue(state, values);
|
|
284
|
+
return [values, next !== previous];
|
|
150
285
|
}
|
|
151
286
|
function getId() {
|
|
152
287
|
index += 1;
|
|
153
288
|
return `oui_combobox_${index}`;
|
|
154
289
|
}
|
|
155
|
-
function getInput(id,
|
|
290
|
+
function getInput(id, select) {
|
|
156
291
|
return createElement("input", {
|
|
157
292
|
id,
|
|
158
293
|
value: select.selectedOptions[0]?.textContent ?? ""
|
|
@@ -164,10 +299,10 @@ function getInput(id, element, label, select) {
|
|
|
164
299
|
}
|
|
165
300
|
function getOption(state, value) {
|
|
166
301
|
const [all] = getOptions(state);
|
|
167
|
-
const valueAsString = getString(value);
|
|
302
|
+
const valueAsString = getString(value).toLocaleLowerCase();
|
|
168
303
|
filterOptions(state, all, true);
|
|
169
304
|
let match;
|
|
170
|
-
for (const option of all) if (
|
|
305
|
+
for (const option of all) if (getAttribute(option, "value")?.toLocaleLowerCase() === valueAsString) {
|
|
171
306
|
match = option;
|
|
172
307
|
break;
|
|
173
308
|
}
|
|
@@ -179,32 +314,50 @@ function getOptions(state) {
|
|
|
179
314
|
}
|
|
180
315
|
function getSelected(update, state, element, next) {
|
|
181
316
|
if (state.autocomplete.none) return false;
|
|
317
|
+
const selected = getAria(element, "selected") === "true";
|
|
182
318
|
if (update === "navigation") {
|
|
183
319
|
if (state.autocomplete.both) return element === next;
|
|
184
|
-
return
|
|
320
|
+
return selected;
|
|
321
|
+
}
|
|
322
|
+
if (update === "none") return selected;
|
|
323
|
+
if (update === "set") {
|
|
324
|
+
if (next == null) return selected;
|
|
325
|
+
if (state.multiple) return element === next ? !selected : selected;
|
|
326
|
+
return element === next;
|
|
185
327
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
328
|
+
return selected;
|
|
329
|
+
}
|
|
330
|
+
function getSelectionItem(option) {
|
|
331
|
+
const icon = createElement("span", { innerHTML: "×" }, { "aria-hidden": true });
|
|
332
|
+
const label = createElement("span", { textContent: option.textContent });
|
|
333
|
+
const item = createElement("li", {}, {
|
|
334
|
+
[ATTRIBUTE_SELECTION_ITEM]: "",
|
|
335
|
+
option: option.id
|
|
336
|
+
});
|
|
337
|
+
item.append(icon, label);
|
|
338
|
+
return item;
|
|
189
339
|
}
|
|
190
340
|
function getState(element, internals) {
|
|
191
341
|
const select = element.querySelector("select");
|
|
192
|
-
if (select == null) throw new Error(MESSAGE_SELECT);
|
|
342
|
+
if (select == null || select.id === "") throw new Error(MESSAGE_SELECT);
|
|
193
343
|
const label = element.querySelector(`label[for="${select.id}"]`);
|
|
194
344
|
if (label == null) throw new Error(MESSAGE_LABEL);
|
|
195
345
|
select.hidden = true;
|
|
196
346
|
element.id = select.id;
|
|
197
|
-
element
|
|
347
|
+
setAttribute(element, "name", select.name === "" ? select.id : select.name);
|
|
198
348
|
const disabled = element.disabled || select.disabled;
|
|
199
349
|
const multiple = element.multiple || select.multiple;
|
|
350
|
+
const readonly = element.readonly;
|
|
200
351
|
const required = element.required || select.required;
|
|
201
352
|
const id = getId();
|
|
202
|
-
const type = multiple ? "select" :
|
|
353
|
+
const type = multiple ? "select" : getAttributeValue(element, "type", TYPES, "select");
|
|
203
354
|
const autocomplete = getAutoComplete(element, type === "select");
|
|
204
355
|
label.htmlFor = id;
|
|
205
356
|
label.id = `${id}_label`;
|
|
206
|
-
|
|
207
|
-
const
|
|
357
|
+
generateIds(select, id);
|
|
358
|
+
const anchor = getAnchor(type, id, label, select, autocomplete, multiple);
|
|
359
|
+
const [content, selected, empty] = getContent(id, label, select, autocomplete, multiple);
|
|
360
|
+
const values = selected.length === 0 ? [""] : selected.map((option) => getAttribute(option, "value") ?? "");
|
|
208
361
|
const floatable = createEmbeddedFloatable(anchor, content, {
|
|
209
362
|
position: "below-start",
|
|
210
363
|
reusable: false
|
|
@@ -218,34 +371,40 @@ function getState(element, internals) {
|
|
|
218
371
|
autocomplete,
|
|
219
372
|
content,
|
|
220
373
|
disabled,
|
|
374
|
+
empty,
|
|
221
375
|
floatable,
|
|
222
376
|
id,
|
|
223
377
|
internals,
|
|
224
378
|
label,
|
|
225
379
|
multiple,
|
|
380
|
+
readonly,
|
|
226
381
|
required,
|
|
227
382
|
select,
|
|
228
383
|
selected,
|
|
229
384
|
type,
|
|
385
|
+
values,
|
|
230
386
|
filter: "",
|
|
231
387
|
listeners: [],
|
|
232
|
-
|
|
388
|
+
name: element.name,
|
|
389
|
+
touched: false
|
|
233
390
|
};
|
|
234
|
-
state.listeners.push(on(
|
|
391
|
+
state.listeners.push(on(anchor, "blur", () => {
|
|
392
|
+
state.touched = true;
|
|
393
|
+
updateValidity(state);
|
|
394
|
+
}), on(content, "click", (event) => {
|
|
235
395
|
onContent(event, content);
|
|
236
396
|
}), on(content, "toggle", () => {
|
|
237
397
|
setAria(anchor, {
|
|
238
398
|
activedescendant: floatable.open ? state.active?.id : void 0,
|
|
239
399
|
expanded: floatable.open
|
|
240
400
|
});
|
|
241
|
-
if (floatable.open)
|
|
401
|
+
if (floatable.open) updateWidth(state);
|
|
242
402
|
}));
|
|
243
403
|
setDisabled(element, state, disabled);
|
|
244
404
|
return state;
|
|
245
405
|
}
|
|
246
|
-
function
|
|
247
|
-
|
|
248
|
-
return TYPES.has(type) ? type : "select";
|
|
406
|
+
function hasTextSelection(element) {
|
|
407
|
+
return element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
|
|
249
408
|
}
|
|
250
409
|
function onAnchor(event, handler) {
|
|
251
410
|
if (event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
@@ -255,17 +414,23 @@ function onAnchor(event, handler) {
|
|
|
255
414
|
if (findAncestor(event, SELECTOR_ANCHOR) != null) handler(event, combobox, state);
|
|
256
415
|
}
|
|
257
416
|
function onAnchorKeydown(event, combobox, state) {
|
|
417
|
+
const isSelect = state.type === "select";
|
|
258
418
|
let preventDefault = true;
|
|
259
419
|
switch (true) {
|
|
420
|
+
case event.key === " " && state.type === "select":
|
|
260
421
|
case event.key === "Enter":
|
|
261
422
|
onEnter(combobox, state);
|
|
262
423
|
break;
|
|
263
424
|
case event.key === "Escape":
|
|
264
|
-
onEscape(
|
|
425
|
+
onEscape(state);
|
|
265
426
|
break;
|
|
266
|
-
case
|
|
427
|
+
case isSelect && KEYS_ABSOLUTE.has(event.key):
|
|
267
428
|
onNavigation(event, combobox, state);
|
|
268
429
|
break;
|
|
430
|
+
case !isSelect && KEYS_SELECTION.has(event.key):
|
|
431
|
+
preventDefault = false;
|
|
432
|
+
if (hasTextSelection(state.anchor)) onSelection(combobox, state);
|
|
433
|
+
break;
|
|
269
434
|
case KEYS_NAVIGATION.has(event.key):
|
|
270
435
|
onNavigation(event, combobox, state);
|
|
271
436
|
break;
|
|
@@ -280,9 +445,16 @@ function onAnchorKeydown(event, combobox, state) {
|
|
|
280
445
|
}
|
|
281
446
|
}
|
|
282
447
|
function onAnchorKeyup(event, combobox, state) {
|
|
283
|
-
if (state.type === "select") return;
|
|
284
|
-
let preventDefault = true;
|
|
285
448
|
const typed = event.key.length === 1 && EXPRESSION_TYPED.test(event.key);
|
|
449
|
+
if (state.type === "select") {
|
|
450
|
+
if (typed) {
|
|
451
|
+
event.preventDefault();
|
|
452
|
+
event.stopPropagation();
|
|
453
|
+
onScrollTo(event, combobox, state);
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
let preventDefault = true;
|
|
286
458
|
if (typed) state.filter += event.key;
|
|
287
459
|
if (state.anchor instanceof HTMLInputElement && (KEYS_REMOVE.has(event.key) || state.anchor.value.length < state.filter.length)) {
|
|
288
460
|
state.filter = state.anchor.value;
|
|
@@ -310,7 +482,15 @@ function onClick(event) {
|
|
|
310
482
|
if (combobox == null || state == null) return;
|
|
311
483
|
const label = findAncestor(event, "label");
|
|
312
484
|
if (label != null) {
|
|
313
|
-
if (label === state.label && combobox.type === "select")
|
|
485
|
+
if (label === state.label && combobox.type === "select") {
|
|
486
|
+
state.anchor.focus();
|
|
487
|
+
state.floatable.show();
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const selection = findAncestor(event, `[${ATTRIBUTE_SELECTION_ITEM}]`);
|
|
492
|
+
if (selection != null) {
|
|
493
|
+
setValue("set", combobox, state, state.selected.find((option) => option.id === getAttribute(selection, "option")));
|
|
314
494
|
return;
|
|
315
495
|
}
|
|
316
496
|
if (findAncestor(event, SELECTOR_ANCHOR) != null) {
|
|
@@ -323,17 +503,22 @@ function onContent(event, content) {
|
|
|
323
503
|
const state = states.get(content);
|
|
324
504
|
if (combobox == null || state == null) return;
|
|
325
505
|
const option = findAncestor(event, SELECTOR_OPTION);
|
|
326
|
-
if (option instanceof HTMLElement) setNext(combobox, state, getOptions(state)[0],
|
|
506
|
+
if (option instanceof HTMLElement) setNext("set", combobox, state, getOptions(state)[0], option);
|
|
327
507
|
state.anchor.focus();
|
|
328
508
|
}
|
|
329
509
|
function onEnter(combobox, state) {
|
|
330
|
-
if (state.
|
|
331
|
-
|
|
510
|
+
if (hasTextSelection(state.anchor)) {
|
|
511
|
+
onSelection(combobox, state);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (!state.floatable.open) {
|
|
515
|
+
state.floatable.show();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
setNext("set", combobox, state, getOptions(state)[0], state.active);
|
|
332
519
|
}
|
|
333
|
-
function onEscape(
|
|
334
|
-
|
|
335
|
-
if (state.type === "select") state.floatable.hide();
|
|
336
|
-
else setNext(combobox, state, getOptions(state)[0], "set", state.active);
|
|
520
|
+
function onEscape(state) {
|
|
521
|
+
state.floatable.hide();
|
|
337
522
|
}
|
|
338
523
|
function onKeydown(event) {
|
|
339
524
|
onAnchor(event, onAnchorKeydown);
|
|
@@ -350,7 +535,7 @@ function onNavigation(event, combobox, state) {
|
|
|
350
535
|
const [all, visible] = getOptions(state);
|
|
351
536
|
if (visible.length === 0) return;
|
|
352
537
|
let next;
|
|
353
|
-
if (skip
|
|
538
|
+
if (skip || state.active == null) next = state.active ?? visible.at(event.key === "ArrowDown" ? 0 : -1);
|
|
354
539
|
else {
|
|
355
540
|
const current = visible.findIndex((option) => option.id === state.active?.id);
|
|
356
541
|
let index;
|
|
@@ -373,25 +558,30 @@ function onNavigation(event, combobox, state) {
|
|
|
373
558
|
}
|
|
374
559
|
if (index != null) next = visible[index];
|
|
375
560
|
}
|
|
376
|
-
setNext(
|
|
561
|
+
setNext(state.autocomplete.none ? "none" : "navigation", combobox, state, all, next);
|
|
377
562
|
}
|
|
378
563
|
function onRemove(combobox, state) {
|
|
379
564
|
const [all] = getOptions(state);
|
|
380
|
-
setNext(combobox, state, all,
|
|
565
|
+
setNext("remove", combobox, state, all, filterOptions(state, all));
|
|
566
|
+
}
|
|
567
|
+
function onScrollTo(event, combobox, state) {
|
|
568
|
+
const [all] = getOptions(state);
|
|
569
|
+
const key = event.key.toLocaleLowerCase();
|
|
570
|
+
const next = all.find((option) => option.textContent?.toLocaleLowerCase().startsWith(key) === true) ?? all.find((option) => option.textContent?.toLocaleLowerCase().includes(key) === true);
|
|
571
|
+
if (next != null) setNext("navigation", combobox, state, all, next);
|
|
572
|
+
}
|
|
573
|
+
function onSelection(combobox, state) {
|
|
574
|
+
state.filter = state.anchor instanceof HTMLInputElement ? state.anchor.value : "";
|
|
575
|
+
filterOptions(state);
|
|
576
|
+
setNext("set", combobox, state, getOptions(state)[0], state.active);
|
|
381
577
|
}
|
|
382
578
|
function onTab(combobox, state) {
|
|
383
579
|
if (combobox !== document.activeElement || !combobox.contains(document.activeElement)) state.floatable.hide();
|
|
384
580
|
}
|
|
385
581
|
function onTyped(combobox, state) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (!state.autocomplete.none) {
|
|
390
|
-
const next = filterOptions(state);
|
|
391
|
-
state.active = next != null && next.textContent?.toLocaleLowerCase().indexOf(state.filter.toLocaleLowerCase()) === 0 ? next : void 0;
|
|
392
|
-
if (next != null) open();
|
|
393
|
-
setNext(combobox, state, getOptions(state)[0], "typed", next);
|
|
394
|
-
} else open();
|
|
582
|
+
if (!state.autocomplete.inline && state.anchor instanceof HTMLInputElement && state.anchor.value.length > 0) state.floatable.show();
|
|
583
|
+
const options = getOptions(state)[0];
|
|
584
|
+
setNext("typed", combobox, state, options, state.autocomplete.none ? state.active ?? options[0] : filterOptions(state));
|
|
395
585
|
}
|
|
396
586
|
function setDisabled(combobox, state, value) {
|
|
397
587
|
state.disabled = value;
|
|
@@ -399,45 +589,172 @@ function setDisabled(combobox, state, value) {
|
|
|
399
589
|
setProperty(combobox, "disabled", value);
|
|
400
590
|
if (state.anchor instanceof HTMLInputElement) state.anchor.readOnly = value;
|
|
401
591
|
}
|
|
402
|
-
function
|
|
403
|
-
|
|
592
|
+
function setLimit(type, combobox, state, value) {
|
|
593
|
+
if (typeof value === "number" && !Number.isNaN(value) ? value !== state[type] : value == null) {
|
|
594
|
+
state[type] = value;
|
|
595
|
+
setAttribute(combobox, type, value);
|
|
596
|
+
updateValidity(state);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function setNext(update, combobox, state, all, next) {
|
|
600
|
+
for (const option of all) setAttribute(option, ATTRIBUTE_OPTION_ACTIVE, option === next ? "" : void 0);
|
|
404
601
|
state.active = next;
|
|
405
602
|
setAria(state.anchor, "activedescendant", next?.id);
|
|
406
603
|
next?.scrollIntoView(scrollOptions);
|
|
407
|
-
setValue(combobox, state,
|
|
604
|
+
setValue(update, combobox, state, next);
|
|
605
|
+
}
|
|
606
|
+
function setReadonly(combobox, state, value) {
|
|
607
|
+
state.readonly = value;
|
|
608
|
+
setAria(state.anchor, "readonly", value);
|
|
609
|
+
setProperty(combobox, "readonly", value);
|
|
610
|
+
if (state.anchor instanceof HTMLInputElement) state.anchor.readOnly = value;
|
|
408
611
|
}
|
|
409
612
|
function setRequired(combobox, state, value) {
|
|
410
613
|
state.required = value;
|
|
411
614
|
setAria(state.anchor, "required", value);
|
|
412
615
|
setProperty(combobox, "required", value);
|
|
616
|
+
updateValidity(state);
|
|
617
|
+
}
|
|
618
|
+
function setValue(update, combobox, state, next) {
|
|
619
|
+
if (state.disabled || state.readonly || state.type === "select" && update !== "set") return;
|
|
620
|
+
let added;
|
|
621
|
+
let shouldUpdate;
|
|
622
|
+
let values;
|
|
623
|
+
if (update === "set" || !(state.anchor instanceof HTMLInputElement)) {
|
|
624
|
+
added = updateSelected(update, state, next);
|
|
625
|
+
[values, shouldUpdate] = getFormValues(state, added);
|
|
626
|
+
} else {
|
|
627
|
+
const [all] = getOptions(state);
|
|
628
|
+
for (const option of all) setAria(option, "selected", false);
|
|
629
|
+
const input = state.anchor.value;
|
|
630
|
+
const content = next?.textContent ?? "";
|
|
631
|
+
const value = next == null ? "" : getAttribute(next, "value") ?? "";
|
|
632
|
+
let selected;
|
|
633
|
+
if (content.toLocaleLowerCase() === input.toLocaleLowerCase()) {
|
|
634
|
+
values = [value];
|
|
635
|
+
if (next != null) {
|
|
636
|
+
selected = next;
|
|
637
|
+
setAria(next, "selected", true);
|
|
638
|
+
}
|
|
639
|
+
} else values = [state.autocomplete.none ? input : ""];
|
|
640
|
+
state.selected = selected == null ? [] : [selected];
|
|
641
|
+
shouldUpdate = state.values[0] !== values[0];
|
|
642
|
+
}
|
|
643
|
+
if (shouldUpdate) updateForm(combobox, state, values);
|
|
644
|
+
if (next == null) return;
|
|
645
|
+
if (state.type === "select") updateSelect(update, state, next, added);
|
|
646
|
+
else if (update !== "remove") updateAutoComplete(update, state, next);
|
|
647
|
+
if (update === "set" && !state.multiple) state.floatable.hide();
|
|
648
|
+
setTimeout(() => {
|
|
649
|
+
updateWidth(state);
|
|
650
|
+
});
|
|
413
651
|
}
|
|
414
|
-
function
|
|
415
|
-
if (
|
|
652
|
+
function setValues(combobox, state, value) {
|
|
653
|
+
if (!Array.isArray(value)) return;
|
|
654
|
+
const strings = (state.multiple ? value : [value]).map((value) => getString(value).toLocaleLowerCase());
|
|
655
|
+
const [all] = getOptions(state);
|
|
656
|
+
const selected = [];
|
|
657
|
+
const values = [];
|
|
658
|
+
let next;
|
|
659
|
+
for (const option of all) {
|
|
660
|
+
const value = getAttribute(option, "value");
|
|
661
|
+
if (value == null || !strings.includes(value.toLocaleLowerCase())) continue;
|
|
662
|
+
selected.push(option);
|
|
663
|
+
values.push(value);
|
|
664
|
+
next ??= option;
|
|
665
|
+
}
|
|
666
|
+
for (const option of state.selected) {
|
|
667
|
+
setAria(option, "selected", false);
|
|
668
|
+
state.anchor.querySelector(`[${ATTRIBUTE_SELECTION_ITEM}][option="${option.id}"]`)?.remove();
|
|
669
|
+
}
|
|
670
|
+
for (const option of selected) {
|
|
671
|
+
setAria(option, "selected", true);
|
|
672
|
+
state.anchor.querySelector(`[${ATTRIBUTE_SELECTION}]`)?.append(getSelectionItem(option));
|
|
673
|
+
}
|
|
674
|
+
state.selected = selected.slice();
|
|
675
|
+
setNext("navigation", combobox, state, all, next);
|
|
676
|
+
updateForm(combobox, state, values);
|
|
677
|
+
}
|
|
678
|
+
function updateAutoComplete(update, state, next) {
|
|
679
|
+
if (!(update === "set" || state.autocomplete.both || state.autocomplete.inline)) return;
|
|
680
|
+
const content = next.textContent ?? "";
|
|
681
|
+
if (update === "typed" && !content.toLocaleLowerCase().startsWith(state.filter.toLocaleLowerCase())) return;
|
|
682
|
+
const { length } = content;
|
|
683
|
+
if (state.anchor instanceof HTMLInputElement) {
|
|
684
|
+
state.anchor.value = content;
|
|
685
|
+
state.anchor.setSelectionRange(update === "typed" ? state.filter.length : length, length);
|
|
686
|
+
} else state.anchor.textContent = content;
|
|
687
|
+
}
|
|
688
|
+
function updateForm(combobox, state, values) {
|
|
689
|
+
const formValues = values.length === 0 ? [""] : values;
|
|
690
|
+
const formData = new FormData();
|
|
691
|
+
for (const value of formValues) formData.append(state.name, value);
|
|
692
|
+
state.values = formValues;
|
|
693
|
+
state.internals.setFormValue(formData);
|
|
694
|
+
updateValidity(state);
|
|
695
|
+
dispatch(combobox, "change");
|
|
696
|
+
}
|
|
697
|
+
function updateSelect(update, state, next, added) {
|
|
698
|
+
if (update !== "set") return;
|
|
699
|
+
if (!state.multiple) {
|
|
700
|
+
state.anchor.textContent = next.textContent ?? "";
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (added === true) state.anchor.querySelector(`[${ATTRIBUTE_SELECTION}]`)?.append(getSelectionItem(next));
|
|
704
|
+
else if (added === false) state.anchor.querySelector(`[${ATTRIBUTE_SELECTION_ITEM}][option="${next.id}"]`)?.remove();
|
|
705
|
+
}
|
|
706
|
+
function updateSelected(update, state, next) {
|
|
416
707
|
const [all] = getOptions(state);
|
|
417
708
|
for (const option of all) setAria(option, "selected", getSelected(update, state, option, next));
|
|
418
|
-
|
|
419
|
-
if (state.
|
|
420
|
-
state.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
709
|
+
if (next == null) return;
|
|
710
|
+
if (state.type !== "select") {
|
|
711
|
+
state.selected = [next];
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (update !== "set") return;
|
|
715
|
+
if (!state.multiple) {
|
|
716
|
+
state.selected = [next];
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const index = state.selected.indexOf(next);
|
|
720
|
+
const added = index === -1;
|
|
721
|
+
if (added) state.selected.push(next);
|
|
722
|
+
else state.selected.splice(index, 1);
|
|
723
|
+
return added;
|
|
724
|
+
}
|
|
725
|
+
function updateValidity(state) {
|
|
726
|
+
if (state.autocomplete.none) {
|
|
727
|
+
const valueMissing = state.required && state.values[0].length === 0;
|
|
728
|
+
state.internals.setValidity({ valueMissing }, INVALID_INPUT);
|
|
729
|
+
state.internals.checkValidity();
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const rangeOverflow = state.multiple && typeof state.max === "number" && state.selected.length > state.max;
|
|
733
|
+
const rangeUnderflow = state.multiple && typeof state.min === "number" && state.selected.length < state.min;
|
|
734
|
+
const badInput = state.required && state.selected.length === 0;
|
|
735
|
+
state.internals.setValidity({
|
|
736
|
+
badInput,
|
|
737
|
+
rangeOverflow,
|
|
738
|
+
rangeUnderflow
|
|
739
|
+
}, join([
|
|
740
|
+
badInput ? INVALID_BAD : void 0,
|
|
741
|
+
rangeOverflow ? getString(INVALID_OVERFLOW).replace("{max}", state.max.toString()) : void 0,
|
|
742
|
+
rangeUnderflow ? getString(INVALID_UNDERFLOW).replace("{min}", state.min.toString()) : void 0
|
|
743
|
+
], "; "));
|
|
744
|
+
state.internals.checkValidity();
|
|
745
|
+
}
|
|
746
|
+
function updateWidth(state) {
|
|
747
|
+
setStyle(state.content, PROPERTY_STYLE, `${state.anchor.getBoundingClientRect().width}px`);
|
|
433
748
|
}
|
|
434
749
|
const TAGNAME = "oui-combobox";
|
|
435
750
|
const ATTRIBUTE_ANCHOR = `${TAGNAME}-anchor`;
|
|
436
751
|
const ATTRIBUTE_CONTENT = `${TAGNAME}-content`;
|
|
752
|
+
const ATTRIBUTE_EMPTY = `${TAGNAME}-empty`;
|
|
437
753
|
const ATTRIBUTE_LIST = `${TAGNAME}-list`;
|
|
438
754
|
const ATTRIBUTE_OPTION = `${TAGNAME}-option`;
|
|
439
|
-
const
|
|
440
|
-
const
|
|
755
|
+
const ATTRIBUTE_OPTION_ACTIVE = `${TAGNAME}-option-active`;
|
|
756
|
+
const ATTRIBUTE_SELECTION = `${TAGNAME}-selection`;
|
|
757
|
+
const ATTRIBUTE_SELECTION_ITEM = `${TAGNAME}-selection-item`;
|
|
441
758
|
const AUTOCOMPLETES = /* @__PURE__ */ new Set([
|
|
442
759
|
"both",
|
|
443
760
|
"inline",
|
|
@@ -445,6 +762,10 @@ const AUTOCOMPLETES = /* @__PURE__ */ new Set([
|
|
|
445
762
|
"none"
|
|
446
763
|
]);
|
|
447
764
|
const EXPRESSION_TYPED = /\p{L}|\s/u;
|
|
765
|
+
const INVALID_BAD = "Please select an option from the list";
|
|
766
|
+
const INVALID_INPUT = "Please enter a value";
|
|
767
|
+
const INVALID_OVERFLOW = "Please select fewer options (maximum is {max})";
|
|
768
|
+
const INVALID_UNDERFLOW = "Please select more options (minimum is {min})";
|
|
448
769
|
const KEYS_ABSOLUTE = /* @__PURE__ */ new Set(["End", "Home"]);
|
|
449
770
|
const KEYS_NAVIGATION = /* @__PURE__ */ new Set([
|
|
450
771
|
"ArrowDown",
|
|
@@ -453,8 +774,10 @@ const KEYS_NAVIGATION = /* @__PURE__ */ new Set([
|
|
|
453
774
|
"PageUp"
|
|
454
775
|
]);
|
|
455
776
|
const KEYS_REMOVE = /* @__PURE__ */ new Set(["Backspace", "Delete"]);
|
|
777
|
+
const KEYS_SELECTION = /* @__PURE__ */ new Set(["ArrowRight", "End"]);
|
|
456
778
|
const MESSAGE_LABEL = `<${TAGNAME}> must contain a <label> element for the <select> element`;
|
|
457
|
-
const MESSAGE_SELECT = `<${TAGNAME}> must contain a <select> element`;
|
|
779
|
+
const MESSAGE_SELECT = `<${TAGNAME}> must contain a valid <select> element`;
|
|
780
|
+
const PROPERTY_STYLE = "--oui-combobox-width";
|
|
458
781
|
const SELECTOR_ANCHOR = `[${ATTRIBUTE_ANCHOR}]`;
|
|
459
782
|
const SELECTOR_OPTION = `[${ATTRIBUTE_OPTION}]`;
|
|
460
783
|
const TYPES = /* @__PURE__ */ new Set(["autocomplete", "select"]);
|