@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.
@@ -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, setAttributes } from "@oscarpalmer/toretto/attribute";
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.#state?.disabled ?? false;
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 this.getAttribute("name") ?? "";
70
+ return getAttribute(this, "name") ?? "";
71
+ }
72
+ /**
73
+ * Is the combobox readonly?
74
+ */
75
+ get readonly() {
76
+ return this.hasAttribute("readonly");
32
77
  }
33
- set name(value) {
34
- if (typeof value === "string") this.setAttribute("name", value);
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.value;
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, "set", option);
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
- this.disabled = this.hasAttribute("disabled");
64
- break;
144
+ case "readonly":
65
145
  case "required":
66
- this.required = this.hasAttribute("required");
67
- break;
68
146
  case "value":
69
- this.value = value;
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
- if (visible.length > 0) return state.active != null && visible.includes(state.active) ? state.active : visible[0];
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 getAnchor(type, id, element, label, select, autocomplete) {
88
- const anchor = type === "autocomplete" ? getInput(id, element, label, select) : getBox(id, element, label, select);
89
- setAria(anchor, "autocomplete", autocomplete.value);
90
- setAttributes(anchor, {
91
- [ATTRIBUTE_ANCHOR]: "",
92
- "aria-controls": `${id}_content`,
93
- "aria-expanded": false,
94
- "aria-haspopup": "listbox",
95
- "aria-labelledby": label.id,
96
- role: "combobox"
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, element, label, select) {
116
- return createElement("div", {
215
+ function getBox(id, select, multiple) {
216
+ const box = createElement("div", {
117
217
  id,
118
- innerHTML: select.selectedOptions[0]?.textContent ?? "",
119
- tabIndex: 0
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(type, id, element, anchor, label, select, autocomplete) {
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", {}, { [ATTRIBUTE_LIST]: "" });
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: `${id}_option_${index}`,
245
+ id: option.id,
137
246
  textContent: option.textContent
138
247
  }, {
139
248
  [ATTRIBUTE_OPTION]: "",
140
- [ATTRIBUTE_OPTION_CURRENT]: index === 0 ? "" : void 0,
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
- content.append(list);
149
- return [content, selected];
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, element, label, select) {
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 (option.getAttribute("value")?.toLocaleLowerCase() === valueAsString) {
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 getAria(element, "selected") === "true";
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
- if (update === "none") return getAria(element, "selected") === "true";
187
- if (update === "set") return element === next;
188
- return state.autocomplete.both && element === next;
328
+ return selected;
329
+ }
330
+ function getSelectionItem(option) {
331
+ const icon = createElement("span", { innerHTML: "&times;" }, { "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.name = select.name === "" ? select.id : select.name;
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" : getType(element);
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
- const anchor = getAnchor(type, id, element, label, select, autocomplete);
207
- const [content, selected] = getContent(type, id, element, anchor, label, select, autocomplete);
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
- value: ""
388
+ name: element.name,
389
+ touched: false
233
390
  };
234
- state.listeners.push(on(content, "click", (event) => {
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) setStyle(content, "--oui-combobox-width", `${anchor.getBoundingClientRect().width}px`);
401
+ if (floatable.open) updateWidth(state);
242
402
  }));
243
403
  setDisabled(element, state, disabled);
244
404
  return state;
245
405
  }
246
- function getType(element) {
247
- const type = getAttribute(element, ATTRIBUTE_TYPE);
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(combobox, state);
425
+ onEscape(state);
265
426
  break;
266
- case state.type === "select" && KEYS_ABSOLUTE.has(event.key):
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") state.anchor.focus();
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], "set", option);
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.floatable.open) setNext(combobox, state, getOptions(state)[0], "set", state.active);
331
- else state.floatable.show();
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(combobox, state) {
334
- if (!state.floatable.open) return;
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 ?? state.active == null) next = state.active ?? visible[0];
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(combobox, state, all, state.autocomplete.none ? "none" : "navigation", next);
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, "none", filterOptions(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
- function open() {
387
- if (state.anchor instanceof HTMLInputElement && state.anchor.value.length > 0 && (state.autocomplete.both || state.autocomplete.list)) state.floatable.show();
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 setNext(combobox, state, all, update, next) {
403
- for (const option of all) setAttribute(option, ATTRIBUTE_OPTION_CURRENT, option === next ? "" : void 0);
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, update, next);
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 setValue(combobox, state, update, next) {
415
- if (state.disabled) return;
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
- state.select.value = next?.getAttribute("value") ?? "";
419
- if (state.value !== state.select.value) {
420
- state.value = state.select.value;
421
- state.internals.setFormValue(state.value);
422
- dispatch(combobox, "change");
423
- dispatch(state.select, "change");
424
- }
425
- if (next == null || update === "none") return;
426
- if (update === "set" || state.autocomplete.both || state.autocomplete.inline) {
427
- const textContent = next.textContent ?? "";
428
- if (state.anchor instanceof HTMLInputElement) state.anchor.value = textContent;
429
- else state.anchor.innerHTML = textContent;
430
- if (state.anchor instanceof HTMLInputElement) state.anchor.setSelectionRange(update === "typed" ? state.filter.length : textContent.length, textContent.length);
431
- }
432
- if (update === "set") state.floatable.hide();
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 ATTRIBUTE_OPTION_CURRENT = `${TAGNAME}-option-current`;
440
- const ATTRIBUTE_TYPE = "type";
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"]);