@oscarpalmer/oui 0.31.0 → 0.32.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 +1 -5
- package/dist/css/combobox.css +95 -10
- package/dist/js/combobox.mjs +250 -192
- package/dist/js/menu.d.mts +1 -1
- package/dist/js/toolbar.d.mts +1 -1
- package/package.json +2 -2
- package/src/css/combobox.scss +92 -13
- package/src/css/mixins.scss +8 -16
- package/src/js/combobox.ts +487 -332
- package/src/js/menu.ts +1 -1
package/dist/js/combobox.mjs
CHANGED
|
@@ -161,30 +161,8 @@ var OuiComboboxElement = class extends HTMLElement {
|
|
|
161
161
|
this.clear();
|
|
162
162
|
}
|
|
163
163
|
};
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
const filter = state.filter.toLocaleLowerCase();
|
|
167
|
-
const all = options ?? getOptions(state)[0];
|
|
168
|
-
const { length } = all;
|
|
169
|
-
const visible = [];
|
|
170
|
-
for (let index = 0; index < length; index += 1) {
|
|
171
|
-
const option = all[index];
|
|
172
|
-
option.hidden = option.textContent?.toLocaleLowerCase().includes(filter) === false;
|
|
173
|
-
if (!option.hidden) visible.push(option);
|
|
174
|
-
}
|
|
175
|
-
state.empty.hidden = visible.length > 0;
|
|
176
|
-
if (state.empty.hidden) return state.active != null && visible.includes(state.active) ? state.active : visible[0];
|
|
177
|
-
}
|
|
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);
|
|
164
|
+
function createAnchor(type, id, label, select, autocomplete, multiple) {
|
|
165
|
+
const [anchor, selection] = type === "autocomplete" ? [createInput(id, select)] : createBox(id, select, multiple);
|
|
188
166
|
setAria(anchor, {
|
|
189
167
|
autocomplete: type === "autocomplete" ? autocomplete.value : void 0,
|
|
190
168
|
controls: `${id}_content`,
|
|
@@ -194,80 +172,186 @@ function getAnchor(type, id, label, select, autocomplete, multiple) {
|
|
|
194
172
|
});
|
|
195
173
|
setAttribute(anchor, ATTRIBUTE_ANCHOR, "");
|
|
196
174
|
setRole(anchor, "combobox");
|
|
197
|
-
return anchor;
|
|
198
|
-
}
|
|
199
|
-
function getAttributeValue(element, attribute, possible, defaultValue) {
|
|
200
|
-
const value = getAttribute(element, attribute);
|
|
201
|
-
return possible.has(value) ? value : defaultValue;
|
|
202
|
-
}
|
|
203
|
-
function getAutoComplete(element, select) {
|
|
204
|
-
let value;
|
|
205
|
-
if (select) value = "list";
|
|
206
|
-
else value = getAttributeValue(element, "autocomplete", AUTOCOMPLETES, "none");
|
|
207
|
-
return {
|
|
208
|
-
value,
|
|
209
|
-
both: value === "both",
|
|
210
|
-
inline: value === "inline",
|
|
211
|
-
list: value === "list",
|
|
212
|
-
none: value === "none"
|
|
213
|
-
};
|
|
175
|
+
return [anchor, selection];
|
|
214
176
|
}
|
|
215
|
-
function
|
|
177
|
+
function createBox(id, select, multiple) {
|
|
216
178
|
const box = createElement("div", {
|
|
217
179
|
id,
|
|
218
180
|
tabIndex: 0,
|
|
219
181
|
textContent: multiple ? "" : select.selectedOptions[0]?.textContent ?? ""
|
|
220
182
|
});
|
|
183
|
+
let selection;
|
|
221
184
|
if (multiple) {
|
|
222
|
-
|
|
223
|
-
for (const option of select.selectedOptions) selection.append(getSelectionItem(option));
|
|
185
|
+
selection = createSelection(select);
|
|
224
186
|
box.append(selection);
|
|
225
187
|
}
|
|
226
|
-
return box;
|
|
188
|
+
return [box, selection];
|
|
227
189
|
}
|
|
228
|
-
function
|
|
190
|
+
function createContent(type, id, label, select, autocomplete, multiple) {
|
|
229
191
|
const content = createElement("div", { id: `${id}_content` }, {
|
|
230
192
|
[ATTRIBUTE_CONTENT]: "",
|
|
231
193
|
popover: "",
|
|
232
194
|
tabindex: -1,
|
|
233
195
|
"aria-labelledby": label.id
|
|
234
196
|
});
|
|
197
|
+
const [filter, input] = createFilter(type, id) ?? [];
|
|
198
|
+
const [list, headers, selected] = createList(label, select, autocomplete, multiple);
|
|
199
|
+
const empty = createEmpty();
|
|
200
|
+
if (filter != null) content.append(filter);
|
|
201
|
+
content.append(list, empty);
|
|
202
|
+
return [
|
|
203
|
+
content,
|
|
204
|
+
input,
|
|
205
|
+
headers,
|
|
206
|
+
selected,
|
|
207
|
+
empty
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
function createEmpty() {
|
|
211
|
+
return createElement("div", {
|
|
212
|
+
hidden: true,
|
|
213
|
+
role: "status",
|
|
214
|
+
textContent: "No results found"
|
|
215
|
+
}, {
|
|
216
|
+
[ATTRIBUTE_EMPTY]: "",
|
|
217
|
+
"aria-live": "polite"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function createFilter(type, id) {
|
|
221
|
+
if (type === "autocomplete") return;
|
|
222
|
+
const container = createElement("div", {}, { [ATTRIBUTE_FILTER]: "" });
|
|
223
|
+
const label = createElement("label", {
|
|
224
|
+
htmlFor: `${id}_filter`,
|
|
225
|
+
textContent: "Filter options"
|
|
226
|
+
}, { [ATTRIBUTE_FILTER_LABEL]: "" });
|
|
227
|
+
const filter = createElement("input", { id: `${id}_filter` }, { [ATTRIBUTE_FILTER_INPUT]: "" });
|
|
228
|
+
container.append(label, filter);
|
|
229
|
+
return [container, filter];
|
|
230
|
+
}
|
|
231
|
+
function createHeader(group) {
|
|
232
|
+
return createElement("li", {
|
|
233
|
+
id: group.id,
|
|
234
|
+
role: "presentation",
|
|
235
|
+
textContent: group.label
|
|
236
|
+
}, { [ATTRIBUTE_HEADER]: "" });
|
|
237
|
+
}
|
|
238
|
+
function createInput(id, select) {
|
|
239
|
+
return createElement("input", {
|
|
240
|
+
id,
|
|
241
|
+
value: select.selectedOptions[0]?.textContent ?? ""
|
|
242
|
+
}, {
|
|
243
|
+
[ATTRIBUTE_ANCHOR]: "",
|
|
244
|
+
autocomplete: "off",
|
|
245
|
+
type: "text"
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function createList(label, select, autocomplete, multiple) {
|
|
235
249
|
const list = createElement("ul", { role: "listbox" }, {
|
|
236
250
|
[ATTRIBUTE_LIST]: "",
|
|
237
251
|
"aria-labelledby": label.id,
|
|
238
252
|
"aria-multiselectable": multiple
|
|
239
253
|
});
|
|
240
|
-
const
|
|
254
|
+
const groups = [...select.querySelectorAll("optgroup")];
|
|
255
|
+
const groupsLength = groups.length;
|
|
241
256
|
const selected = [];
|
|
257
|
+
let headers;
|
|
258
|
+
if (groupsLength > 0) {
|
|
259
|
+
headers = [];
|
|
260
|
+
let firstGroup;
|
|
261
|
+
for (let groupIndex = 0; groupIndex < groupsLength; groupIndex += 1) {
|
|
262
|
+
const group = groups[groupIndex];
|
|
263
|
+
const options = [...group.querySelectorAll("option")];
|
|
264
|
+
if (options.length === 0) continue;
|
|
265
|
+
firstGroup ??= groupIndex;
|
|
266
|
+
const header = createHeader(group);
|
|
267
|
+
headers.push(header);
|
|
268
|
+
list.append(header);
|
|
269
|
+
createOptions(options, autocomplete, list, selected, groupIndex === firstGroup, header.id);
|
|
270
|
+
}
|
|
271
|
+
} else createOptions([...select.options], autocomplete, list, selected, true);
|
|
272
|
+
return [
|
|
273
|
+
list,
|
|
274
|
+
headers,
|
|
275
|
+
selected
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
function createOption(index, option, autocomplete, active, group) {
|
|
279
|
+
return createElement("li", {
|
|
280
|
+
id: option.id,
|
|
281
|
+
role: "option",
|
|
282
|
+
textContent: option.textContent
|
|
283
|
+
}, {
|
|
284
|
+
group,
|
|
285
|
+
[ATTRIBUTE_OPTION]: "",
|
|
286
|
+
[ATTRIBUTE_OPTION_ACTIVE]: active && index === 0 ? "" : void 0,
|
|
287
|
+
value: option.value,
|
|
288
|
+
"aria-selected": !autocomplete.none && option.selected
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function createOptions(options, autocomplete, list, selected, active, group) {
|
|
242
292
|
for (let index = 0; index < options.length; index += 1) {
|
|
243
293
|
const option = options[index];
|
|
244
|
-
const item =
|
|
245
|
-
id: option.id,
|
|
246
|
-
textContent: option.textContent
|
|
247
|
-
}, {
|
|
248
|
-
[ATTRIBUTE_OPTION]: "",
|
|
249
|
-
[ATTRIBUTE_OPTION_ACTIVE]: index === 0 ? "" : void 0,
|
|
250
|
-
role: "option",
|
|
251
|
-
value: option.value,
|
|
252
|
-
"aria-selected": !autocomplete.none && option.selected
|
|
253
|
-
});
|
|
294
|
+
const item = createOption(index, option, autocomplete, active, group);
|
|
254
295
|
if (option.selected) selected.push(item);
|
|
255
296
|
list.append(item);
|
|
256
297
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
];
|
|
298
|
+
}
|
|
299
|
+
function createSelection(select) {
|
|
300
|
+
const selection = createElement("ul", {}, { [ATTRIBUTE_SELECTION]: "" });
|
|
301
|
+
for (const option of select.selectedOptions) selection.append(getSelectionItem(option));
|
|
302
|
+
return selection;
|
|
303
|
+
}
|
|
304
|
+
function filterOptions(state, options, clear) {
|
|
305
|
+
if (clear || state.autocomplete.none) state.filter.value = "";
|
|
306
|
+
const filter = state.filter.value.toLocaleLowerCase();
|
|
307
|
+
const groups = state.headers?.reduce((record, header) => {
|
|
308
|
+
record[header.id] = 0;
|
|
309
|
+
return record;
|
|
310
|
+
}, {}) ?? {};
|
|
311
|
+
const all = options ?? getOptions(state)[0];
|
|
312
|
+
let { length } = all;
|
|
313
|
+
const visible = [];
|
|
314
|
+
for (let index = 0; index < length; index += 1) {
|
|
315
|
+
const option = all[index];
|
|
316
|
+
option.hidden = option.textContent?.toLocaleLowerCase().includes(filter) === false;
|
|
317
|
+
if (!option.hidden) {
|
|
318
|
+
groups[getAttribute(option, "group") ?? ""] += 1;
|
|
319
|
+
visible.push(option);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (state.headers != null) {
|
|
323
|
+
length = state.headers.length;
|
|
324
|
+
for (let index = 0; index < length; index += 1) {
|
|
325
|
+
const header = state.headers[index];
|
|
326
|
+
header.hidden = groups[header.id] === 0;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
state.empty.hidden = visible.length > 0;
|
|
330
|
+
if (state.empty.hidden) return state.active != null && visible.includes(state.active) ? state.active : visible[0];
|
|
331
|
+
}
|
|
332
|
+
function generateIds(select, prefix) {
|
|
333
|
+
const elements = [...select.querySelectorAll("optgroup"), ...select.options];
|
|
334
|
+
const { length } = elements;
|
|
335
|
+
for (let index = 0; index < length; index += 1) {
|
|
336
|
+
const element = elements[index];
|
|
337
|
+
if (element.id === "") element.id = `${prefix}${element instanceof HTMLOptGroupElement ? "_group_" : "_option_"}${index}`;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function getAttributeValue(element, attribute, possible, defaultValue) {
|
|
341
|
+
const value = getAttribute(element, attribute);
|
|
342
|
+
return possible.has(value) ? value : defaultValue;
|
|
343
|
+
}
|
|
344
|
+
function getAutoComplete(element, select) {
|
|
345
|
+
let value;
|
|
346
|
+
if (select) value = "list";
|
|
347
|
+
else value = getAttributeValue(element, "autocomplete", AUTOCOMPLETES, "none");
|
|
348
|
+
return {
|
|
349
|
+
value,
|
|
350
|
+
both: value === "both",
|
|
351
|
+
inline: value === "inline",
|
|
352
|
+
list: value === "list",
|
|
353
|
+
none: value === "none"
|
|
354
|
+
};
|
|
271
355
|
}
|
|
272
356
|
function getFormValue(state, values) {
|
|
273
357
|
if (values.length === 0 || !state.multiple) return values[0];
|
|
@@ -287,16 +371,6 @@ function getId() {
|
|
|
287
371
|
index += 1;
|
|
288
372
|
return `oui_combobox_${index}`;
|
|
289
373
|
}
|
|
290
|
-
function getInput(id, select) {
|
|
291
|
-
return createElement("input", {
|
|
292
|
-
id,
|
|
293
|
-
value: select.selectedOptions[0]?.textContent ?? ""
|
|
294
|
-
}, {
|
|
295
|
-
[ATTRIBUTE_ANCHOR]: "",
|
|
296
|
-
autocomplete: "off",
|
|
297
|
-
type: "text"
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
374
|
function getOption(state, value) {
|
|
301
375
|
const [all] = getOptions(state);
|
|
302
376
|
const valueAsString = getString(value).toLocaleLowerCase();
|
|
@@ -355,8 +429,8 @@ function getState(element, internals) {
|
|
|
355
429
|
label.htmlFor = id;
|
|
356
430
|
label.id = `${id}_label`;
|
|
357
431
|
generateIds(select, id);
|
|
358
|
-
const anchor =
|
|
359
|
-
const [content, selected, empty] =
|
|
432
|
+
const [anchor, selection] = createAnchor(type, id, label, select, autocomplete, multiple);
|
|
433
|
+
const [content, filter, headers, selected, empty] = createContent(type, id, label, select, autocomplete, multiple);
|
|
360
434
|
const values = selected.length === 0 ? [""] : selected.map((option) => getAttribute(option, "value") ?? "");
|
|
361
435
|
const floatable = createEmbeddedFloatable(anchor, content, {
|
|
362
436
|
position: "below-start",
|
|
@@ -373,6 +447,7 @@ function getState(element, internals) {
|
|
|
373
447
|
disabled,
|
|
374
448
|
empty,
|
|
375
449
|
floatable,
|
|
450
|
+
headers,
|
|
376
451
|
id,
|
|
377
452
|
internals,
|
|
378
453
|
label,
|
|
@@ -381,14 +456,19 @@ function getState(element, internals) {
|
|
|
381
456
|
required,
|
|
382
457
|
select,
|
|
383
458
|
selected,
|
|
459
|
+
selection,
|
|
384
460
|
type,
|
|
385
461
|
values,
|
|
386
|
-
|
|
462
|
+
active: selected[0],
|
|
463
|
+
filter: {
|
|
464
|
+
element: filter,
|
|
465
|
+
value: ""
|
|
466
|
+
},
|
|
387
467
|
listeners: [],
|
|
388
468
|
name: element.name,
|
|
389
469
|
touched: false
|
|
390
470
|
};
|
|
391
|
-
state.listeners.push(on(
|
|
471
|
+
state.listeners.push(on(element, "focusout", () => {
|
|
392
472
|
state.touched = true;
|
|
393
473
|
updateValidity(state);
|
|
394
474
|
}), on(content, "click", (event) => {
|
|
@@ -398,84 +478,21 @@ function getState(element, internals) {
|
|
|
398
478
|
activedescendant: floatable.open ? state.active?.id : void 0,
|
|
399
479
|
expanded: floatable.open
|
|
400
480
|
});
|
|
401
|
-
if (floatable.open)
|
|
481
|
+
if (floatable.open) {
|
|
482
|
+
state.filter.element?.focus();
|
|
483
|
+
updateWidth(state);
|
|
484
|
+
}
|
|
402
485
|
}));
|
|
403
486
|
setDisabled(element, state, disabled);
|
|
487
|
+
setLimit("max", element, state, getAttribute(element, "max"));
|
|
488
|
+
setLimit("min", element, state, getAttribute(element, "min"));
|
|
489
|
+
setReadonly(element, state, readonly);
|
|
490
|
+
setRequired(element, state, required);
|
|
404
491
|
return state;
|
|
405
492
|
}
|
|
406
493
|
function hasTextSelection(element) {
|
|
407
494
|
return element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
|
|
408
495
|
}
|
|
409
|
-
function onAnchor(event, handler) {
|
|
410
|
-
if (event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
411
|
-
const combobox = findAncestor(event, TAGNAME);
|
|
412
|
-
const state = states.get(combobox);
|
|
413
|
-
if (combobox == null || state == null) return;
|
|
414
|
-
if (findAncestor(event, SELECTOR_ANCHOR) != null) handler(event, combobox, state);
|
|
415
|
-
}
|
|
416
|
-
function onAnchorKeydown(event, combobox, state) {
|
|
417
|
-
const isSelect = state.type === "select";
|
|
418
|
-
let preventDefault = true;
|
|
419
|
-
switch (true) {
|
|
420
|
-
case event.key === " " && state.type === "select":
|
|
421
|
-
case event.key === "Enter":
|
|
422
|
-
onEnter(combobox, state);
|
|
423
|
-
break;
|
|
424
|
-
case event.key === "Escape":
|
|
425
|
-
onEscape(state);
|
|
426
|
-
break;
|
|
427
|
-
case isSelect && KEYS_ABSOLUTE.has(event.key):
|
|
428
|
-
onNavigation(event, combobox, state);
|
|
429
|
-
break;
|
|
430
|
-
case !isSelect && KEYS_SELECTION.has(event.key):
|
|
431
|
-
preventDefault = false;
|
|
432
|
-
if (hasTextSelection(state.anchor)) onSelection(combobox, state);
|
|
433
|
-
break;
|
|
434
|
-
case KEYS_NAVIGATION.has(event.key):
|
|
435
|
-
onNavigation(event, combobox, state);
|
|
436
|
-
break;
|
|
437
|
-
default:
|
|
438
|
-
preventDefault = false;
|
|
439
|
-
if (event.key === "Tab") onTab(combobox, state);
|
|
440
|
-
break;
|
|
441
|
-
}
|
|
442
|
-
if (preventDefault) {
|
|
443
|
-
event.preventDefault();
|
|
444
|
-
event.stopPropagation();
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
function onAnchorKeyup(event, combobox, state) {
|
|
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;
|
|
458
|
-
if (typed) state.filter += event.key;
|
|
459
|
-
if (state.anchor instanceof HTMLInputElement && (KEYS_REMOVE.has(event.key) || state.anchor.value.length < state.filter.length)) {
|
|
460
|
-
state.filter = state.anchor.value;
|
|
461
|
-
filterOptions(state);
|
|
462
|
-
}
|
|
463
|
-
switch (true) {
|
|
464
|
-
case typed:
|
|
465
|
-
onTyped(combobox, state);
|
|
466
|
-
break;
|
|
467
|
-
case KEYS_REMOVE.has(event.key):
|
|
468
|
-
onRemove(combobox, state);
|
|
469
|
-
break;
|
|
470
|
-
default:
|
|
471
|
-
preventDefault = false;
|
|
472
|
-
break;
|
|
473
|
-
}
|
|
474
|
-
if (preventDefault) {
|
|
475
|
-
event.preventDefault();
|
|
476
|
-
event.stopPropagation();
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
496
|
function onClick(event) {
|
|
480
497
|
const combobox = findAncestor(event, TAGNAME);
|
|
481
498
|
const state = states.get(combobox);
|
|
@@ -504,7 +521,7 @@ function onContent(event, content) {
|
|
|
504
521
|
if (combobox == null || state == null) return;
|
|
505
522
|
const option = findAncestor(event, SELECTOR_OPTION);
|
|
506
523
|
if (option instanceof HTMLElement) setNext("set", combobox, state, getOptions(state)[0], option);
|
|
507
|
-
state.anchor.focus();
|
|
524
|
+
(state.filter.element ?? state.anchor).focus();
|
|
508
525
|
}
|
|
509
526
|
function onEnter(combobox, state) {
|
|
510
527
|
if (hasTextSelection(state.anchor)) {
|
|
@@ -520,11 +537,58 @@ function onEnter(combobox, state) {
|
|
|
520
537
|
function onEscape(state) {
|
|
521
538
|
state.floatable.hide();
|
|
522
539
|
}
|
|
540
|
+
function onInput(event, handler) {
|
|
541
|
+
if (event.ctrlKey || event.metaKey) return;
|
|
542
|
+
const combobox = findAncestor(event, TAGNAME);
|
|
543
|
+
const state = states.get(combobox);
|
|
544
|
+
if (combobox == null || state == null) return;
|
|
545
|
+
if ((findAncestor(event, SELECTOR_ANCHOR) ?? findAncestor(event, SELECTOR_FILTER)) != null) handler(event, combobox, state);
|
|
546
|
+
}
|
|
547
|
+
function onInputKeydown(event, combobox, state) {
|
|
548
|
+
const isSelect = state.type === "select";
|
|
549
|
+
let preventDefault = true;
|
|
550
|
+
switch (true) {
|
|
551
|
+
case event.key === "Enter":
|
|
552
|
+
onEnter(combobox, state);
|
|
553
|
+
break;
|
|
554
|
+
case event.key === "Escape":
|
|
555
|
+
onEscape(state);
|
|
556
|
+
break;
|
|
557
|
+
case !isSelect && KEYS_SELECTION.has(event.key):
|
|
558
|
+
preventDefault = false;
|
|
559
|
+
if (hasTextSelection(state.anchor)) onSelection(combobox, state);
|
|
560
|
+
break;
|
|
561
|
+
case KEYS_NAVIGATION.has(event.key):
|
|
562
|
+
onNavigation(event, combobox, state);
|
|
563
|
+
break;
|
|
564
|
+
default:
|
|
565
|
+
preventDefault = false;
|
|
566
|
+
if (event.key === "Tab") onTab(combobox, state);
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
if (preventDefault) {
|
|
570
|
+
event.preventDefault();
|
|
571
|
+
event.stopPropagation();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function onInputKeyup(event, combobox, state) {
|
|
575
|
+
const filter = state.filter.element ?? state.anchor;
|
|
576
|
+
const typed = event.key.length === 1 && EXPRESSION_TYPED.test(event.key);
|
|
577
|
+
if (!(filter instanceof HTMLInputElement) || !(typed || KEYS_REMOVE.has(event.key) || filter.value.length < state.filter.value.length)) return;
|
|
578
|
+
event.preventDefault();
|
|
579
|
+
event.stopPropagation();
|
|
580
|
+
if (typed) state.filter.value += event.key;
|
|
581
|
+
else state.filter.value = filter.value;
|
|
582
|
+
const [options] = getOptions(state);
|
|
583
|
+
const next = filterOptions(state, options);
|
|
584
|
+
if (typed) onTyped(combobox, state, options, next);
|
|
585
|
+
else setNext("remove", combobox, state, options, next);
|
|
586
|
+
}
|
|
523
587
|
function onKeydown(event) {
|
|
524
|
-
|
|
588
|
+
onInput(event, onInputKeydown);
|
|
525
589
|
}
|
|
526
590
|
function onKeyup(event) {
|
|
527
|
-
|
|
591
|
+
onInput(event, onInputKeyup);
|
|
528
592
|
}
|
|
529
593
|
function onNavigation(event, combobox, state) {
|
|
530
594
|
let skip = false;
|
|
@@ -560,34 +624,22 @@ function onNavigation(event, combobox, state) {
|
|
|
560
624
|
}
|
|
561
625
|
setNext(state.autocomplete.none ? "none" : "navigation", combobox, state, all, next);
|
|
562
626
|
}
|
|
563
|
-
function onRemove(combobox, state) {
|
|
564
|
-
const [all] = getOptions(state);
|
|
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
627
|
function onSelection(combobox, state) {
|
|
574
|
-
state.filter = state.anchor instanceof HTMLInputElement ? state.anchor.value : "";
|
|
628
|
+
state.filter.value = state.anchor instanceof HTMLInputElement ? state.anchor.value : "";
|
|
575
629
|
filterOptions(state);
|
|
576
630
|
setNext("set", combobox, state, getOptions(state)[0], state.active);
|
|
577
631
|
}
|
|
578
632
|
function onTab(combobox, state) {
|
|
579
633
|
if (combobox !== document.activeElement || !combobox.contains(document.activeElement)) state.floatable.hide();
|
|
580
634
|
}
|
|
581
|
-
function onTyped(combobox, state) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
635
|
+
function onTyped(combobox, state, options, next) {
|
|
636
|
+
const filter = state.filter.element ?? state.anchor;
|
|
637
|
+
if (!state.autocomplete.inline && filter instanceof HTMLInputElement && filter.value.length > 0) state.floatable.show();
|
|
638
|
+
const all = options ?? getOptions(state)[0];
|
|
639
|
+
setNext("typed", combobox, state, all, state.autocomplete.none ? state.active ?? all[0] : next ?? filterOptions(state, options));
|
|
585
640
|
}
|
|
586
641
|
function setDisabled(combobox, state, value) {
|
|
587
|
-
|
|
588
|
-
setAria(state.anchor, "disabled", value);
|
|
589
|
-
setProperty(combobox, "disabled", value);
|
|
590
|
-
if (state.anchor instanceof HTMLInputElement) state.anchor.readOnly = value;
|
|
642
|
+
setProperties("disabled", "disabled", combobox, state, value);
|
|
591
643
|
}
|
|
592
644
|
function setLimit(type, combobox, state, value) {
|
|
593
645
|
if (typeof value === "number" && !Number.isNaN(value) ? value !== state[type] : value == null) {
|
|
@@ -603,16 +655,18 @@ function setNext(update, combobox, state, all, next) {
|
|
|
603
655
|
next?.scrollIntoView(scrollOptions);
|
|
604
656
|
setValue(update, combobox, state, next);
|
|
605
657
|
}
|
|
658
|
+
function setProperties(key, property, combobox, state, value) {
|
|
659
|
+
state[key] = value;
|
|
660
|
+
setAria(state.anchor, key, value);
|
|
661
|
+
setProperty(combobox, key, value);
|
|
662
|
+
if (state.filter.element != null) state.filter.element[property] = value;
|
|
663
|
+
if (state.anchor instanceof HTMLInputElement) state.anchor[property] = value;
|
|
664
|
+
}
|
|
606
665
|
function setReadonly(combobox, state, value) {
|
|
607
|
-
|
|
608
|
-
setAria(state.anchor, "readonly", value);
|
|
609
|
-
setProperty(combobox, "readonly", value);
|
|
610
|
-
if (state.anchor instanceof HTMLInputElement) state.anchor.readOnly = value;
|
|
666
|
+
setProperties("readonly", "readOnly", combobox, state, value);
|
|
611
667
|
}
|
|
612
668
|
function setRequired(combobox, state, value) {
|
|
613
|
-
|
|
614
|
-
setAria(state.anchor, "required", value);
|
|
615
|
-
setProperty(combobox, "required", value);
|
|
669
|
+
setProperties("required", "required", combobox, state, value);
|
|
616
670
|
updateValidity(state);
|
|
617
671
|
}
|
|
618
672
|
function setValue(update, combobox, state, next) {
|
|
@@ -665,11 +719,11 @@ function setValues(combobox, state, value) {
|
|
|
665
719
|
}
|
|
666
720
|
for (const option of state.selected) {
|
|
667
721
|
setAria(option, "selected", false);
|
|
668
|
-
state.
|
|
722
|
+
state.selection?.querySelector(`[option="${option.id}"]`)?.remove();
|
|
669
723
|
}
|
|
670
724
|
for (const option of selected) {
|
|
671
725
|
setAria(option, "selected", true);
|
|
672
|
-
state.
|
|
726
|
+
state.selection?.append(getSelectionItem(option));
|
|
673
727
|
}
|
|
674
728
|
state.selected = selected.slice();
|
|
675
729
|
setNext("navigation", combobox, state, all, next);
|
|
@@ -678,11 +732,11 @@ function setValues(combobox, state, value) {
|
|
|
678
732
|
function updateAutoComplete(update, state, next) {
|
|
679
733
|
if (!(update === "set" || state.autocomplete.both || state.autocomplete.inline)) return;
|
|
680
734
|
const content = next.textContent ?? "";
|
|
681
|
-
if (update === "typed" && !content.toLocaleLowerCase().startsWith(state.filter.toLocaleLowerCase())) return;
|
|
735
|
+
if (update === "typed" && !content.toLocaleLowerCase().startsWith(state.filter.value.toLocaleLowerCase())) return;
|
|
682
736
|
const { length } = content;
|
|
683
737
|
if (state.anchor instanceof HTMLInputElement) {
|
|
684
738
|
state.anchor.value = content;
|
|
685
|
-
state.anchor.setSelectionRange(update === "typed" ? state.filter.length : length, length);
|
|
739
|
+
state.anchor.setSelectionRange(update === "typed" ? state.filter.value.length : length, length);
|
|
686
740
|
} else state.anchor.textContent = content;
|
|
687
741
|
}
|
|
688
742
|
function updateForm(combobox, state, values) {
|
|
@@ -700,8 +754,8 @@ function updateSelect(update, state, next, added) {
|
|
|
700
754
|
state.anchor.textContent = next.textContent ?? "";
|
|
701
755
|
return;
|
|
702
756
|
}
|
|
703
|
-
if (added === true) state.
|
|
704
|
-
else if (added === false) state.
|
|
757
|
+
if (added === true) state.selection?.append(getSelectionItem(next));
|
|
758
|
+
else if (added === false) state.selection?.querySelector(`[option="${next.id}"]`)?.remove();
|
|
705
759
|
}
|
|
706
760
|
function updateSelected(update, state, next) {
|
|
707
761
|
const [all] = getOptions(state);
|
|
@@ -750,6 +804,10 @@ const TAGNAME = "oui-combobox";
|
|
|
750
804
|
const ATTRIBUTE_ANCHOR = `${TAGNAME}-anchor`;
|
|
751
805
|
const ATTRIBUTE_CONTENT = `${TAGNAME}-content`;
|
|
752
806
|
const ATTRIBUTE_EMPTY = `${TAGNAME}-empty`;
|
|
807
|
+
const ATTRIBUTE_FILTER = `${TAGNAME}-filter`;
|
|
808
|
+
const ATTRIBUTE_FILTER_INPUT = `${ATTRIBUTE_FILTER}-input`;
|
|
809
|
+
const ATTRIBUTE_FILTER_LABEL = `${ATTRIBUTE_FILTER}-label`;
|
|
810
|
+
const ATTRIBUTE_HEADER = `${TAGNAME}-header`;
|
|
753
811
|
const ATTRIBUTE_LIST = `${TAGNAME}-list`;
|
|
754
812
|
const ATTRIBUTE_OPTION = `${TAGNAME}-option`;
|
|
755
813
|
const ATTRIBUTE_OPTION_ACTIVE = `${TAGNAME}-option-active`;
|
|
@@ -766,7 +824,6 @@ const INVALID_BAD = "Please select an option from the list";
|
|
|
766
824
|
const INVALID_INPUT = "Please enter a value";
|
|
767
825
|
const INVALID_OVERFLOW = "Please select fewer options (maximum is {max})";
|
|
768
826
|
const INVALID_UNDERFLOW = "Please select more options (minimum is {min})";
|
|
769
|
-
const KEYS_ABSOLUTE = /* @__PURE__ */ new Set(["End", "Home"]);
|
|
770
827
|
const KEYS_NAVIGATION = /* @__PURE__ */ new Set([
|
|
771
828
|
"ArrowDown",
|
|
772
829
|
"ArrowUp",
|
|
@@ -779,6 +836,7 @@ const MESSAGE_LABEL = `<${TAGNAME}> must contain a <label> element for the <sele
|
|
|
779
836
|
const MESSAGE_SELECT = `<${TAGNAME}> must contain a valid <select> element`;
|
|
780
837
|
const PROPERTY_STYLE = "--oui-combobox-width";
|
|
781
838
|
const SELECTOR_ANCHOR = `[${ATTRIBUTE_ANCHOR}]`;
|
|
839
|
+
const SELECTOR_FILTER = `[${ATTRIBUTE_FILTER_INPUT}]`;
|
|
782
840
|
const SELECTOR_OPTION = `[${ATTRIBUTE_OPTION}]`;
|
|
783
841
|
const TYPES = /* @__PURE__ */ new Set(["autocomplete", "select"]);
|
|
784
842
|
const comboboxes = /* @__PURE__ */ new WeakMap();
|
package/dist/js/menu.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|
package/dist/js/toolbar.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oscarpalmer/oui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Styles, components, and utilities for building websites and applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"css",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"postcss-cli": "^11",
|
|
107
107
|
"postcss-input-range": "^6",
|
|
108
108
|
"postcss-scss": "^4",
|
|
109
|
-
"sass": "^1.
|
|
109
|
+
"sass": "^1.102",
|
|
110
110
|
"stylelint": "^17.14",
|
|
111
111
|
"stylelint-config-standard-scss": "^17",
|
|
112
112
|
"tsdown": "^0.22",
|