@oscarpalmer/oui 0.29.1 → 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.
Files changed (51) hide show
  1. package/dist/css/badge.css +4 -4
  2. package/dist/css/combobox.css +165 -0
  3. package/dist/css/form.css +18 -14
  4. package/dist/js/combobox.d.mts +91 -0
  5. package/dist/js/combobox.mjs +797 -0
  6. package/dist/js/dialog.d.mts +4 -3
  7. package/dist/js/dialog.mjs +1 -1
  8. package/dist/js/{draggable-fIYXd8eO.d.mts → draggable-a6w0J1OD.d.mts} +1 -2
  9. package/dist/js/floatable/floatable.embedded.d.mts +1 -1
  10. package/dist/js/floatable/floatable.embedded.mjs +2 -2
  11. package/dist/js/floatable/floatable.standalone.d.mts +1 -1
  12. package/dist/js/{floatable.embedded-DUR8bQBK.d.mts → floatable.embedded-Bct4U5Dm.d.mts} +0 -1
  13. package/dist/js/focus-trap/focus-trap.embedded.mjs +13 -9
  14. package/dist/js/index.d.mts +9 -4
  15. package/dist/js/index.mjs +2 -1
  16. package/dist/js/internal/draggable.d.mts +1 -1
  17. package/dist/js/internal/draggable.mjs +22 -21
  18. package/dist/js/internal/navigable.d.mts +0 -1
  19. package/dist/js/internal/navigable.mjs +4 -3
  20. package/dist/js/menu.mjs +5 -2
  21. package/dist/js/movable/movable.embedded.d.mts +1 -1
  22. package/dist/js/movable/movable.embedded.mjs +7 -12
  23. package/dist/js/movable/movable.standalone.d.mts +1 -1
  24. package/dist/js/popover.d.mts +1 -2
  25. package/dist/js/popover.mjs +1 -1
  26. package/dist/js/resizable/resizable.embedded.d.mts +1 -1
  27. package/dist/js/resizable/resizable.embedded.mjs +41 -20
  28. package/dist/js/resizable/resizable.standalone.d.mts +1 -1
  29. package/dist/js/{resizable.embedded-CXrUVPzR.d.mts → resizable.embedded-3qoR0Bjm.d.mts} +0 -1
  30. package/dist/js/sortable/sortable.embedded.d.mts +1 -1
  31. package/dist/js/sortable/sortable.embedded.mjs +28 -37
  32. package/dist/js/sortable/sortable.standalone.d.mts +1 -1
  33. package/dist/js/splitter.d.mts +2 -2
  34. package/dist/js/splitter.mjs +11 -11
  35. package/dist/js/tooltip.d.mts +1 -2
  36. package/dist/js/tooltip.mjs +3 -3
  37. package/package.json +7 -3
  38. package/src/css/badge.scss +1 -18
  39. package/src/css/combobox.scss +137 -0
  40. package/src/css/form.scss +18 -80
  41. package/src/css/mixins.scss +86 -0
  42. package/src/js/combobox.ts +1479 -0
  43. package/src/js/focus-trap/focus-trap.embedded.ts +19 -11
  44. package/src/js/index.ts +1 -0
  45. package/src/js/internal/draggable.ts +33 -31
  46. package/src/js/internal/navigable.ts +5 -1
  47. package/src/js/menu.ts +5 -2
  48. package/src/js/movable/movable.embedded.ts +3 -8
  49. package/src/js/resizable/resizable.embedded.ts +57 -20
  50. package/src/js/sortable/sortable.embedded.ts +21 -26
  51. package/src/js/splitter.ts +37 -23
