@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.
@@ -161,30 +161,8 @@ var OuiComboboxElement = class extends HTMLElement {
161
161
  this.clear();
162
162
  }
163
163
  };
164
- function filterOptions(state, options, clear) {
165
- if (clear || state.autocomplete.none) state.filter = "";
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 getBox(id, select, multiple) {
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
- const selection = createElement("ul", {}, { [ATTRIBUTE_SELECTION]: "" });
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 getContent(id, label, select, autocomplete, multiple) {
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 options = [...select.options];
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 = createElement("li", {
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
- 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
- ];
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 = getAnchor(type, id, label, select, autocomplete, multiple);
359
- const [content, selected, empty] = getContent(id, label, select, autocomplete, multiple);
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
- filter: "",
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(anchor, "blur", () => {
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) updateWidth(state);
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
- onAnchor(event, onAnchorKeydown);
588
+ onInput(event, onInputKeydown);
525
589
  }
526
590
  function onKeyup(event) {
527
- onAnchor(event, onAnchorKeyup);
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
- 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));
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
- state.disabled = value;
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
- state.readonly = value;
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
- state.required = value;
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.anchor.querySelector(`[${ATTRIBUTE_SELECTION_ITEM}][option="${option.id}"]`)?.remove();
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.anchor.querySelector(`[${ATTRIBUTE_SELECTION}]`)?.append(getSelectionItem(option));
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.anchor.querySelector(`[${ATTRIBUTE_SELECTION}]`)?.append(getSelectionItem(next));
704
- else if (added === false) state.anchor.querySelector(`[${ATTRIBUTE_SELECTION_ITEM}][option="${next.id}"]`)?.remove();
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();
@@ -1 +1 @@
1
- export { };
1
+ export {}
@@ -1 +1 @@
1
- export { };
1
+ export {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/oui",
3
- "version": "0.31.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.101",
109
+ "sass": "^1.102",
110
110
  "stylelint": "^17.14",
111
111
  "stylelint-config-standard-scss": "^17",
112
112
  "tsdown": "^0.22",