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