@@ -0,0 +1,1479 @@
1
+ import {clamp} from '@oscarpalmer/atoms/number';
2
+ import {getString, join} from '@oscarpalmer/atoms/string';
3
+ import {getAria, setAria, setRole} from '@oscarpalmer/toretto/aria';
4
+ import {getAttribute, setAttribute} from '@oscarpalmer/toretto/attribute';
5
+ import {createElement} from '@oscarpalmer/toretto/create';
6
+ import {dispatch, on} from '@oscarpalmer/toretto/event';
7
+ import {findAncestor} from '@oscarpalmer/toretto/find';
8
+ import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
9
+ import {setProperty} from '@oscarpalmer/toretto/property';
10
+ import {setStyle} from '@oscarpalmer/toretto/style';
11
+ import {createEmbeddedFloatable, type OuiFloatable} from './floatable/floatable.embedded';
12
+
13
+ // #region Types
14
+
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ [TAGNAME]: OuiComboboxElement;
18
+ }
19
+ }
20
+
21
+ export class OuiComboboxElement extends HTMLElement {
22
+ static formAssociated = true;
23
+
24
+ static observedAttributes = ['disabled', 'max', 'min', 'readonly', 'required', 'value'];
25
+
26
+ readonly #internals: ElementInternals;
27
+
28
+ readonly #state: OuiComboboxState;
29
+
30
+ /**
31
+ * Is the combobox disabled?
32
+ */
33
+ get disabled(): boolean {
34
+ return this.hasAttribute('disabled');
35
+ }
36
+
37
+ /**
38
+ * Should the combobox be disabled?
39
+ */
40
+ set disabled(value: boolean) {
41
+ if (typeof value === 'boolean' && value !== this.#state.disabled) {
42
+ setDisabled(this, this.#state, value);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Maximum number of selected options
48
+ */
49
+ get max(): number | undefined {
50
+ return this.#state.max;
51
+ }
52
+
53
+ /**
54
+ * Set the maximum number of selected options
55
+ */
56
+ set max(value: number) {
57
+ setLimit('max', this, this.#state, value);
58
+ }
59
+
60
+ /**
61
+ * Minimum number of selected options
62
+ */
63
+ get min(): number | undefined {
64
+ return this.#state.min;
65
+ }
66
+
67
+ /**
68
+ * Set the minimum number of selected options
69
+ */
70
+ set min(value: number) {
71
+ setLimit('min', this, this.#state, value);
72
+ }
73
+
74
+ /**
75
+ * Does the combobox allow multiple values?
76
+ */
77
+ get multiple(): boolean {
78
+ return this.hasAttribute('multiple');
79
+ }
80
+
81
+ /**
82
+ * Name of the combobox
83
+ */
84
+ get name(): string {
85
+ return getAttribute(this, 'name') ?? '';
86
+ }
87
+
88
+ /**
89
+ * Is the combobox readonly?
90
+ */
91
+ get readonly(): boolean {
92
+ return this.hasAttribute('readonly');
93
+ }
94
+
95
+ /**
96
+ * Should the combobox be readonly?
97
+ */
98
+ set readonly(value: boolean) {
99
+ if (typeof value === 'boolean' && value !== this.#state.readonly) {
100
+ setReadonly(this, this.#state, value);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Is the combobox required?
106
+ */
107
+ get required(): boolean {
108
+ return this.hasAttribute('required');
109
+ }
110
+
111
+ /**
112
+ * Should the combobox be required?
113
+ */
114
+ set required(value: boolean) {
115
+ if (typeof value === 'boolean' && value !== this.#state.required) {
116
+ setRequired(this, this.#state, value);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Selected options
122
+ */
123
+ get selected(): HTMLElement[] {
124
+ return this.#state.selected.slice();
125
+ }
126
+
127
+ /**
128
+ * Type of combobox
129
+ */
130
+ get type(): OuiComboboxType {
131
+ return this.#state.type;
132
+ }
133
+
134
+ /**
135
+ * Current value
136
+ */
137
+ get value(): string {
138
+ return this.#state.values[0];
139
+ }
140
+
141
+ /**
142
+ * Set the value
143
+ */
144
+ set value(value: unknown) {
145
+ const [option, all] = getOption(this.#state, value);
146
+
147
+ if (option != null) {
148
+ setNext('set', this, this.#state, all, option);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Current values
154
+ */
155
+ get values(): string[] {
156
+ return this.#state.values.slice();
157
+ }
158
+
159
+ /**
160
+ * Set the values
161
+ */
162
+ set values(value: unknown[]) {
163
+ setValues(this, this.#state, value);
164
+ }
165
+
166
+ constructor() {
167
+ super();
168
+
169
+ this.#internals = this.attachInternals();
170
+ this.#state = getState(this, this.#internals);
171
+
172
+ comboboxes.set(this.#state.content, this);
173
+
174
+ states.set(this, this.#state);
175
+ states.set(this.#state.content, this.#state);
176
+ }
177
+
178
+ attributeChangedCallback(name: string, _: string | null, value: string | null): void {
179
+ switch (name) {
180
+ case 'disabled':
181
+ case 'readonly':
182
+ case 'required':
183
+ case 'value':
184
+ this[name] = this.hasAttribute(name);
185
+ break;
186
+
187
+ case 'max':
188
+ case 'min':
189
+ setLimit(name, this, this.#state, value == null ? undefined : Number.parseInt(value, 10));
190
+ break;
191
+
192
+ default:
193
+ break;
194
+ }
195
+ }
196
+
197
+ clear(): void {
198
+ setValues(this, this.#state, []);
199
+ }
200
+
201
+ formResetCallback(): void {
202
+ this.#state.touched = false;
203
+
204
+ this.clear();
205
+ }
206
+ }
207
+
208
+ type OuiComboboxAutoComplete = Record<OuiComboboxAutoCompleteType, boolean> & {
209
+ value: OuiComboboxAutoCompleteType;
210
+ };
211
+
212
+ type OuiComboboxAutoCompleteType = 'both' | 'inline' | 'list' | 'none';
213
+
214
+ type OuiComboboxState = {
215
+ active?: HTMLElement;
216
+ anchor: HTMLElement;
217
+ autocomplete: OuiComboboxAutoComplete;
218
+ content: HTMLElement;
219
+ disabled: boolean;
220
+ empty: HTMLElement;
221
+ filter: string;
222
+ floatable: OuiFloatable;
223
+ id: string;
224
+ internals: ElementInternals;
225
+ label: HTMLLabelElement;
226
+ listeners: RemovableEventListener[];
227
+ max?: number;
228
+ min?: number;
229
+ name: string;
230
+ multiple: boolean;
231
+ readonly: boolean;
232
+ required: boolean;
233
+ select: HTMLSelectElement;
234
+ selected: HTMLElement[];
235
+ touched: boolean;
236
+ type: OuiComboboxType;
237
+ values: string[];
238
+ };
239
+
240
+ type OuiComboboxType = 'autocomplete' | 'select';
241
+
242
+ type OuiComboboxUpdate = 'navigation' | 'none' | 'remove' | 'set' | 'typed';
243
+
244
+ // #endregion
245
+
246
+ // #region Functions
247
+
248
+ function filterOptions(
249
+ state: OuiComboboxState,
250
+ options?: HTMLElement[],
251
+ clear?: boolean,
252
+ ): HTMLElement | undefined {
253
+ if (clear || state.autocomplete.none) {
254
+ state.filter = '';
255
+ }
256
+
257
+ const filter = state.filter.toLocaleLowerCase();
258
+
259
+ const all = options ?? getOptions(state)[0];
260
+ const {length} = all;
261
+
262
+ const visible: HTMLElement[] = [];
263
+
264
+ for (let index = 0; index < length; index += 1) {
265
+ const option = all[index];
266
+
267
+ option.hidden = option.textContent?.toLocaleLowerCase().includes(filter) === false;
268
+
269
+ if (!option.hidden) {
270
+ visible.push(option);
271
+ }
272
+ }
273
+
274
+ state.empty.hidden = visible.length > 0;
275
+
276
+ if (state.empty.hidden) {
277
+ return state.active != null && visible.includes(state.active) ? state.active : visible[0];
278
+ }
279
+ }
280
+
281
+ function generateIds(select: HTMLSelectElement, prefix: string): void {
282
+ const options = [...select.options];
283
+ const {length} = options;
284
+
285
+ for (let index = 0; index < length; index += 1) {
286
+ const option = options[index];
287
+
288
+ if (option.id === '') {
289
+ option.id = `${prefix}_option_${index}`;
290
+ }
291
+ }
292
+ }
293
+
294
+ function getAnchor(
295
+ type: OuiComboboxType,
296
+ id: string,
297
+ label: HTMLLabelElement,
298
+ select: HTMLSelectElement,
299
+ autocomplete: OuiComboboxAutoComplete,
300
+ multiple: boolean,
301
+ ): HTMLElement {
302
+ const anchor = type === 'autocomplete' ? getInput(id, select) : getBox(id, select, multiple);
303
+
304
+ setAria(anchor, {
305
+ autocomplete: type === 'autocomplete' ? autocomplete.value : undefined,
306
+ controls: `${id}_content`,
307
+ expanded: false,
308
+ haspopup: 'listbox',
309
+ labelledby: label.id,
310
+ });
311
+
312
+ setAttribute(anchor, ATTRIBUTE_ANCHOR, '');
313
+
314
+ setRole(anchor, 'combobox');
315
+
316
+ return anchor;
317
+ }
318
+
319
+ function getAttributeValue<Value>(
320
+ element: HTMLElement,
321
+ attribute: string,
322
+ possible: Set<Value>,
323
+ defaultValue: Value,
324
+ ): Value {
325
+ const value = getAttribute(element, attribute);
326
+
327
+ return possible.has(value as Value) ? (value as Value) : defaultValue;
328
+ }
329
+
330
+ function getAutoComplete(element: OuiComboboxElement, select: boolean): OuiComboboxAutoComplete {
331
+ let value: OuiComboboxAutoCompleteType;
332
+
333
+ if (select) {
334
+ value = 'list';
335
+ } else {
336
+ value = getAttributeValue(element, 'autocomplete', AUTOCOMPLETES, 'none');
337
+ }
338
+
339
+ return {
340
+ value,
341
+ both: value === 'both',
342
+ inline: value === 'inline',
343
+ list: value === 'list',
344
+ none: value === 'none',
345
+ };
346
+ }
347
+
348
+ function getBox(id: string, select: HTMLSelectElement, multiple: boolean): HTMLElement {
349
+ const box = createElement('div', {
350
+ id,
351
+ tabIndex: 0,
352
+ textContent: multiple ? '' : (select.selectedOptions[0]?.textContent ?? ''),
353
+ });
354
+
355
+ if (multiple) {
356
+ const selection = createElement(
357
+ 'ul',
358
+ {
359
+ // tabIndex: 0, TODO: handle navigation
360
+ },
361
+ {
362
+ [ATTRIBUTE_SELECTION]: '',
363
+ },
364
+ );
365
+
366
+ for (const option of select.selectedOptions) {
367
+ selection.append(getSelectionItem(option));
368
+ }
369
+
370
+ box.append(selection);
371
+ }
372
+
373
+ return box;
374
+ }
375
+
376
+ function getContent(
377
+ id: string,
378
+ label: HTMLLabelElement,
379
+ select: HTMLSelectElement,
380
+ autocomplete: OuiComboboxAutoComplete,
381
+ multiple: boolean,
382
+ ): [HTMLElement, HTMLElement[], HTMLElement] {
383
+ const content = createElement(
384
+ 'div',
385
+ {
386
+ id: `${id}_content`,
387
+ },
388
+ {
389
+ [ATTRIBUTE_CONTENT]: '',
390
+ popover: '',
391
+ tabindex: -1,
392
+ 'aria-labelledby': label.id,
393
+ },
394
+ );
395
+
396
+ const list = createElement(
397
+ 'ul',
398
+ {
399
+ role: 'listbox',
400
+ },
401
+ {
402
+ [ATTRIBUTE_LIST]: '',
403
+ 'aria-labelledby': label.id,
404
+ 'aria-multiselectable': multiple,
405
+ },
406
+ );
407
+
408
+ const options = [...select.options];
409
+
410
+ const selected: HTMLElement[] = [];
411
+
412
+ for (let index = 0; index < options.length; index += 1) {
413
+ const option = options[index];
414
+
415
+ const item = createElement(
416
+ 'li',
417
+ {
418
+ id: option.id,
419
+ textContent: option.textContent,
420
+ },
421
+ {
422
+ [ATTRIBUTE_OPTION]: '',
423
+ [ATTRIBUTE_OPTION_ACTIVE]: index === 0 ? '' : undefined,
424
+ role: 'option',
425
+ value: option.value,
426
+ 'aria-selected': !autocomplete.none && option.selected,
427
+ },
428
+ );
429
+
430
+ if (option.selected) {
431
+ selected.push(item);
432
+ }
433
+
434
+ list.append(item);
435
+ }
436
+
437
+ const empty = createElement(
438
+ 'div',
439
+ {
440
+ hidden: true,
441
+ role: 'status',
442
+ textContent: 'No results found',
443
+ },
444
+ {
445
+ [ATTRIBUTE_EMPTY]: '',
446
+ 'aria-live': 'polite',
447
+ },
448
+ );
449
+
450
+ content.append(list, empty);
451
+
452
+ return [content, selected, empty];
453
+ }
454
+
455
+ function getFormValue(state: OuiComboboxState, values: string[]): string {
456
+ if (values.length === 0 || !state.multiple) {
457
+ return values[0];
458
+ }
459
+
460
+ let formValues: string[] = [];
461
+
462
+ for (const value of values) {
463
+ formValues.push(`${state.name}=${value}`);
464
+ }
465
+
466
+ return join(formValues, '&');
467
+ }
468
+
469
+ function getFormValues(state: OuiComboboxState, added?: boolean): [string[], boolean] {
470
+ let values: string[];
471
+
472
+ if (state.multiple) {
473
+ values =
474
+ typeof added === 'boolean'
475
+ ? state.selected.map(option => getAttribute(option, 'value') ?? '')
476
+ : state.values;
477
+ } else {
478
+ values = [getAttribute(state.selected[0], 'value') ?? ''];
479
+ }
480
+
481
+ const previous = getFormValue(state, state.values);
482
+ const next = getFormValue(state, values);
483
+
484
+ return [values, next !== previous];
485
+ }
486
+
487
+ function getId(): string {
488
+ index += 1;
489
+
490
+ return `oui_combobox_${index}`;
491
+ }
492
+
493
+ function getInput(id: string, select: HTMLSelectElement): HTMLInputElement {
494
+ return createElement(
495
+ 'input',
496
+ {
497
+ id,
498
+ value: select.selectedOptions[0]?.textContent ?? '',
499
+ },
500
+ {
501
+ [ATTRIBUTE_ANCHOR]: '',
502
+ autocomplete: 'off',
503
+ type: 'text',
504
+ },
505
+ );
506
+ }
507
+
508
+ function getOption(
509
+ state: OuiComboboxState,
510
+ value: unknown,
511
+ ): [HTMLElement | undefined, HTMLElement[]] {
512
+ const [all] = getOptions(state);
513
+ const valueAsString = getString(value).toLocaleLowerCase();
514
+
515
+ filterOptions(state, all, true);
516
+
517
+ let match: HTMLElement | undefined;
518
+
519
+ for (const option of all) {
520
+ if (getAttribute(option, 'value')?.toLocaleLowerCase() === valueAsString) {
521
+ match = option;
522
+
523
+ break;
524
+ }
525
+ }
526
+
527
+ return [match, all];
528
+ }
529
+
530
+ function getOptions(state: OuiComboboxState): HTMLElement[][] {
531
+ const all = [...state.content.querySelectorAll(`[${ATTRIBUTE_OPTION}]`)].filter(
532
+ option => option instanceof HTMLElement,
533
+ );
534
+
535
+ const visible = all.filter(option => !option.hidden);
536
+
537
+ return [all, visible];
538
+ }
539
+
540
+ function getSelected(
541
+ update: OuiComboboxUpdate,
542
+ state: OuiComboboxState,
543
+ element: HTMLElement,
544
+ next?: HTMLElement,
545
+ ): boolean {
546
+ if (state.autocomplete.none) {
547
+ return false;
548
+ }
549
+
550
+ const selected = getAria(element, 'selected') === 'true';
551
+
552
+ if (update === 'navigation') {
553
+ if (state.autocomplete.both) {
554
+ return element === next;
555
+ }
556
+
557
+ return selected;
558
+ }
559
+
560
+ if (update === 'none') {
561
+ return selected;
562
+ }
563
+
564
+ if (update === 'set') {
565
+ if (next == null) {
566
+ return selected;
567
+ }
568
+
569
+ if (state.multiple) {
570
+ return element === next ? !selected : selected;
571
+ }
572
+
573
+ return element === next;
574
+ }
575
+
576
+ return selected;
577
+ }
578
+
579
+ function getSelectionItem(option: HTMLElement): HTMLElement {
580
+ const icon = createElement(
581
+ 'span',
582
+ {
583
+ innerHTML: '&times;',
584
+ },
585
+ {
586
+ 'aria-hidden': true,
587
+ },
588
+ );
589
+
590
+ const label = createElement('span', {
591
+ textContent: option.textContent,
592
+ });
593
+
594
+ const item = createElement(
595
+ 'li',
596
+ {},
597
+ {
598
+ [ATTRIBUTE_SELECTION_ITEM]: '',
599
+ option: option.id,
600
+ },
601
+ );
602
+
603
+ item.append(icon, label);
604
+
605
+ return item;
606
+ }
607
+
608
+ function getState(element: OuiComboboxElement, internals: ElementInternals): OuiComboboxState {
609
+ const select = element.querySelector('select');
610
+
611
+ if (select == null || select.id === '') {
612
+ throw new Error(MESSAGE_SELECT);
613
+ }
614
+
615
+ const label = element.querySelector(`label[for="${select.id}"]`) as HTMLLabelElement;
616
+
617
+ if (label == null) {
618
+ throw new Error(MESSAGE_LABEL);
619
+ }
620
+
621
+ select.hidden = true;
622
+
623
+ element.id = select.id;
624
+
625
+ setAttribute(element, 'name', select.name === '' ? select.id : select.name);
626
+
627
+ const disabled = element.disabled || select.disabled;
628
+ const multiple = element.multiple || select.multiple;
629
+ const readonly = element.readonly;
630
+ const required = element.required || select.required;
631
+
632
+ const id = getId();
633
+ const type = multiple ? 'select' : getAttributeValue(element, 'type', TYPES, 'select');
634
+
635
+ const autocomplete = getAutoComplete(element, type === 'select');
636
+
637
+ label.htmlFor = id;
638
+ label.id = `${id}_label`;
639
+
640
+ generateIds(select, id);
641
+
642
+ const anchor = getAnchor(type, id, label, select, autocomplete, multiple);
643
+
644
+ const [content, selected, empty] = getContent(id, label, select, autocomplete, multiple);
645
+
646
+ const values =
647
+ selected.length === 0 ? [''] : selected.map(option => getAttribute(option, 'value') ?? '');
648
+
649
+ const floatable = createEmbeddedFloatable(anchor, content, {
650
+ position: 'below-start',
651
+ reusable: false,
652
+ });
653
+
654
+ floatable.update();
655
+
656
+ select.insertAdjacentElement('afterend', anchor);
657
+ anchor.insertAdjacentElement('afterend', content);
658
+
659
+ select.remove();
660
+
661
+ const state: OuiComboboxState = {
662
+ anchor,
663
+ autocomplete,
664
+ content,
665
+ disabled,
666
+ empty,
667
+ floatable,
668
+ id,
669
+ internals,
670
+ label,
671
+ multiple,
672
+ readonly,
673
+ required,
674
+ select,
675
+ selected,
676
+ type,
677
+ values,
678
+ filter: '',
679
+ listeners: [],
680
+ name: element.name,
681
+ touched: false,
682
+ };
683
+
684
+ state.listeners.push(
685
+ on(anchor, 'blur', () => {
686
+ state.touched = true;
687
+
688
+ updateValidity(state);
689
+ }),
690
+ on(content, 'click', event => {
691
+ onContent(event, content);
692
+ }),
693
+ on(content, 'toggle', () => {
694
+ setAria(anchor, {
695
+ activedescendant: floatable.open ? state.active?.id : undefined,
696
+ expanded: floatable.open,
697
+ });
698
+
699
+ if (floatable.open) {
700
+ updateWidth(state);
701
+ }
702
+ }),
703
+ );
704
+
705
+ setDisabled(element, state, disabled);
706
+
707
+ return state;
708
+ }
709
+
710
+ function hasTextSelection(element: HTMLElement): element is HTMLInputElement {
711
+ return element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
712
+ }
713
+
714
+ function onAnchor(
715
+ event: KeyboardEvent,
716
+ handler: (event: KeyboardEvent, combobox: OuiComboboxElement, state: OuiComboboxState) => void,
717
+ ): void {
718
+ if (event.ctrlKey || event.metaKey || event.shiftKey) {
719
+ return;
720
+ }
721
+
722
+ const combobox = findAncestor(event, TAGNAME);
723
+ const state = states.get(combobox!);
724
+
725
+ if (combobox == null || state == null) {
726
+ return;
727
+ }
728
+
729
+ const anchor = findAncestor(event, SELECTOR_ANCHOR);
730
+
731
+ if (anchor != null) {
732
+ handler(event, combobox, state);
733
+ }
734
+ }
735
+
736
+ function onAnchorKeydown(
737
+ event: KeyboardEvent,
738
+ combobox: OuiComboboxElement,
739
+ state: OuiComboboxState,
740
+ ): void {
741
+ const isSelect = state.type === 'select';
742
+
743
+ let preventDefault = true;
744
+
745
+ switch (true) {
746
+ case event.key === ' ' && state.type === 'select':
747
+ case event.key === 'Enter':
748
+ onEnter(combobox, state);
749
+ break;
750
+
751
+ case event.key === 'Escape':
752
+ onEscape(state);
753
+ break;
754
+
755
+ case isSelect && KEYS_ABSOLUTE.has(event.key):
756
+ onNavigation(event, combobox, state);
757
+ break;
758
+
759
+ case !isSelect && KEYS_SELECTION.has(event.key): {
760
+ preventDefault = false;
761
+
762
+ if (hasTextSelection(state.anchor)) {
763
+ onSelection(combobox, state);
764
+ }
765
+
766
+ break;
767
+ }
768
+
769
+ case KEYS_NAVIGATION.has(event.key):
770
+ onNavigation(event, combobox, state);
771
+ break;
772
+
773
+ default: {
774
+ preventDefault = false;
775
+
776
+ if (event.key === 'Tab') {
777
+ onTab(combobox, state);
778
+ }
779
+
780
+ break;
781
+ }
782
+ }
783
+
784
+ if (preventDefault) {
785
+ event.preventDefault();
786
+ event.stopPropagation();
787
+ }
788
+ }
789
+
790
+ function onAnchorKeyup(
791
+ event: KeyboardEvent,
792
+ combobox: OuiComboboxElement,
793
+ state: OuiComboboxState,
794
+ ): void {
795
+ const typed = event.key.length === 1 && EXPRESSION_TYPED.test(event.key);
796
+
797
+ if (state.type === 'select') {
798
+ if (typed) {
799
+ event.preventDefault();
800
+ event.stopPropagation();
801
+
802
+ onScrollTo(event, combobox, state);
803
+ }
804
+
805
+ return;
806
+ }
807
+
808
+ let preventDefault = true;
809
+
810
+ if (typed) {
811
+ state.filter += event.key;
812
+ }
813
+
814
+ if (
815
+ state.anchor instanceof HTMLInputElement &&
816
+ (KEYS_REMOVE.has(event.key) || state.anchor.value.length < state.filter.length)
817
+ ) {
818
+ state.filter = state.anchor.value;
819
+
820
+ filterOptions(state);
821
+ }
822
+
823
+ switch (true) {
824
+ case typed:
825
+ onTyped(combobox, state);
826
+ break;
827
+
828
+ case KEYS_REMOVE.has(event.key):
829
+ onRemove(combobox, state);
830
+ break;
831
+
832
+ default: {
833
+ preventDefault = false;
834
+
835
+ break;
836
+ }
837
+ }
838
+
839
+ if (preventDefault) {
840
+ event.preventDefault();
841
+ event.stopPropagation();
842
+ }
843
+ }
844
+
845
+ function onClick(event: PointerEvent): void {
846
+ const combobox = findAncestor(event, TAGNAME);
847
+ const state = states.get(combobox!);
848
+
849
+ if (combobox == null || state == null) {
850
+ return;
851
+ }
852
+
853
+ const label = findAncestor(event, 'label');
854
+
855
+ if (label != null) {
856
+ if (label === state.label && combobox.type === 'select') {
857
+ state.anchor.focus();
858
+
859
+ state.floatable.show();
860
+ }
861
+
862
+ return;
863
+ }
864
+
865
+ const selection = findAncestor(event, `[${ATTRIBUTE_SELECTION_ITEM}]`);
866
+
867
+ if (selection != null) {
868
+ const option = state.selected.find(option => option.id === getAttribute(selection, 'option'));
869
+
870
+ setValue('set', combobox, state, option);
871
+
872
+ return;
873
+ }
874
+
875
+ const anchor = findAncestor(event, SELECTOR_ANCHOR);
876
+
877
+ if (anchor != null) {
878
+ state.floatable.show();
879
+
880
+ return;
881
+ }
882
+ }
883
+
884
+ function onContent(event: PointerEvent, content: HTMLElement): void {
885
+ const combobox = comboboxes.get(content);
886
+ const state = states.get(content);
887
+
888
+ if (combobox == null || state == null) {
889
+ return;
890
+ }
891
+
892
+ const option = findAncestor(event, SELECTOR_OPTION);
893
+
894
+ if (option instanceof HTMLElement) {
895
+ setNext('set', combobox, state, getOptions(state)[0], option);
896
+ }
897
+
898
+ state.anchor.focus();
899
+ }
900
+
901
+ function onEnter(combobox: OuiComboboxElement, state: OuiComboboxState): void {
902
+ if (hasTextSelection(state.anchor)) {
903
+ onSelection(combobox, state);
904
+
905
+ return;
906
+ }
907
+
908
+ if (!state.floatable.open) {
909
+ state.floatable.show();
910
+
911
+ return;
912
+ }
913
+
914
+ setNext('set', combobox, state, getOptions(state)[0], state.active);
915
+ }
916
+
917
+ function onEscape(state: OuiComboboxState): void {
918
+ state.floatable.hide();
919
+ }
920
+
921
+ function onKeydown(event: KeyboardEvent): void {
922
+ onAnchor(event, onAnchorKeydown);
923
+ }
924
+
925
+ function onKeyup(event: KeyboardEvent): void {
926
+ onAnchor(event, onAnchorKeyup);
927
+ }
928
+
929
+ function onNavigation(
930
+ event: KeyboardEvent,
931
+ combobox: OuiComboboxElement,
932
+ state: OuiComboboxState,
933
+ ): void {
934
+ let skip = false;
935
+
936
+ if (!state.floatable.open) {
937
+ skip = true;
938
+
939
+ state.floatable.show();
940
+ }
941
+
942
+ const [all, visible] = getOptions(state);
943
+
944
+ if (visible.length === 0) {
945
+ return;
946
+ }
947
+
948
+ let next: HTMLElement | undefined;
949
+
950
+ if (skip || state.active == null) {
951
+ next = state.active ?? visible.at(event.key === 'ArrowDown' ? 0 : -1);
952
+ } else {
953
+ const current = visible.findIndex(option => option.id === state.active?.id);
954
+
955
+ let index: number | undefined;
956
+
957
+ switch (event.key) {
958
+ case 'ArrowDown':
959
+ case 'ArrowUp':
960
+ index = clamp(current + (event.key === 'ArrowDown' ? 1 : -1), 0, visible.length - 1, true);
961
+ break;
962
+
963
+ case 'End':
964
+ index = visible.length - 1;
965
+ break;
966
+
967
+ case 'Home':
968
+ index = 0;
969
+ break;
970
+
971
+ case 'PageDown':
972
+ case 'PageUp':
973
+ index = clamp(current + (event.key === 'PageDown' ? 10 : -10), 0, visible.length - 1);
974
+ break;
975
+
976
+ default:
977
+ break;
978
+ }
979
+
980
+ if (index != null) {
981
+ next = visible[index];
982
+ }
983
+ }
984
+
985
+ setNext(state.autocomplete.none ? 'none' : 'navigation', combobox, state, all, next);
986
+ }
987
+
988
+ function onRemove(combobox: OuiComboboxElement, state: OuiComboboxState): void {
989
+ const [all] = getOptions(state);
990
+
991
+ const next = filterOptions(state, all);
992
+
993
+ setNext('remove', combobox, state, all, next);
994
+ }
995
+
996
+ function onScrollTo(
997
+ event: KeyboardEvent,
998
+ combobox: OuiComboboxElement,
999
+ state: OuiComboboxState,
1000
+ ): void {
1001
+ const [all] = getOptions(state);
1002
+
1003
+ const key = event.key.toLocaleLowerCase();
1004
+
1005
+ const next =
1006
+ all.find(option => option.textContent?.toLocaleLowerCase().startsWith(key) === true) ??
1007
+ all.find(option => option.textContent?.toLocaleLowerCase().includes(key) === true);
1008
+
1009
+ if (next != null) {
1010
+ setNext('navigation', combobox, state, all, next);
1011
+ }
1012
+ }
1013
+
1014
+ function onSelection(combobox: OuiComboboxElement, state: OuiComboboxState): void {
1015
+ state.filter = state.anchor instanceof HTMLInputElement ? state.anchor.value : '';
1016
+
1017
+ filterOptions(state);
1018
+
1019
+ setNext('set', combobox, state, getOptions(state)[0], state.active);
1020
+ }
1021
+
1022
+ function onTab(combobox: OuiComboboxElement, state: OuiComboboxState): void {
1023
+ if (combobox !== document.activeElement || !combobox.contains(document.activeElement)) {
1024
+ state.floatable.hide();
1025
+ }
1026
+ }
1027
+
1028
+ function onTyped(combobox: OuiComboboxElement, state: OuiComboboxState): void {
1029
+ if (
1030
+ !state.autocomplete.inline &&
1031
+ state.anchor instanceof HTMLInputElement &&
1032
+ state.anchor.value.length > 0
1033
+ ) {
1034
+ state.floatable.show();
1035
+ }
1036
+
1037
+ const options = getOptions(state)[0];
1038
+
1039
+ const next = state.autocomplete.none ? (state.active ?? options[0]) : filterOptions(state);
1040
+
1041
+ setNext('typed', combobox, state, options, next);
1042
+ }
1043
+
1044
+ function setDisabled(combobox: OuiComboboxElement, state: OuiComboboxState, value: boolean): void {
1045
+ state.disabled = value;
1046
+
1047
+ setAria(state.anchor, 'disabled', value);
1048
+ setProperty(combobox, 'disabled', value);
1049
+
1050
+ if (state.anchor instanceof HTMLInputElement) {
1051
+ state.anchor.readOnly = value;
1052
+ }
1053
+ }
1054
+
1055
+ function setLimit(
1056
+ type: 'max' | 'min',
1057
+ combobox: OuiComboboxElement,
1058
+ state: OuiComboboxState,
1059
+ value?: number,
1060
+ ): void {
1061
+ const updateLimit =
1062
+ typeof value === 'number' && !Number.isNaN(value) ? value !== state[type] : value == null;
1063
+
1064
+ if (updateLimit) {
1065
+ state[type] = value;
1066
+
1067
+ setAttribute(combobox, type, value);
1068
+
1069
+ updateValidity(state);
1070
+ }
1071
+ }
1072
+
1073
+ function setNext(
1074
+ update: OuiComboboxUpdate,
1075
+ combobox: OuiComboboxElement,
1076
+ state: OuiComboboxState,
1077
+ all: HTMLElement[],
1078
+ next?: HTMLElement,
1079
+ ): void {
1080
+ for (const option of all) {
1081
+ setAttribute(option, ATTRIBUTE_OPTION_ACTIVE, option === next ? '' : undefined);
1082
+ }
1083
+
1084
+ state.active = next;
1085
+
1086
+ setAria(state.anchor, 'activedescendant', next?.id);
1087
+
1088
+ next?.scrollIntoView(scrollOptions);
1089
+
1090
+ setValue(update, combobox, state, next);
1091
+ }
1092
+
1093
+ function setReadonly(combobox: OuiComboboxElement, state: OuiComboboxState, value: boolean): void {
1094
+ state.readonly = value;
1095
+
1096
+ setAria(state.anchor, 'readonly', value);
1097
+ setProperty(combobox, 'readonly', value);
1098
+
1099
+ if (state.anchor instanceof HTMLInputElement) {
1100
+ state.anchor.readOnly = value;
1101
+ }
1102
+ }
1103
+
1104
+ function setRequired(combobox: OuiComboboxElement, state: OuiComboboxState, value: boolean): void {
1105
+ state.required = value;
1106
+
1107
+ setAria(state.anchor, 'required', value);
1108
+ setProperty(combobox, 'required', value);
1109
+
1110
+ updateValidity(state);
1111
+ }
1112
+
1113
+ function setValue(
1114
+ update: OuiComboboxUpdate,
1115
+ combobox: OuiComboboxElement,
1116
+ state: OuiComboboxState,
1117
+ next?: HTMLElement,
1118
+ ): void {
1119
+ if (state.disabled || state.readonly || (state.type === 'select' && update !== 'set')) {
1120
+ return;
1121
+ }
1122
+
1123
+ let added: boolean | undefined;
1124
+ let shouldUpdate: boolean;
1125
+ let values: string[];
1126
+
1127
+ if (update === 'set' || !(state.anchor instanceof HTMLInputElement)) {
1128
+ added = updateSelected(update, state, next);
1129
+
1130
+ [values, shouldUpdate] = getFormValues(state, added);
1131
+ } else {
1132
+ const [all] = getOptions(state);
1133
+
1134
+ for (const option of all) {
1135
+ setAria(option, 'selected', false);
1136
+ }
1137
+
1138
+ const input = state.anchor.value;
1139
+
1140
+ const content = next?.textContent ?? '';
1141
+ const value = next == null ? '' : (getAttribute(next, 'value') ?? '');
1142
+
1143
+ let selected: HTMLElement | undefined;
1144
+
1145
+ if (content.toLocaleLowerCase() === input.toLocaleLowerCase()) {
1146
+ values = [value];
1147
+
1148
+ if (next != null) {
1149
+ selected = next;
1150
+
1151
+ setAria(next, 'selected', true);
1152
+ }
1153
+ } else {
1154
+ values = [state.autocomplete.none ? input : ''];
1155
+ }
1156
+
1157
+ state.selected = selected == null ? [] : [selected];
1158
+
1159
+ shouldUpdate = state.values[0] !== values[0];
1160
+ }
1161
+
1162
+ if (shouldUpdate) {
1163
+ updateForm(combobox, state, values);
1164
+ }
1165
+
1166
+ if (next == null) {
1167
+ return;
1168
+ }
1169
+
1170
+ if (state.type === 'select') {
1171
+ updateSelect(update, state, next, added);
1172
+ } else if (update !== 'remove') {
1173
+ updateAutoComplete(update, state, next);
1174
+ }
1175
+
1176
+ if (update === 'set' && !state.multiple) {
1177
+ state.floatable.hide();
1178
+ }
1179
+
1180
+ setTimeout(() => {
1181
+ updateWidth(state);
1182
+ });
1183
+ }
1184
+
1185
+ function setValues(combobox: OuiComboboxElement, state: OuiComboboxState, value: unknown): void {
1186
+ if (!Array.isArray(value)) {
1187
+ return;
1188
+ }
1189
+
1190
+ const strings = (state.multiple ? value : [value]).map(value =>
1191
+ getString(value).toLocaleLowerCase(),
1192
+ );
1193
+
1194
+ const [all] = getOptions(state);
1195
+
1196
+ const selected: HTMLElement[] = [];
1197
+ const values: string[] = [];
1198
+
1199
+ let next: HTMLElement | undefined;
1200
+
1201
+ for (const option of all) {
1202
+ const value = getAttribute(option, 'value');
1203
+
1204
+ if (value == null || !strings.includes(value.toLocaleLowerCase())) {
1205
+ continue;
1206
+ }
1207
+
1208
+ selected.push(option);
1209
+ values.push(value);
1210
+
1211
+ next ??= option;
1212
+ }
1213
+
1214
+ for (const option of state.selected) {
1215
+ setAria(option, 'selected', false);
1216
+
1217
+ state.anchor.querySelector(`[${ATTRIBUTE_SELECTION_ITEM}][option="${option.id}"]`)?.remove();
1218
+ }
1219
+
1220
+ for (const option of selected) {
1221
+ setAria(option, 'selected', true);
1222
+
1223
+ state.anchor.querySelector(`[${ATTRIBUTE_SELECTION}]`)?.append(getSelectionItem(option));
1224
+ }
1225
+
1226
+ state.selected = selected.slice();
1227
+
1228
+ setNext('navigation', combobox, state, all, next);
1229
+
1230
+ updateForm(combobox, state, values);
1231
+ }
1232
+
1233
+ function updateAutoComplete(
1234
+ update: OuiComboboxUpdate,
1235
+ state: OuiComboboxState,
1236
+ next: HTMLElement,
1237
+ ): void {
1238
+ if (!(update === 'set' || state.autocomplete.both || state.autocomplete.inline)) {
1239
+ return;
1240
+ }
1241
+
1242
+ const content = next.textContent ?? '';
1243
+
1244
+ if (
1245
+ update === 'typed' &&
1246
+ !content.toLocaleLowerCase().startsWith(state.filter.toLocaleLowerCase())
1247
+ ) {
1248
+ return;
1249
+ }
1250
+
1251
+ const {length} = content;
1252
+
1253
+ if (state.anchor instanceof HTMLInputElement) {
1254
+ state.anchor.value = content;
1255
+
1256
+ state.anchor.setSelectionRange(update === 'typed' ? state.filter.length : length, length);
1257
+ } else {
1258
+ state.anchor.textContent = content;
1259
+ }
1260
+ }
1261
+
1262
+ function updateForm(combobox: OuiComboboxElement, state: OuiComboboxState, values: string[]): void {
1263
+ const formValues = values.length === 0 ? [''] : values;
1264
+
1265
+ const formData = new FormData();
1266
+
1267
+ for (const value of formValues) {
1268
+ formData.append(state.name, value);
1269
+ }
1270
+
1271
+ state.values = formValues;
1272
+
1273
+ state.internals.setFormValue(formData);
1274
+
1275
+ updateValidity(state);
1276
+
1277
+ dispatch(combobox, 'change');
1278
+ }
1279
+
1280
+ function updateSelect(
1281
+ update: OuiComboboxUpdate,
1282
+ state: OuiComboboxState,
1283
+ next: HTMLElement,
1284
+ added?: boolean,
1285
+ ): void {
1286
+ if (update !== 'set') {
1287
+ return;
1288
+ }
1289
+
1290
+ if (!state.multiple) {
1291
+ state.anchor.textContent = next.textContent ?? '';
1292
+
1293
+ return;
1294
+ }
1295
+
1296
+ if (added === true) {
1297
+ state.anchor.querySelector(`[${ATTRIBUTE_SELECTION}]`)?.append(getSelectionItem(next));
1298
+ } else if (added === false) {
1299
+ state.anchor.querySelector(`[${ATTRIBUTE_SELECTION_ITEM}][option="${next.id}"]`)?.remove();
1300
+ }
1301
+ }
1302
+
1303
+ function updateSelected(
1304
+ update: OuiComboboxUpdate,
1305
+ state: OuiComboboxState,
1306
+ next?: HTMLElement,
1307
+ ): boolean | undefined {
1308
+ const [all] = getOptions(state);
1309
+
1310
+ for (const option of all) {
1311
+ setAria(option, 'selected', getSelected(update, state, option, next));
1312
+ }
1313
+
1314
+ if (next == null) {
1315
+ return;
1316
+ }
1317
+
1318
+ if (state.type !== 'select') {
1319
+ state.selected = [next];
1320
+
1321
+ return;
1322
+ }
1323
+
1324
+ if (update !== 'set') {
1325
+ return;
1326
+ }
1327
+
1328
+ if (!state.multiple) {
1329
+ state.selected = [next];
1330
+
1331
+ return;
1332
+ }
1333
+
1334
+ const index = state.selected.indexOf(next);
1335
+ const added = index === -1;
1336
+
1337
+ if (added) {
1338
+ state.selected.push(next);
1339
+ } else {
1340
+ state.selected.splice(index, 1);
1341
+ }
1342
+
1343
+ return added;
1344
+ }
1345
+
1346
+ function updateValidity(state: OuiComboboxState): void {
1347
+ if (state.autocomplete.none) {
1348
+ const valueMissing = state.required && state.values[0].length === 0;
1349
+
1350
+ state.internals.setValidity(
1351
+ {
1352
+ valueMissing,
1353
+ },
1354
+ INVALID_INPUT,
1355
+ );
1356
+
1357
+ state.internals.checkValidity();
1358
+
1359
+ return;
1360
+ }
1361
+
1362
+ const rangeOverflow =
1363
+ state.multiple && typeof state.max === 'number' && state.selected.length > state.max;
1364
+
1365
+ const rangeUnderflow =
1366
+ state.multiple && typeof state.min === 'number' && state.selected.length < state.min;
1367
+
1368
+ const badInput = state.required && state.selected.length === 0;
1369
+
1370
+ state.internals.setValidity(
1371
+ {
1372
+ badInput,
1373
+ rangeOverflow,
1374
+ rangeUnderflow,
1375
+ },
1376
+ join(
1377
+ [
1378
+ badInput ? INVALID_BAD : undefined,
1379
+ rangeOverflow
1380
+ ? getString(INVALID_OVERFLOW).replace('{max}', state.max!.toString())
1381
+ : undefined,
1382
+ rangeUnderflow
1383
+ ? getString(INVALID_UNDERFLOW).replace('{min}', state.min!.toString())
1384
+ : undefined,
1385
+ ],
1386
+ '; ',
1387
+ ),
1388
+ );
1389
+
1390
+ state.internals.checkValidity();
1391
+ }
1392
+
1393
+ function updateWidth(state: OuiComboboxState): void {
1394
+ setStyle(state.content, PROPERTY_STYLE, `${state.anchor.getBoundingClientRect().width}px`);
1395
+ }
1396
+
1397
+ // #endregion
1398
+
1399
+ // #region Variables
1400
+
1401
+ const TAGNAME = 'oui-combobox';
1402
+
1403
+ const ATTRIBUTE_ANCHOR = `${TAGNAME}-anchor`;
1404
+
1405
+ const ATTRIBUTE_CONTENT = `${TAGNAME}-content`;
1406
+
1407
+ const ATTRIBUTE_EMPTY = `${TAGNAME}-empty`;
1408
+
1409
+ const ATTRIBUTE_LIST = `${TAGNAME}-list`;
1410
+
1411
+ const ATTRIBUTE_OPTION = `${TAGNAME}-option`;
1412
+
1413
+ const ATTRIBUTE_OPTION_ACTIVE = `${TAGNAME}-option-active`;
1414
+
1415
+ const ATTRIBUTE_SELECTION = `${TAGNAME}-selection`;
1416
+
1417
+ const ATTRIBUTE_SELECTION_ITEM = `${TAGNAME}-selection-item`;
1418
+
1419
+ const AUTOCOMPLETES = new Set<OuiComboboxAutoCompleteType>(['both', 'inline', 'list', 'none']);
1420
+
1421
+ const EXPRESSION_TYPED = /\p{L}|\s/u;
1422
+
1423
+ const INVALID_BAD = 'Please select an option from the list';
1424
+
1425
+ const INVALID_INPUT = 'Please enter a value';
1426
+
1427
+ const INVALID_OVERFLOW = 'Please select fewer options (maximum is {max})';
1428
+
1429
+ const INVALID_UNDERFLOW = 'Please select more options (minimum is {min})';
1430
+
1431
+ const KEYS_ABSOLUTE = new Set(['End', 'Home']);
1432
+
1433
+ const KEYS_NAVIGATION = new Set(['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp']);
1434
+
1435
+ const KEYS_REMOVE = new Set(['Backspace', 'Delete']);
1436
+
1437
+ const KEYS_SELECTION = new Set(['ArrowRight', 'End']);
1438
+
1439
+ const MESSAGE_LABEL = `<${TAGNAME}> must contain a <label> element for the <select> element`;
1440
+
1441
+ const MESSAGE_SELECT = `<${TAGNAME}> must contain a valid <select> element`;
1442
+
1443
+ const PROPERTY_STYLE = '--oui-combobox-width';
1444
+
1445
+ const SELECTOR_ANCHOR = `[${ATTRIBUTE_ANCHOR}]`;
1446
+
1447
+ const SELECTOR_OPTION = `[${ATTRIBUTE_OPTION}]`;
1448
+
1449
+ const TYPES = new Set<OuiComboboxType>(['autocomplete', 'select']);
1450
+
1451
+ const comboboxes = new WeakMap<HTMLElement, OuiComboboxElement>();
1452
+
1453
+ const scrollOptions: ScrollIntoViewOptions = {
1454
+ behavior: 'smooth',
1455
+ block: 'nearest',
1456
+ inline: 'nearest',
1457
+ };
1458
+
1459
+ const states = new WeakMap<HTMLElement, OuiComboboxState>();
1460
+
1461
+ let index = 0;
1462
+
1463
+ // #endregion
1464
+
1465
+ // #region Initialization
1466
+
1467
+ on(document, 'click', onClick);
1468
+
1469
+ on(document, 'keydown', onKeydown, {
1470
+ passive: false,
1471
+ });
1472
+
1473
+ on(document, 'keyup', onKeyup, {
1474
+ passive: false,
1475
+ });
1476
+
1477
+ customElements.define(TAGNAME, OuiComboboxElement);
1478
+
1479
+ // #endregion