@oscarpalmer/oui 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/combobox.css +93 -42
- package/dist/js/combobox.mjs +192 -250
- package/dist/js/index.d.mts +643 -14
- package/dist/js/index.mjs +4875 -18
- package/dist/js/menu.d.mts +1 -1
- package/dist/js/toolbar.d.mts +1 -1
- package/package.json +4 -4
- package/src/css/combobox.scss +98 -31
- package/src/js/combobox.ts +1623 -847
- package/src/js/internal/navigable.ts +10 -3
package/src/js/combobox.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import type {GenericCallback} from '@oscarpalmer/atoms/models';
|
|
1
2
|
import {clamp} from '@oscarpalmer/atoms/number';
|
|
2
|
-
import {getString,
|
|
3
|
-
import {getAria, setAria
|
|
3
|
+
import {getString, parse} from '@oscarpalmer/atoms/string';
|
|
4
|
+
import {getAria, setAria} from '@oscarpalmer/toretto/aria';
|
|
4
5
|
import {getAttribute, setAttribute} from '@oscarpalmer/toretto/attribute';
|
|
5
6
|
import {createElement} from '@oscarpalmer/toretto/create';
|
|
7
|
+
import {getData, setData} from '@oscarpalmer/toretto/data';
|
|
6
8
|
import {dispatch, on} from '@oscarpalmer/toretto/event';
|
|
7
9
|
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';
|
|
10
|
+
import type {AnyAriaBooleanAttribute, RemovableEventListener} from '@oscarpalmer/toretto/models';
|
|
11
11
|
import {createEmbeddedFloatable, type OuiFloatable} from './floatable/floatable.embedded';
|
|
12
12
|
|
|
13
13
|
// #region Types
|
|
@@ -18,57 +18,188 @@ declare global {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
type ApplySelectionParameters = {
|
|
22
|
+
added: boolean;
|
|
23
|
+
emit?: boolean;
|
|
24
|
+
option?: HTMLElement;
|
|
25
|
+
selected?: HTMLElement[];
|
|
26
|
+
state: OuiComboboxState;
|
|
27
|
+
values?: string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type CreateAnchor = {
|
|
31
|
+
container?: HTMLElement;
|
|
32
|
+
element: HTMLElement;
|
|
33
|
+
selection?: OuiComboboxSelection;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type CreatAnchorParameters = {
|
|
37
|
+
autocomplete: OuiComboboxAutoComplete;
|
|
38
|
+
id: string;
|
|
39
|
+
label: HTMLLabelElement;
|
|
40
|
+
multiple: boolean;
|
|
41
|
+
select: HTMLSelectElement;
|
|
42
|
+
type: OuiComboboxType;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type CreateContent = {
|
|
46
|
+
element: HTMLElement;
|
|
47
|
+
filter?: CreateFilter;
|
|
48
|
+
list: CreateList;
|
|
49
|
+
message: HTMLElement;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type CreateContentParameters = {
|
|
53
|
+
autocomplete: OuiComboboxAutoComplete;
|
|
54
|
+
id: string;
|
|
55
|
+
label: HTMLLabelElement;
|
|
56
|
+
multiple: boolean;
|
|
57
|
+
select: HTMLSelectElement;
|
|
58
|
+
type: OuiComboboxType;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type CreateFilter = {
|
|
62
|
+
container: HTMLElement;
|
|
63
|
+
element: HTMLInputElement;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type CreateFilterParameters = {
|
|
67
|
+
autocomplete: OuiComboboxAutoCompleteType;
|
|
68
|
+
id: string;
|
|
69
|
+
label: HTMLLabelElement;
|
|
70
|
+
type: OuiComboboxType;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type CreateGroupParameters = {
|
|
74
|
+
autocomplete: OuiComboboxAutoComplete;
|
|
75
|
+
group: HTMLOptGroupElement;
|
|
76
|
+
groupOptions: HTMLOptionElement[];
|
|
77
|
+
selected: HTMLElement[];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type CreateInputParameters = {
|
|
81
|
+
autocomplete: OuiComboboxAutoCompleteType;
|
|
82
|
+
id: string;
|
|
83
|
+
label: HTMLLabelElement;
|
|
84
|
+
type: OuiComboboxType;
|
|
85
|
+
value: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type CreateList = {
|
|
89
|
+
element: HTMLElement;
|
|
90
|
+
groups?: OuiComboboxGroup[];
|
|
91
|
+
options: HTMLElement[];
|
|
92
|
+
selected: HTMLElement[];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type CreateListParameters = {
|
|
96
|
+
autocomplete: OuiComboboxAutoComplete;
|
|
97
|
+
id: string;
|
|
98
|
+
label: HTMLLabelElement;
|
|
99
|
+
multiple: boolean;
|
|
100
|
+
select: HTMLSelectElement;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type CreateOptionParameters = {
|
|
104
|
+
autocomplete: OuiComboboxAutoComplete;
|
|
105
|
+
group?: string;
|
|
106
|
+
index: number;
|
|
107
|
+
option: HTMLOptionElement;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
type CreateOptionsParameters = {
|
|
111
|
+
autocomplete: OuiComboboxAutoComplete;
|
|
112
|
+
group?: string;
|
|
113
|
+
list: HTMLElement;
|
|
114
|
+
options: HTMLOptionElement[];
|
|
115
|
+
selected: HTMLElement[];
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
type CreateSelectionItemsParameters = {
|
|
119
|
+
container: HTMLElement;
|
|
120
|
+
items: OuiComboboxSelectionItem[];
|
|
121
|
+
options: HTMLElement[];
|
|
122
|
+
state?: OuiComboboxState;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
type OuiComboboxAnchor = {
|
|
126
|
+
container?: HTMLElement;
|
|
127
|
+
element: HTMLElement;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
type OuiComboboxAutoComplete = Record<OuiComboboxAutoCompleteType, boolean> & {
|
|
131
|
+
value: OuiComboboxAutoCompleteType;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
type OuiComboboxAutoCompleteType = 'both' | 'inline' | 'list' | 'none';
|
|
135
|
+
|
|
21
136
|
export class OuiComboboxElement extends HTMLElement {
|
|
22
137
|
static formAssociated = true;
|
|
23
138
|
|
|
24
|
-
static observedAttributes = ['
|
|
139
|
+
static observedAttributes = ['max', 'min', 'required', 'value'];
|
|
25
140
|
|
|
26
141
|
readonly #internals: ElementInternals;
|
|
27
142
|
|
|
28
|
-
|
|
143
|
+
#restored?: string[];
|
|
144
|
+
|
|
145
|
+
#state!: OuiComboboxState;
|
|
29
146
|
|
|
30
147
|
/**
|
|
31
148
|
* Is the combobox disabled?
|
|
32
149
|
*/
|
|
33
150
|
get disabled(): boolean {
|
|
34
|
-
return this
|
|
151
|
+
return this.#state?.disabled ?? false;
|
|
35
152
|
}
|
|
36
153
|
|
|
37
154
|
/**
|
|
38
155
|
* Should the combobox be disabled?
|
|
39
156
|
*/
|
|
40
157
|
set disabled(value: boolean) {
|
|
41
|
-
if (typeof value === 'boolean' && value !== this
|
|
42
|
-
setDisabled(this
|
|
158
|
+
if (typeof value === 'boolean' && value !== this.disabled) {
|
|
159
|
+
setDisabled(this.#state, value);
|
|
43
160
|
}
|
|
44
161
|
}
|
|
45
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Current filter value
|
|
165
|
+
*/
|
|
166
|
+
get filter(): string {
|
|
167
|
+
return this.#state.filter.element.value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Set the filter value
|
|
172
|
+
*/
|
|
173
|
+
set filter(value: string) {
|
|
174
|
+
setFilter(this.#state, value);
|
|
175
|
+
}
|
|
176
|
+
|
|
46
177
|
/**
|
|
47
178
|
* Maximum number of selected options
|
|
48
179
|
*/
|
|
49
180
|
get max(): number | undefined {
|
|
50
|
-
return this.#state
|
|
181
|
+
return this.#state?.max;
|
|
51
182
|
}
|
|
52
183
|
|
|
53
184
|
/**
|
|
54
185
|
* Set the maximum number of selected options
|
|
55
186
|
*/
|
|
56
187
|
set max(value: number) {
|
|
57
|
-
setLimit('max', this
|
|
188
|
+
setLimit('max', this.#state, value);
|
|
58
189
|
}
|
|
59
190
|
|
|
60
191
|
/**
|
|
61
192
|
* Minimum number of selected options
|
|
62
193
|
*/
|
|
63
194
|
get min(): number | undefined {
|
|
64
|
-
return this.#state
|
|
195
|
+
return this.#state?.min;
|
|
65
196
|
}
|
|
66
197
|
|
|
67
198
|
/**
|
|
68
199
|
* Set the minimum number of selected options
|
|
69
200
|
*/
|
|
70
201
|
set min(value: number) {
|
|
71
|
-
setLimit('min', this
|
|
202
|
+
setLimit('min', this.#state, value);
|
|
72
203
|
}
|
|
73
204
|
|
|
74
205
|
/**
|
|
@@ -89,15 +220,15 @@ export class OuiComboboxElement extends HTMLElement {
|
|
|
89
220
|
* Is the combobox readonly?
|
|
90
221
|
*/
|
|
91
222
|
get readonly(): boolean {
|
|
92
|
-
return this
|
|
223
|
+
return this.#state?.readonly ?? false;
|
|
93
224
|
}
|
|
94
225
|
|
|
95
226
|
/**
|
|
96
227
|
* Should the combobox be readonly?
|
|
97
228
|
*/
|
|
98
229
|
set readonly(value: boolean) {
|
|
99
|
-
if (typeof value === 'boolean' && value !== this
|
|
100
|
-
setReadonly(this
|
|
230
|
+
if (typeof value === 'boolean' && value !== this.readonly) {
|
|
231
|
+
setReadonly(this.#state, value);
|
|
101
232
|
}
|
|
102
233
|
}
|
|
103
234
|
|
|
@@ -105,15 +236,15 @@ export class OuiComboboxElement extends HTMLElement {
|
|
|
105
236
|
* Is the combobox required?
|
|
106
237
|
*/
|
|
107
238
|
get required(): boolean {
|
|
108
|
-
return this
|
|
239
|
+
return this.#state?.required ?? false;
|
|
109
240
|
}
|
|
110
241
|
|
|
111
242
|
/**
|
|
112
243
|
* Should the combobox be required?
|
|
113
244
|
*/
|
|
114
245
|
set required(value: boolean) {
|
|
115
|
-
if (typeof value === 'boolean' && value !== this
|
|
116
|
-
setRequired(this
|
|
246
|
+
if (typeof value === 'boolean' && value !== this.required) {
|
|
247
|
+
setRequired(this.#state, value);
|
|
117
248
|
}
|
|
118
249
|
}
|
|
119
250
|
|
|
@@ -121,7 +252,7 @@ export class OuiComboboxElement extends HTMLElement {
|
|
|
121
252
|
* Selected options
|
|
122
253
|
*/
|
|
123
254
|
get selected(): HTMLElement[] {
|
|
124
|
-
return this.#state.selected.slice();
|
|
255
|
+
return this.#state.options.selected.slice();
|
|
125
256
|
}
|
|
126
257
|
|
|
127
258
|
/**
|
|
@@ -135,58 +266,53 @@ export class OuiComboboxElement extends HTMLElement {
|
|
|
135
266
|
* Current value
|
|
136
267
|
*/
|
|
137
268
|
get value(): string {
|
|
138
|
-
return this.#state.values[0];
|
|
269
|
+
return this.#state.values.current[0];
|
|
139
270
|
}
|
|
140
271
|
|
|
141
272
|
/**
|
|
142
273
|
* Set the value
|
|
143
274
|
*/
|
|
144
275
|
set value(value: unknown) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (option != null) {
|
|
148
|
-
setNext('set', this, this.#state, all, option);
|
|
149
|
-
}
|
|
276
|
+
setValue(this.#state, value);
|
|
150
277
|
}
|
|
151
278
|
|
|
152
279
|
/**
|
|
153
280
|
* Current values
|
|
154
281
|
*/
|
|
155
282
|
get values(): string[] {
|
|
156
|
-
return this.#state.values.slice();
|
|
283
|
+
return this.#state.values.current.slice();
|
|
157
284
|
}
|
|
158
285
|
|
|
159
286
|
/**
|
|
160
287
|
* Set the values
|
|
161
288
|
*/
|
|
162
289
|
set values(value: unknown[]) {
|
|
163
|
-
setValues(this
|
|
290
|
+
setValues(this.#state, value);
|
|
164
291
|
}
|
|
165
292
|
|
|
166
293
|
constructor() {
|
|
167
294
|
super();
|
|
168
295
|
|
|
169
296
|
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
297
|
}
|
|
177
298
|
|
|
178
299
|
attributeChangedCallback(name: string, _: string | null, value: string | null): void {
|
|
300
|
+
if (this.#state == null) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
179
304
|
switch (name) {
|
|
180
|
-
case 'disabled':
|
|
181
|
-
case 'readonly':
|
|
182
305
|
case 'required':
|
|
183
|
-
case 'value':
|
|
184
306
|
this[name] = this.hasAttribute(name);
|
|
185
307
|
break;
|
|
186
308
|
|
|
187
309
|
case 'max':
|
|
188
310
|
case 'min':
|
|
189
|
-
setLimit(name, this
|
|
311
|
+
setLimit(name, this.#state, value);
|
|
312
|
+
break;
|
|
313
|
+
|
|
314
|
+
case 'value':
|
|
315
|
+
this.value = value ?? '';
|
|
190
316
|
break;
|
|
191
317
|
|
|
192
318
|
default:
|
|
@@ -194,328 +320,565 @@ export class OuiComboboxElement extends HTMLElement {
|
|
|
194
320
|
}
|
|
195
321
|
}
|
|
196
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Clear all selected options
|
|
325
|
+
*/
|
|
197
326
|
clear(): void {
|
|
198
|
-
setValues(this
|
|
327
|
+
setValues(this.#state, []);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
connectedCallback(): void {
|
|
331
|
+
if (this.#state == null) {
|
|
332
|
+
this.#initialize();
|
|
333
|
+
} else {
|
|
334
|
+
attachCombobox(this.#state);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
disconnectedCallback(): void {
|
|
339
|
+
if (this.#state != null) {
|
|
340
|
+
detachCombobox(this.#state);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
formDisabledCallback(disabled: boolean): void {
|
|
345
|
+
this.disabled = disabled;
|
|
199
346
|
}
|
|
200
347
|
|
|
201
348
|
formResetCallback(): void {
|
|
202
|
-
this.#state.
|
|
349
|
+
setValues(this.#state, this.#state.values.initial, false);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
formStateRestoreCallback(state: string | File | FormData, reason: unknown): void {
|
|
353
|
+
if (reason !== 'restore') {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const parsed = typeof state === 'string' ? parse(state) : undefined;
|
|
358
|
+
|
|
359
|
+
if (!Array.isArray(parsed)) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (this.#state == null) {
|
|
364
|
+
this.#restored = parsed;
|
|
365
|
+
} else {
|
|
366
|
+
setValues(this.#state, parsed);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Set combobox localizations for a specific language
|
|
372
|
+
*
|
|
373
|
+
* @param language Language code
|
|
374
|
+
* @param localizations Localizations for language
|
|
375
|
+
*/
|
|
376
|
+
setLocalizations(language: string, localizations: Partial<OuiComboboxLocalizations>): void;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Set combobox localizations for one or more languages
|
|
380
|
+
*
|
|
381
|
+
* @param localizations Localizations for one or more languages
|
|
382
|
+
*/
|
|
383
|
+
setLocalizations(localizations: Record<string, Partial<OuiComboboxLocalizations>>): void;
|
|
384
|
+
|
|
385
|
+
setLocalizations(
|
|
386
|
+
first: string | Record<string, Partial<OuiComboboxLocalizations>>,
|
|
387
|
+
second?: Partial<OuiComboboxLocalizations>,
|
|
388
|
+
): void {
|
|
389
|
+
updateLocalizations(
|
|
390
|
+
typeof first === 'string'
|
|
391
|
+
? {
|
|
392
|
+
[first]: second!,
|
|
393
|
+
}
|
|
394
|
+
: first,
|
|
395
|
+
this.#state.localizations,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#initialize(): void {
|
|
400
|
+
this.tabIndex = -1;
|
|
401
|
+
|
|
402
|
+
this.#state = createState(this, this.#internals);
|
|
203
403
|
|
|
204
|
-
this
|
|
404
|
+
if (this.#restored != null) {
|
|
405
|
+
setValues(this.#state, this.#restored, false);
|
|
406
|
+
|
|
407
|
+
this.#restored = undefined;
|
|
408
|
+
}
|
|
205
409
|
}
|
|
206
410
|
}
|
|
207
411
|
|
|
208
412
|
type OuiComboboxFilter = {
|
|
209
|
-
element
|
|
413
|
+
element: HTMLInputElement;
|
|
210
414
|
value: string;
|
|
211
415
|
};
|
|
212
416
|
|
|
213
|
-
type
|
|
214
|
-
|
|
417
|
+
type OuiComboboxGroup = {
|
|
418
|
+
element: HTMLElement;
|
|
419
|
+
header: HTMLElement;
|
|
420
|
+
options: HTMLElement[];
|
|
215
421
|
};
|
|
216
422
|
|
|
217
|
-
type
|
|
423
|
+
type OuiComboboxLabel = {
|
|
424
|
+
element: HTMLLabelElement;
|
|
425
|
+
id: string;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
type OuiComboboxLocalizations = {
|
|
429
|
+
empty: string | (() => string);
|
|
430
|
+
input: string | (() => string);
|
|
431
|
+
invalid: string | (() => string);
|
|
432
|
+
max: string | ((max: number) => string);
|
|
433
|
+
min: string | ((min: number) => string);
|
|
434
|
+
remove: string | ((label: string, value: string) => string);
|
|
435
|
+
results: string | ((amount: number) => string);
|
|
436
|
+
selected: string | ((amount: number) => string);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
type OuiComboboxOptions = {
|
|
440
|
+
all: HTMLElement[];
|
|
441
|
+
selected: HTMLElement[];
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
type OuiComboboxPreview = 'none' | 'set' | 'typed';
|
|
445
|
+
|
|
446
|
+
type OuiComboboxSelection = {
|
|
447
|
+
container: HTMLElement;
|
|
448
|
+
items: OuiComboboxSelectionItem[];
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
type OuiComboboxSelectionItem = {
|
|
452
|
+
button: HTMLButtonElement;
|
|
453
|
+
element: HTMLElement;
|
|
454
|
+
};
|
|
218
455
|
|
|
219
456
|
type OuiComboboxState = {
|
|
220
457
|
active?: HTMLElement;
|
|
221
|
-
anchor:
|
|
458
|
+
anchor: OuiComboboxAnchor;
|
|
222
459
|
autocomplete: OuiComboboxAutoComplete;
|
|
223
460
|
content: HTMLElement;
|
|
224
461
|
disabled: boolean;
|
|
225
|
-
empty: HTMLElement;
|
|
226
462
|
filter: OuiComboboxFilter;
|
|
227
463
|
floatable: OuiFloatable;
|
|
228
|
-
|
|
464
|
+
groups?: OuiComboboxGroup[];
|
|
465
|
+
host: OuiComboboxElement;
|
|
229
466
|
id: string;
|
|
230
467
|
internals: ElementInternals;
|
|
231
|
-
label:
|
|
468
|
+
label: OuiComboboxLabel;
|
|
232
469
|
listeners: RemovableEventListener[];
|
|
470
|
+
localizations: Record<string, Partial<OuiComboboxLocalizations>>;
|
|
233
471
|
max?: number;
|
|
234
472
|
min?: number;
|
|
473
|
+
message: HTMLElement;
|
|
235
474
|
name: string;
|
|
236
475
|
multiple: boolean;
|
|
476
|
+
options: OuiComboboxOptions;
|
|
237
477
|
readonly: boolean;
|
|
238
478
|
required: boolean;
|
|
239
479
|
select: HTMLSelectElement;
|
|
240
|
-
|
|
241
|
-
selection?: HTMLElement;
|
|
242
|
-
touched: boolean;
|
|
480
|
+
selection?: OuiComboboxSelection;
|
|
243
481
|
type: OuiComboboxType;
|
|
244
|
-
values:
|
|
482
|
+
values: OuiComboboxValues;
|
|
245
483
|
};
|
|
246
484
|
|
|
247
485
|
type OuiComboboxType = 'autocomplete' | 'select';
|
|
248
486
|
|
|
249
|
-
type
|
|
487
|
+
type OuiComboboxValues = {
|
|
488
|
+
current: string[];
|
|
489
|
+
initial: string[];
|
|
490
|
+
};
|
|
250
491
|
|
|
251
492
|
// #endregion
|
|
252
493
|
|
|
253
494
|
// #region Functions
|
|
254
495
|
|
|
255
|
-
function
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
labelledby: label.id,
|
|
272
|
-
});
|
|
496
|
+
function applySelection(parameters: ApplySelectionParameters): void {
|
|
497
|
+
const {added, emit, option, selected, state} = parameters;
|
|
498
|
+
|
|
499
|
+
const {length} = state.options.all;
|
|
500
|
+
|
|
501
|
+
for (let index = 0; index < length; index += 1) {
|
|
502
|
+
const option = state.options.all[index];
|
|
503
|
+
|
|
504
|
+
setAria(option, 'selected', (selected ?? state.options.selected).includes(option));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const values =
|
|
508
|
+
parameters.values ??
|
|
509
|
+
(state.autocomplete.none
|
|
510
|
+
? [state.filter.element.value]
|
|
511
|
+
: (selected ?? state.options.selected).map(option => getData(option, 'value', false) ?? ''));
|
|
273
512
|
|
|
274
|
-
|
|
275
|
-
|
|
513
|
+
if (values.length === 0) {
|
|
514
|
+
values.push('');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (selected != null) {
|
|
518
|
+
state.options.selected = selected;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const update =
|
|
522
|
+
values.length === state.values.current.length
|
|
523
|
+
? values.some(value => !state.values.current.includes(value))
|
|
524
|
+
: true;
|
|
276
525
|
|
|
277
|
-
|
|
526
|
+
if (!update) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
updateForm(state, values, emit);
|
|
531
|
+
updatePreview('set', state, option);
|
|
532
|
+
updateSelection(state, option, added, selected);
|
|
278
533
|
}
|
|
279
534
|
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
535
|
+
function attachCombobox(state: OuiComboboxState): void {
|
|
536
|
+
const {anchor, content, filter, label, listeners, select} = state;
|
|
537
|
+
|
|
538
|
+
label.element.htmlFor = filter.element.id;
|
|
539
|
+
label.element.id = `${state.id}_label`;
|
|
540
|
+
|
|
541
|
+
select.hidden = true;
|
|
542
|
+
|
|
543
|
+
select.insertAdjacentElement('afterend', anchor.container ?? anchor.element);
|
|
544
|
+
(anchor.container ?? anchor.element).insertAdjacentElement('afterend', content);
|
|
545
|
+
|
|
546
|
+
select.remove();
|
|
547
|
+
|
|
548
|
+
state.floatable = createEmbeddedFloatable(anchor.container ?? anchor.element, content, {
|
|
549
|
+
position: 'below-start',
|
|
550
|
+
reusable: false,
|
|
289
551
|
});
|
|
290
552
|
|
|
291
|
-
|
|
553
|
+
state.floatable.update();
|
|
554
|
+
|
|
555
|
+
listeners.push(
|
|
556
|
+
on(state.host, 'focusout', () => {
|
|
557
|
+
setTimeout(() => {
|
|
558
|
+
onFocusout(state);
|
|
559
|
+
});
|
|
560
|
+
}),
|
|
561
|
+
on(content, 'click', event => {
|
|
562
|
+
onClickContent(event, content);
|
|
563
|
+
}),
|
|
564
|
+
on(content, 'toggle', () => {
|
|
565
|
+
setAria(anchor.element, 'expanded', state.floatable.open);
|
|
566
|
+
|
|
567
|
+
setAria(
|
|
568
|
+
filter.element,
|
|
569
|
+
'activedescendant',
|
|
570
|
+
state.floatable.open ? state.active?.id : undefined,
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
if (state.floatable.open) {
|
|
574
|
+
filter.element.focus();
|
|
575
|
+
}
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
states.map.set(state.host, state);
|
|
580
|
+
states.map.set(state.content, state);
|
|
581
|
+
states.set.add(state);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function createAnchor(parameters: CreatAnchorParameters): CreateAnchor {
|
|
585
|
+
const {autocomplete, id, label, select, type} = parameters;
|
|
586
|
+
|
|
587
|
+
let container: HTMLElement | undefined;
|
|
588
|
+
let element: HTMLElement;
|
|
589
|
+
let selection: OuiComboboxSelection | undefined;
|
|
292
590
|
|
|
293
|
-
if (
|
|
294
|
-
|
|
591
|
+
if (parameters.type === 'autocomplete') {
|
|
592
|
+
element = createInput({
|
|
593
|
+
id,
|
|
594
|
+
label,
|
|
595
|
+
type,
|
|
596
|
+
autocomplete: autocomplete.value,
|
|
597
|
+
value: select.selectedOptions[0]?.textContent ?? '',
|
|
598
|
+
});
|
|
599
|
+
} else {
|
|
600
|
+
({container, element, selection} = createButton(parameters));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
setAttribute(container ?? element, ATTRIBUTE_ANCHOR, '');
|
|
604
|
+
|
|
605
|
+
return {container, element, selection};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function createButton(parameters: CreatAnchorParameters): CreateAnchor {
|
|
609
|
+
const {id, label, multiple, select} = parameters;
|
|
295
610
|
|
|
296
|
-
|
|
611
|
+
const element = createElement('button', {
|
|
612
|
+
aria: {
|
|
613
|
+
controls: `${id}_content`,
|
|
614
|
+
expanded: false,
|
|
615
|
+
haspopup: 'listbox',
|
|
616
|
+
labelledby: `${label.id} ${id}`,
|
|
617
|
+
},
|
|
618
|
+
property: {
|
|
619
|
+
id,
|
|
620
|
+
textContent: multiple ? '' : (select.selectedOptions[0]?.textContent ?? ''),
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
if (!multiple) {
|
|
625
|
+
return {element};
|
|
297
626
|
}
|
|
298
627
|
|
|
299
|
-
|
|
628
|
+
const container = createElement('div');
|
|
629
|
+
const selection = createSelection(id, select);
|
|
630
|
+
|
|
631
|
+
container.append(selection.container, element);
|
|
632
|
+
|
|
633
|
+
return {container, element, selection};
|
|
300
634
|
}
|
|
301
635
|
|
|
302
|
-
function createContent(
|
|
303
|
-
type
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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`,
|
|
636
|
+
function createContent(parameters: CreateContentParameters): CreateContent {
|
|
637
|
+
const {autocomplete, id, label, multiple, select, type} = parameters;
|
|
638
|
+
|
|
639
|
+
const element = createElement('div', {
|
|
640
|
+
aria: {
|
|
641
|
+
labelledby: label.id,
|
|
320
642
|
},
|
|
321
|
-
{
|
|
643
|
+
attribute: {
|
|
322
644
|
[ATTRIBUTE_CONTENT]: '',
|
|
323
645
|
popover: '',
|
|
324
|
-
tabindex: -1,
|
|
325
|
-
'aria-labelledby': label.id,
|
|
326
646
|
},
|
|
327
|
-
|
|
647
|
+
property: {
|
|
648
|
+
id: `${id}_content`,
|
|
649
|
+
tabIndex: -1,
|
|
650
|
+
},
|
|
651
|
+
});
|
|
328
652
|
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
const
|
|
653
|
+
const filter = createFilter({id, label, type, autocomplete: autocomplete.value});
|
|
654
|
+
const list = createList({autocomplete, id, label, multiple, select});
|
|
655
|
+
const message = createMessage();
|
|
332
656
|
|
|
333
657
|
if (filter != null) {
|
|
334
|
-
|
|
658
|
+
element.append(filter.container);
|
|
335
659
|
}
|
|
336
660
|
|
|
337
|
-
|
|
661
|
+
element.append(list.element, message);
|
|
338
662
|
|
|
339
|
-
return
|
|
663
|
+
return {element, filter, list, message};
|
|
340
664
|
}
|
|
341
665
|
|
|
342
|
-
function
|
|
343
|
-
|
|
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
|
-
}
|
|
666
|
+
function createFilter(parameters: CreateFilterParameters): CreateFilter | undefined {
|
|
667
|
+
const {autocomplete, id, label, type} = parameters;
|
|
356
668
|
|
|
357
|
-
function createFilter(
|
|
358
|
-
type: OuiComboboxType,
|
|
359
|
-
id: string,
|
|
360
|
-
): [HTMLElement, HTMLInputElement] | undefined {
|
|
361
669
|
if (type === 'autocomplete') {
|
|
362
670
|
return;
|
|
363
671
|
}
|
|
364
672
|
|
|
365
|
-
const container = createElement(
|
|
366
|
-
|
|
367
|
-
{},
|
|
368
|
-
{
|
|
673
|
+
const container = createElement('div', {
|
|
674
|
+
attribute: {
|
|
369
675
|
[ATTRIBUTE_FILTER]: '',
|
|
370
676
|
},
|
|
371
|
-
);
|
|
677
|
+
});
|
|
372
678
|
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
679
|
+
const element = createInput({autocomplete, id, label, type, value: ''});
|
|
680
|
+
|
|
681
|
+
setAttribute(element, ATTRIBUTE_FILTER_INPUT, '');
|
|
682
|
+
|
|
683
|
+
container.append(element);
|
|
684
|
+
|
|
685
|
+
return {container, element};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function createGroup(parameters: CreateGroupParameters): OuiComboboxGroup {
|
|
689
|
+
const {autocomplete, group, groupOptions, selected} = parameters;
|
|
690
|
+
|
|
691
|
+
const element = createElement('li', {
|
|
692
|
+
aria: {
|
|
693
|
+
label: group.label,
|
|
378
694
|
},
|
|
379
|
-
{
|
|
380
|
-
|
|
695
|
+
property: {
|
|
696
|
+
role: 'group',
|
|
381
697
|
},
|
|
382
|
-
);
|
|
698
|
+
});
|
|
383
699
|
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
id: `${id}_filter`,
|
|
700
|
+
const list = createElement('ul', {
|
|
701
|
+
property: {
|
|
702
|
+
role: 'none',
|
|
388
703
|
},
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const header = createHeader(group);
|
|
393
707
|
|
|
394
|
-
|
|
708
|
+
element.append(header, list);
|
|
395
709
|
|
|
396
|
-
return
|
|
710
|
+
return {
|
|
711
|
+
element,
|
|
712
|
+
header,
|
|
713
|
+
options: createOptions({
|
|
714
|
+
autocomplete,
|
|
715
|
+
list,
|
|
716
|
+
selected,
|
|
717
|
+
group: header.id,
|
|
718
|
+
options: groupOptions,
|
|
719
|
+
}),
|
|
720
|
+
};
|
|
397
721
|
}
|
|
398
722
|
|
|
399
723
|
function createHeader(group: HTMLOptGroupElement): HTMLElement {
|
|
400
|
-
return createElement(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
id: group.id,
|
|
404
|
-
role: 'presentation',
|
|
405
|
-
textContent: group.label,
|
|
724
|
+
return createElement('div', {
|
|
725
|
+
aria: {
|
|
726
|
+
hidden: true,
|
|
406
727
|
},
|
|
407
|
-
{
|
|
728
|
+
attribute: {
|
|
408
729
|
[ATTRIBUTE_HEADER]: '',
|
|
409
730
|
},
|
|
410
|
-
|
|
731
|
+
property: {
|
|
732
|
+
id: group.id,
|
|
733
|
+
textContent: group.label,
|
|
734
|
+
},
|
|
735
|
+
});
|
|
411
736
|
}
|
|
412
737
|
|
|
413
|
-
function createInput(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
738
|
+
function createInput(parameters: CreateInputParameters): HTMLInputElement {
|
|
739
|
+
const {autocomplete, id, label, type, value} = parameters;
|
|
740
|
+
|
|
741
|
+
return createElement('input', {
|
|
742
|
+
aria: {
|
|
743
|
+
autocomplete: autocomplete,
|
|
744
|
+
controls: `${id}_list`,
|
|
745
|
+
expanded: type === 'select',
|
|
746
|
+
haspopup: 'listbox',
|
|
747
|
+
labelledby: label.id,
|
|
419
748
|
},
|
|
420
|
-
{
|
|
421
|
-
|
|
749
|
+
attribute: {
|
|
750
|
+
autocorrect: 'off',
|
|
422
751
|
autocomplete: 'off',
|
|
752
|
+
form: '',
|
|
753
|
+
spellcheck: 'false',
|
|
423
754
|
type: 'text',
|
|
424
755
|
},
|
|
425
|
-
|
|
756
|
+
property: {
|
|
757
|
+
id,
|
|
758
|
+
value,
|
|
759
|
+
role: 'combobox',
|
|
760
|
+
},
|
|
761
|
+
});
|
|
426
762
|
}
|
|
427
763
|
|
|
428
|
-
function createList(
|
|
429
|
-
label
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
'ul',
|
|
436
|
-
{
|
|
437
|
-
role: 'listbox',
|
|
764
|
+
function createList(parameters: CreateListParameters): CreateList {
|
|
765
|
+
const {autocomplete, id, label, multiple, select} = parameters;
|
|
766
|
+
|
|
767
|
+
const element = createElement('ul', {
|
|
768
|
+
aria: {
|
|
769
|
+
labelledby: label.id,
|
|
770
|
+
multiselectable: multiple,
|
|
438
771
|
},
|
|
439
|
-
{
|
|
772
|
+
attribute: {
|
|
440
773
|
[ATTRIBUTE_LIST]: '',
|
|
441
|
-
|
|
442
|
-
'aria-multiselectable': multiple,
|
|
774
|
+
tabindex: -1,
|
|
443
775
|
},
|
|
444
|
-
|
|
776
|
+
property: {
|
|
777
|
+
id: `${id}_list`,
|
|
778
|
+
role: 'listbox',
|
|
779
|
+
},
|
|
780
|
+
});
|
|
445
781
|
|
|
446
|
-
const
|
|
447
|
-
const
|
|
782
|
+
const children = [...select.children];
|
|
783
|
+
const childrenLength = children.length;
|
|
448
784
|
|
|
785
|
+
const options: HTMLElement[] = [];
|
|
449
786
|
const selected: HTMLElement[] = [];
|
|
450
787
|
|
|
451
|
-
let
|
|
788
|
+
let groups: OuiComboboxGroup[] | undefined;
|
|
452
789
|
|
|
453
|
-
|
|
454
|
-
|
|
790
|
+
for (let childIndex = 0; childIndex < childrenLength; childIndex += 1) {
|
|
791
|
+
const child = children[childIndex];
|
|
455
792
|
|
|
456
|
-
|
|
793
|
+
if (child instanceof HTMLOptionElement) {
|
|
794
|
+
const option = createOption({
|
|
795
|
+
autocomplete,
|
|
796
|
+
index: childIndex,
|
|
797
|
+
option: child,
|
|
798
|
+
});
|
|
457
799
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const options = [...group.querySelectorAll('option')];
|
|
461
|
-
const optionsLength = options.length;
|
|
800
|
+
element.append(option);
|
|
801
|
+
options.push(option);
|
|
462
802
|
|
|
463
|
-
if (
|
|
464
|
-
|
|
803
|
+
if (child.selected) {
|
|
804
|
+
selected.push(option);
|
|
465
805
|
}
|
|
466
806
|
|
|
467
|
-
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (!(child instanceof HTMLOptGroupElement)) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
468
813
|
|
|
469
|
-
|
|
814
|
+
const group = createGroup({
|
|
815
|
+
autocomplete,
|
|
816
|
+
selected,
|
|
817
|
+
group: child,
|
|
818
|
+
groupOptions: [...child.children] as HTMLOptionElement[],
|
|
819
|
+
});
|
|
470
820
|
|
|
471
|
-
|
|
472
|
-
list.append(header);
|
|
821
|
+
groups ??= [];
|
|
473
822
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
createOptions([...select.options], autocomplete, list, selected, true);
|
|
823
|
+
element.append(group.element, ...group.options);
|
|
824
|
+
groups.push(group);
|
|
825
|
+
options.push(...group.options);
|
|
478
826
|
}
|
|
479
827
|
|
|
480
|
-
return
|
|
828
|
+
return {element, groups, options, selected};
|
|
481
829
|
}
|
|
482
830
|
|
|
483
|
-
function
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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,
|
|
831
|
+
function createMessage(): HTMLElement {
|
|
832
|
+
return createElement('div', {
|
|
833
|
+
aria: {
|
|
834
|
+
live: 'polite',
|
|
496
835
|
},
|
|
497
|
-
{
|
|
498
|
-
|
|
836
|
+
attribute: {
|
|
837
|
+
[ATTRIBUTE_MESSAGE]: '',
|
|
838
|
+
},
|
|
839
|
+
property: {
|
|
840
|
+
hidden: true,
|
|
841
|
+
role: 'status',
|
|
842
|
+
},
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function createOption(parameters: CreateOptionParameters): HTMLElement {
|
|
847
|
+
const {autocomplete, group, option} = parameters;
|
|
848
|
+
|
|
849
|
+
const item = createElement('li', {
|
|
850
|
+
aria: {
|
|
851
|
+
selected: !autocomplete.none && option.selected,
|
|
852
|
+
},
|
|
853
|
+
attribute: {
|
|
499
854
|
[ATTRIBUTE_OPTION]: '',
|
|
500
|
-
[ATTRIBUTE_OPTION_ACTIVE]: active && index === 0 ? '' : undefined,
|
|
501
|
-
value: option.value,
|
|
502
|
-
'aria-selected': !autocomplete.none && option.selected,
|
|
503
855
|
},
|
|
504
|
-
|
|
856
|
+
property: {
|
|
857
|
+
id: option.id,
|
|
858
|
+
role: 'option',
|
|
859
|
+
textContent: option.textContent,
|
|
860
|
+
},
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
setData(item, {
|
|
864
|
+
group,
|
|
865
|
+
value: option.value,
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
return item;
|
|
505
869
|
}
|
|
506
870
|
|
|
507
|
-
function createOptions(
|
|
508
|
-
options
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
active: boolean,
|
|
513
|
-
group?: string,
|
|
514
|
-
): void {
|
|
871
|
+
function createOptions(parameters: CreateOptionsParameters): HTMLElement[] {
|
|
872
|
+
const {autocomplete, group, list, options, selected} = parameters;
|
|
873
|
+
|
|
874
|
+
const items: HTMLElement[] = [];
|
|
875
|
+
|
|
515
876
|
for (let index = 0; index < options.length; index += 1) {
|
|
516
877
|
const option = options[index];
|
|
517
878
|
|
|
518
|
-
const item = createOption(
|
|
879
|
+
const item = createOption({autocomplete, group, index, option});
|
|
880
|
+
|
|
881
|
+
items.push(item);
|
|
519
882
|
|
|
520
883
|
if (option.selected) {
|
|
521
884
|
selected.push(item);
|
|
@@ -523,80 +886,320 @@ function createOptions(
|
|
|
523
886
|
|
|
524
887
|
list.append(item);
|
|
525
888
|
}
|
|
889
|
+
|
|
890
|
+
return items;
|
|
526
891
|
}
|
|
527
892
|
|
|
528
|
-
function createSelection(select: HTMLSelectElement):
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
{},
|
|
532
|
-
{
|
|
893
|
+
function createSelection(id: string, select: HTMLSelectElement): OuiComboboxSelection {
|
|
894
|
+
const container = createElement('ul', {
|
|
895
|
+
attribute: {
|
|
533
896
|
[ATTRIBUTE_SELECTION]: '',
|
|
534
897
|
},
|
|
535
|
-
|
|
898
|
+
property: {
|
|
899
|
+
id: `${id}_selection`,
|
|
900
|
+
},
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
const options = [...select.selectedOptions];
|
|
904
|
+
|
|
905
|
+
const items: OuiComboboxSelectionItem[] = [];
|
|
906
|
+
|
|
907
|
+
createSelectionItems({container, items, options});
|
|
536
908
|
|
|
537
|
-
|
|
538
|
-
|
|
909
|
+
const {button} = items[0] ?? {};
|
|
910
|
+
|
|
911
|
+
if (button != null) {
|
|
912
|
+
button.tabIndex = 0;
|
|
539
913
|
}
|
|
540
914
|
|
|
541
|
-
return
|
|
915
|
+
return {
|
|
916
|
+
items,
|
|
917
|
+
container: container,
|
|
918
|
+
};
|
|
542
919
|
}
|
|
543
920
|
|
|
544
|
-
function
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
921
|
+
function createSelectionItem(
|
|
922
|
+
option: HTMLElement,
|
|
923
|
+
state?: OuiComboboxState,
|
|
924
|
+
): OuiComboboxSelectionItem {
|
|
925
|
+
const element = createElement('li');
|
|
926
|
+
|
|
927
|
+
const localization = state == null ? option.textContent : getLocalization('remove', state);
|
|
928
|
+
|
|
929
|
+
const button = createElement('button', {
|
|
930
|
+
aria: {
|
|
931
|
+
label:
|
|
932
|
+
typeof localization === 'function'
|
|
933
|
+
? localization(option.textContent ?? '', getData(option, 'value', false) ?? '')
|
|
934
|
+
: localization,
|
|
935
|
+
},
|
|
936
|
+
attribute: {
|
|
937
|
+
[ATTRIBUTE_SELECTION_ITEM]: '',
|
|
938
|
+
tabindex: -1,
|
|
939
|
+
},
|
|
940
|
+
data: {
|
|
941
|
+
option: option.id,
|
|
942
|
+
},
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
const icon = createElement('span', {
|
|
946
|
+
aria: {
|
|
947
|
+
hidden: true,
|
|
948
|
+
},
|
|
949
|
+
property: {
|
|
950
|
+
innerHTML: '×',
|
|
951
|
+
},
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
const label = createElement('span', {
|
|
955
|
+
property: {
|
|
956
|
+
textContent: option.textContent,
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
button.append(icon, label);
|
|
961
|
+
element.append(button);
|
|
962
|
+
|
|
963
|
+
return {
|
|
964
|
+
button,
|
|
965
|
+
element,
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function createSelectionItems(parameters: CreateSelectionItemsParameters): void {
|
|
970
|
+
const {container, items, options, state} = parameters;
|
|
971
|
+
|
|
972
|
+
const {length} = options;
|
|
973
|
+
|
|
974
|
+
for (let index = 0; index < length; index += 1) {
|
|
975
|
+
const option = options[index];
|
|
976
|
+
|
|
977
|
+
const item = createSelectionItem(option, state);
|
|
978
|
+
|
|
979
|
+
container.append(item.element);
|
|
980
|
+
items.push(item);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function createState(element: OuiComboboxElement, internals: ElementInternals): OuiComboboxState {
|
|
985
|
+
const select = element.querySelector('select');
|
|
986
|
+
|
|
987
|
+
if (select == null || select.id === '') {
|
|
988
|
+
throw new Error(MESSAGE_SELECT);
|
|
551
989
|
}
|
|
552
990
|
|
|
553
|
-
const
|
|
991
|
+
const label = element.querySelector(`label[for="${select.id}"]`) as HTMLLabelElement;
|
|
554
992
|
|
|
555
|
-
|
|
556
|
-
(
|
|
557
|
-
|
|
993
|
+
if (label == null) {
|
|
994
|
+
throw new Error(MESSAGE_LABEL);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
element.id = select.id;
|
|
558
998
|
|
|
559
|
-
|
|
999
|
+
setAttribute(element, 'name', select.name === '' ? select.id : select.name);
|
|
1000
|
+
|
|
1001
|
+
const disabled = getBooleanAria(element, 'disabled') || select.disabled;
|
|
1002
|
+
const multiple = element.multiple || select.multiple;
|
|
1003
|
+
const readonly = getBooleanAria(element, 'readonly');
|
|
1004
|
+
const required = getBooleanAria(element, 'required') || select.required;
|
|
1005
|
+
|
|
1006
|
+
const id = getId();
|
|
1007
|
+
const type = multiple ? 'select' : getAttributeValue(element, 'type', TYPES, 'select');
|
|
1008
|
+
|
|
1009
|
+
const autocomplete = getAutoComplete(element, type === 'select');
|
|
1010
|
+
|
|
1011
|
+
const labelId = label.id;
|
|
1012
|
+
|
|
1013
|
+
label.htmlFor = id;
|
|
1014
|
+
label.id = `${id}_label`;
|
|
1015
|
+
|
|
1016
|
+
generateIds(id, select);
|
|
1017
|
+
|
|
1018
|
+
const anchor = createAnchor({autocomplete, id, label, multiple, select, type});
|
|
1019
|
+
const content = createContent({autocomplete, id, label, multiple, select, type});
|
|
1020
|
+
|
|
1021
|
+
const values =
|
|
1022
|
+
content.list.selected.length === 0
|
|
1023
|
+
? ['']
|
|
1024
|
+
: content.list.selected.map(option => getData(option, 'value', false) ?? '');
|
|
1025
|
+
|
|
1026
|
+
const state: OuiComboboxState = {
|
|
1027
|
+
autocomplete,
|
|
1028
|
+
disabled,
|
|
1029
|
+
id,
|
|
1030
|
+
internals,
|
|
1031
|
+
multiple,
|
|
1032
|
+
readonly,
|
|
1033
|
+
required,
|
|
1034
|
+
select,
|
|
1035
|
+
type,
|
|
1036
|
+
anchor: {
|
|
1037
|
+
container: anchor.container,
|
|
1038
|
+
element: anchor.element,
|
|
1039
|
+
},
|
|
1040
|
+
active: content.list.selected[0],
|
|
1041
|
+
content: content.element,
|
|
1042
|
+
filter: {
|
|
1043
|
+
element: content.filter?.element ?? (anchor.element as HTMLInputElement),
|
|
1044
|
+
value: '',
|
|
1045
|
+
},
|
|
1046
|
+
floatable: undefined as never,
|
|
1047
|
+
groups: content.list.groups,
|
|
1048
|
+
host: element,
|
|
1049
|
+
label: {
|
|
1050
|
+
element: label,
|
|
1051
|
+
id: labelId,
|
|
560
1052
|
},
|
|
561
|
-
|
|
562
|
-
|
|
1053
|
+
listeners: [],
|
|
1054
|
+
localizations: {},
|
|
1055
|
+
message: content.message,
|
|
1056
|
+
name: element.name,
|
|
1057
|
+
options: {
|
|
1058
|
+
all: content.list.options,
|
|
1059
|
+
selected: content.list.selected,
|
|
1060
|
+
},
|
|
1061
|
+
selection: anchor.selection,
|
|
1062
|
+
values: {
|
|
1063
|
+
current: values.slice(),
|
|
1064
|
+
initial: values.slice(),
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
563
1067
|
|
|
564
|
-
|
|
565
|
-
let {length} = all;
|
|
1068
|
+
attachCombobox(state);
|
|
566
1069
|
|
|
567
|
-
|
|
1070
|
+
setDisabled(state, disabled);
|
|
1071
|
+
setLimit('max', state, getAttribute(element, 'max'));
|
|
1072
|
+
setLimit('min', state, getAttribute(element, 'min'));
|
|
1073
|
+
setReadonly(state, readonly);
|
|
1074
|
+
setRequired(state, required);
|
|
1075
|
+
|
|
1076
|
+
filterOptions(state);
|
|
1077
|
+
|
|
1078
|
+
if (content.list.selected.length > 0) {
|
|
1079
|
+
updatePreview('set', state, content.list.selected[0]);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
updateForm(state, state.values.current, false);
|
|
1083
|
+
updateMessage(state, getVisible(state).length);
|
|
1084
|
+
|
|
1085
|
+
return state;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function detachCombobox(state: OuiComboboxState): void {
|
|
1089
|
+
const {content, floatable, host, label, select} = state;
|
|
1090
|
+
|
|
1091
|
+
const listeners = state.listeners.splice(0);
|
|
1092
|
+
const {length} = listeners;
|
|
568
1093
|
|
|
569
1094
|
for (let index = 0; index < length; index += 1) {
|
|
570
|
-
|
|
1095
|
+
listeners[index]();
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
floatable.destroy();
|
|
1099
|
+
|
|
1100
|
+
const anchor = state.anchor.container ?? state.anchor.element;
|
|
1101
|
+
|
|
1102
|
+
anchor.insertAdjacentElement('beforebegin', select);
|
|
1103
|
+
|
|
1104
|
+
anchor.remove();
|
|
1105
|
+
content.remove();
|
|
1106
|
+
|
|
1107
|
+
label.element.htmlFor = select.id;
|
|
1108
|
+
label.element.id = state.label.id;
|
|
1109
|
+
|
|
1110
|
+
select.hidden = false;
|
|
1111
|
+
|
|
1112
|
+
states.map.delete(host);
|
|
1113
|
+
states.map.delete(content);
|
|
1114
|
+
states.set.delete(state);
|
|
1115
|
+
}
|
|
571
1116
|
|
|
572
|
-
|
|
1117
|
+
function filterOptions(state: OuiComboboxState): HTMLElement | undefined {
|
|
1118
|
+
function filter(options: HTMLElement[]): boolean {
|
|
1119
|
+
const {length} = options;
|
|
573
1120
|
|
|
574
|
-
|
|
575
|
-
groups[getAttribute(option, 'group') ?? ''] += 1;
|
|
1121
|
+
let count = 0;
|
|
576
1122
|
|
|
577
|
-
|
|
1123
|
+
for (let index = 0; index < length; index += 1) {
|
|
1124
|
+
const option = options[index];
|
|
1125
|
+
|
|
1126
|
+
option.hidden =
|
|
1127
|
+
!state.autocomplete.none && !option.textContent.toLocaleLowerCase().includes(value);
|
|
1128
|
+
|
|
1129
|
+
if (!option.hidden) {
|
|
1130
|
+
count += 1;
|
|
1131
|
+
|
|
1132
|
+
visible.push(option);
|
|
1133
|
+
}
|
|
578
1134
|
}
|
|
1135
|
+
|
|
1136
|
+
return count === 0;
|
|
579
1137
|
}
|
|
580
1138
|
|
|
581
|
-
|
|
582
|
-
length = state.headers.length;
|
|
1139
|
+
const value = state.filter.element.value.toLocaleLowerCase();
|
|
583
1140
|
|
|
584
|
-
|
|
585
|
-
|
|
1141
|
+
const visible: HTMLElement[] = [];
|
|
1142
|
+
|
|
1143
|
+
if (state.groups == null) {
|
|
1144
|
+
filter(state.options.all);
|
|
1145
|
+
} else {
|
|
1146
|
+
const groupsLength = state.groups.length;
|
|
1147
|
+
|
|
1148
|
+
for (let groupIndex = 0; groupIndex < groupsLength; groupIndex += 1) {
|
|
1149
|
+
const {element, options} = state.groups[groupIndex];
|
|
586
1150
|
|
|
587
|
-
|
|
1151
|
+
element.hidden = filter(options);
|
|
588
1152
|
}
|
|
589
1153
|
}
|
|
590
1154
|
|
|
591
|
-
state
|
|
1155
|
+
updateMessage(state, visible.length);
|
|
592
1156
|
|
|
593
|
-
|
|
594
|
-
|
|
1157
|
+
return state.active != null && visible.includes(state.active) ? state.active : visible[0];
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function filterOption(
|
|
1161
|
+
state: OuiComboboxState,
|
|
1162
|
+
value: unknown,
|
|
1163
|
+
textContent?: boolean,
|
|
1164
|
+
): HTMLElement | undefined {
|
|
1165
|
+
const filterTextContent = textContent ?? false;
|
|
1166
|
+
|
|
1167
|
+
const filter = getString(value).toLocaleLowerCase();
|
|
1168
|
+
|
|
1169
|
+
filterOptions(state);
|
|
1170
|
+
|
|
1171
|
+
const {length} = state.options.all;
|
|
1172
|
+
let match: HTMLElement | undefined;
|
|
1173
|
+
|
|
1174
|
+
for (let index = 0; index < length; index += 1) {
|
|
1175
|
+
const option = state.options.all[index];
|
|
1176
|
+
|
|
1177
|
+
const value = (
|
|
1178
|
+
filterTextContent ? option.textContent : (getData(option, 'value', false) ?? '')
|
|
1179
|
+
).toLocaleLowerCase();
|
|
1180
|
+
|
|
1181
|
+
if (value === filter) {
|
|
1182
|
+
match = option;
|
|
1183
|
+
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
595
1186
|
}
|
|
1187
|
+
|
|
1188
|
+
return match;
|
|
596
1189
|
}
|
|
597
1190
|
|
|
598
|
-
function generateIds(
|
|
599
|
-
const elements = [...select.
|
|
1191
|
+
function generateIds(prefix: string, select: HTMLSelectElement): void {
|
|
1192
|
+
const elements = [...select.children].flatMap(child =>
|
|
1193
|
+
child instanceof HTMLOptGroupElement
|
|
1194
|
+
? [
|
|
1195
|
+
child,
|
|
1196
|
+
...[...child.children].filter(groupChild => groupChild instanceof HTMLOptionElement),
|
|
1197
|
+
]
|
|
1198
|
+
: child instanceof HTMLOptionElement
|
|
1199
|
+
? [child]
|
|
1200
|
+
: [],
|
|
1201
|
+
);
|
|
1202
|
+
|
|
600
1203
|
const {length} = elements;
|
|
601
1204
|
|
|
602
1205
|
for (let index = 0; index < length; index += 1) {
|
|
@@ -637,36 +1240,10 @@ function getAutoComplete(element: OuiComboboxElement, select: boolean): OuiCombo
|
|
|
637
1240
|
};
|
|
638
1241
|
}
|
|
639
1242
|
|
|
640
|
-
function
|
|
641
|
-
|
|
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
|
-
}
|
|
1243
|
+
function getBooleanAria(element: HTMLElement, attribute: AnyAriaBooleanAttribute): boolean {
|
|
1244
|
+
const value = getAria(element, attribute);
|
|
665
1245
|
|
|
666
|
-
|
|
667
|
-
const next = getFormValue(state, values);
|
|
668
|
-
|
|
669
|
-
return [values, next !== previous];
|
|
1246
|
+
return value === true || value === 'true';
|
|
670
1247
|
}
|
|
671
1248
|
|
|
672
1249
|
function getId(): string {
|
|
@@ -675,234 +1252,82 @@ function getId(): string {
|
|
|
675
1252
|
return `oui_combobox_${index}`;
|
|
676
1253
|
}
|
|
677
1254
|
|
|
678
|
-
function
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
)
|
|
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];
|
|
1255
|
+
function getLanguage(state: OuiComboboxState): string {
|
|
1256
|
+
return (
|
|
1257
|
+
state.host.closest('[lang]')?.getAttribute('lang') ?? document.documentElement.lang
|
|
1258
|
+
).toLowerCase();
|
|
698
1259
|
}
|
|
699
1260
|
|
|
700
|
-
function
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1261
|
+
function getLocalization<Key extends keyof OuiComboboxLocalizations>(
|
|
1262
|
+
key: Key,
|
|
1263
|
+
state: OuiComboboxState,
|
|
1264
|
+
): OuiComboboxLocalizations[Key] {
|
|
1265
|
+
const language = getLanguage(state);
|
|
704
1266
|
|
|
705
|
-
const
|
|
1267
|
+
const allLocalizations = [state.localizations, LOCALIZATIONS.globals];
|
|
1268
|
+
const {length} = allLocalizations;
|
|
706
1269
|
|
|
707
|
-
|
|
708
|
-
|
|
1270
|
+
let localization: OuiComboboxLocalizations[Key] | undefined;
|
|
1271
|
+
let shortLanguage: string | undefined;
|
|
709
1272
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
state: OuiComboboxState,
|
|
713
|
-
element: HTMLElement,
|
|
714
|
-
next?: HTMLElement,
|
|
715
|
-
): boolean {
|
|
716
|
-
if (state.autocomplete.none) {
|
|
717
|
-
return false;
|
|
718
|
-
}
|
|
1273
|
+
for (let index = 0; index < length; index += 1) {
|
|
1274
|
+
const localizations = allLocalizations[index];
|
|
719
1275
|
|
|
720
|
-
|
|
1276
|
+
localization = localizations?.[language]?.[key];
|
|
721
1277
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
return element === next;
|
|
1278
|
+
if (localization != null) {
|
|
1279
|
+
break;
|
|
725
1280
|
}
|
|
726
1281
|
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
if (update === 'none') {
|
|
731
|
-
return selected;
|
|
732
|
-
}
|
|
1282
|
+
shortLanguage ??= language.split('-')[0];
|
|
733
1283
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
return selected;
|
|
1284
|
+
if (shortLanguage === language) {
|
|
1285
|
+
continue;
|
|
737
1286
|
}
|
|
738
1287
|
|
|
739
|
-
|
|
740
|
-
return element === next ? !selected : selected;
|
|
741
|
-
}
|
|
1288
|
+
localization = localizations?.[shortLanguage]?.[key];
|
|
742
1289
|
|
|
743
|
-
|
|
1290
|
+
if (localization != null) {
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
744
1293
|
}
|
|
745
1294
|
|
|
746
|
-
return
|
|
1295
|
+
return localization ?? LOCALIZATIONS.defaults[key];
|
|
747
1296
|
}
|
|
748
1297
|
|
|
749
|
-
function
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
{
|
|
753
|
-
innerHTML: '×',
|
|
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);
|
|
1298
|
+
function getVisible(state: OuiComboboxState): HTMLElement[] {
|
|
1299
|
+
return state.options.all.filter(option => !option.hidden);
|
|
1300
|
+
}
|
|
774
1301
|
|
|
775
|
-
|
|
1302
|
+
function hasTextSelection(element: HTMLElement): element is HTMLInputElement {
|
|
1303
|
+
return element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
|
|
776
1304
|
}
|
|
777
1305
|
|
|
778
|
-
function
|
|
779
|
-
const
|
|
1306
|
+
function onClickContent(event: PointerEvent, content: HTMLElement): void {
|
|
1307
|
+
const state = states.map.get(content);
|
|
780
1308
|
|
|
781
|
-
if (
|
|
782
|
-
|
|
1309
|
+
if (state == null) {
|
|
1310
|
+
return;
|
|
783
1311
|
}
|
|
784
1312
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (label == null) {
|
|
788
|
-
throw new Error(MESSAGE_LABEL);
|
|
1313
|
+
if (state.disabled) {
|
|
1314
|
+
return;
|
|
789
1315
|
}
|
|
790
1316
|
|
|
791
|
-
|
|
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
|
-
}
|
|
1317
|
+
const option = findAncestor(event, SELECTOR_OPTION);
|
|
898
1318
|
|
|
899
|
-
|
|
900
|
-
|
|
1319
|
+
if (!(option instanceof HTMLElement)) {
|
|
1320
|
+
state.filter.element.focus();
|
|
1321
|
+
} else if (state.readonly) {
|
|
1322
|
+
updatePreview('set', state, option);
|
|
1323
|
+
} else {
|
|
1324
|
+
toggleOption(state, option);
|
|
1325
|
+
}
|
|
901
1326
|
}
|
|
902
1327
|
|
|
903
|
-
function
|
|
1328
|
+
function onClickDocument(event: PointerEvent): void {
|
|
904
1329
|
const combobox = findAncestor(event, TAGNAME);
|
|
905
|
-
const state = states.get(combobox!);
|
|
1330
|
+
const state = states.map.get(combobox!);
|
|
906
1331
|
|
|
907
1332
|
if (combobox == null || state == null) {
|
|
908
1333
|
return;
|
|
@@ -911,10 +1336,12 @@ function onClick(event: PointerEvent): void {
|
|
|
911
1336
|
const label = findAncestor(event, 'label');
|
|
912
1337
|
|
|
913
1338
|
if (label != null) {
|
|
914
|
-
if (label === state.label && combobox.type === 'select') {
|
|
915
|
-
state.anchor.focus();
|
|
1339
|
+
if (label === state.label.element && combobox.type === 'select') {
|
|
1340
|
+
state.anchor.element.focus();
|
|
916
1341
|
|
|
917
|
-
state.
|
|
1342
|
+
if (!state.disabled) {
|
|
1343
|
+
state.floatable.show();
|
|
1344
|
+
}
|
|
918
1345
|
}
|
|
919
1346
|
|
|
920
1347
|
return;
|
|
@@ -923,118 +1350,196 @@ function onClick(event: PointerEvent): void {
|
|
|
923
1350
|
const selection = findAncestor(event, `[${ATTRIBUTE_SELECTION_ITEM}]`);
|
|
924
1351
|
|
|
925
1352
|
if (selection != null) {
|
|
926
|
-
const option = state.selected.find(
|
|
1353
|
+
const option = state.options.selected.find(
|
|
1354
|
+
option => option.id === getData(selection, 'option', false),
|
|
1355
|
+
);
|
|
927
1356
|
|
|
928
|
-
|
|
1357
|
+
if (state.disabled) {
|
|
1358
|
+
state.anchor.element.focus();
|
|
1359
|
+
} else if (state.readonly) {
|
|
1360
|
+
updateTabindices(state, option);
|
|
1361
|
+
} else {
|
|
1362
|
+
toggleOption(state, option);
|
|
1363
|
+
}
|
|
929
1364
|
|
|
930
1365
|
return;
|
|
931
1366
|
}
|
|
932
1367
|
|
|
933
1368
|
const anchor = findAncestor(event, SELECTOR_ANCHOR);
|
|
934
1369
|
|
|
935
|
-
if (anchor != null) {
|
|
1370
|
+
if (anchor != null && !state.disabled) {
|
|
936
1371
|
state.floatable.show();
|
|
937
1372
|
|
|
938
1373
|
return;
|
|
939
1374
|
}
|
|
940
1375
|
}
|
|
941
1376
|
|
|
942
|
-
function
|
|
943
|
-
|
|
944
|
-
const state = states.get(content);
|
|
945
|
-
|
|
946
|
-
if (combobox == null || state == null) {
|
|
1377
|
+
function onEnter(state: OuiComboboxState): void {
|
|
1378
|
+
if (state.disabled || state.readonly) {
|
|
947
1379
|
return;
|
|
948
1380
|
}
|
|
949
1381
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1382
|
+
if (hasTextSelection(state.anchor.element)) {
|
|
1383
|
+
onSelection(state);
|
|
1384
|
+
} else {
|
|
1385
|
+
toggleOption(state, state.active);
|
|
954
1386
|
}
|
|
955
|
-
|
|
956
|
-
(state.filter.element ?? state.anchor).focus();
|
|
957
1387
|
}
|
|
958
1388
|
|
|
959
|
-
function
|
|
960
|
-
|
|
961
|
-
|
|
1389
|
+
function onEscape(state: OuiComboboxState): void {
|
|
1390
|
+
state.floatable.hide();
|
|
1391
|
+
}
|
|
962
1392
|
|
|
1393
|
+
function onFilter(
|
|
1394
|
+
type: InputEvent['inputType'],
|
|
1395
|
+
state: OuiComboboxState,
|
|
1396
|
+
manual?: boolean,
|
|
1397
|
+
emit?: boolean,
|
|
1398
|
+
): void {
|
|
1399
|
+
if (state.disabled || state.readonly) {
|
|
963
1400
|
return;
|
|
964
1401
|
}
|
|
965
1402
|
|
|
966
|
-
|
|
1403
|
+
const diff = state.filter.value !== state.filter.element.value;
|
|
1404
|
+
|
|
1405
|
+
if (!state.autocomplete.inline && state.filter.element.value.length > 0 && !(manual ?? false)) {
|
|
967
1406
|
state.floatable.show();
|
|
1407
|
+
}
|
|
968
1408
|
|
|
969
|
-
|
|
1409
|
+
const option = state.autocomplete.none
|
|
1410
|
+
? (state.active ?? state.options.all[0])
|
|
1411
|
+
: filterOptions(state);
|
|
1412
|
+
|
|
1413
|
+
let update: OuiComboboxPreview | undefined;
|
|
1414
|
+
|
|
1415
|
+
if (type.includes('delete')) {
|
|
1416
|
+
update = 'none';
|
|
1417
|
+
} else if (type.includes('insert')) {
|
|
1418
|
+
update = 'typed';
|
|
970
1419
|
}
|
|
971
1420
|
|
|
972
|
-
|
|
973
|
-
|
|
1421
|
+
if (update != null) {
|
|
1422
|
+
if (state.autocomplete.none) {
|
|
1423
|
+
applySelection({state, added: false, emit: (emit ?? true) && diff});
|
|
1424
|
+
} else {
|
|
1425
|
+
updatePreview(update, state, option);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
974
1428
|
|
|
975
|
-
|
|
976
|
-
|
|
1429
|
+
if (diff) {
|
|
1430
|
+
state.filter.value = state.filter.element.value;
|
|
1431
|
+
|
|
1432
|
+
dispatch(state.host, 'input');
|
|
1433
|
+
}
|
|
977
1434
|
}
|
|
978
1435
|
|
|
979
|
-
function
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1436
|
+
function onFocusout(state: OuiComboboxState): void {
|
|
1437
|
+
if (
|
|
1438
|
+
state.disabled ||
|
|
1439
|
+
state.readonly ||
|
|
1440
|
+
state.type === 'select' ||
|
|
1441
|
+
state.host === document.activeElement ||
|
|
1442
|
+
state.host.contains(document.activeElement)
|
|
1443
|
+
) {
|
|
984
1444
|
return;
|
|
985
1445
|
}
|
|
986
1446
|
|
|
1447
|
+
const option = filterOption(state, state.filter.element.value, true);
|
|
1448
|
+
|
|
1449
|
+
applySelection({
|
|
1450
|
+
option,
|
|
1451
|
+
state,
|
|
1452
|
+
added: false,
|
|
1453
|
+
selected: option == null ? [] : [option],
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function onInput(event: InputEvent): void {
|
|
1458
|
+
const combobox = findAncestor(event, TAGNAME);
|
|
1459
|
+
const state = states.map.get(combobox!);
|
|
1460
|
+
|
|
1461
|
+
if (combobox != null && state != null && event.target === state.filter.element) {
|
|
1462
|
+
onFilter(event.inputType, state);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function onKeydown(event: KeyboardEvent): void {
|
|
987
1467
|
const combobox = findAncestor(event, TAGNAME);
|
|
988
|
-
const state = states.get(combobox!);
|
|
1468
|
+
const state = states.map.get(combobox!);
|
|
989
1469
|
|
|
990
1470
|
if (combobox == null || state == null) {
|
|
991
1471
|
return;
|
|
992
1472
|
}
|
|
993
1473
|
|
|
994
|
-
const
|
|
1474
|
+
const item = findAncestor(event, SELECTOR_SELECTION_ITEM);
|
|
995
1475
|
|
|
996
|
-
if (
|
|
997
|
-
|
|
1476
|
+
if (item instanceof HTMLElement) {
|
|
1477
|
+
onKeydownSelection(event, state, item);
|
|
1478
|
+
|
|
1479
|
+
return;
|
|
998
1480
|
}
|
|
1481
|
+
|
|
1482
|
+
onKeydownFilter(event, state);
|
|
999
1483
|
}
|
|
1000
1484
|
|
|
1001
|
-
function
|
|
1002
|
-
|
|
1003
|
-
combobox: OuiComboboxElement,
|
|
1004
|
-
state: OuiComboboxState,
|
|
1005
|
-
): void {
|
|
1485
|
+
function onKeydownFilter(event: KeyboardEvent, state: OuiComboboxState): void {
|
|
1486
|
+
const ignore = state.disabled || state.readonly;
|
|
1006
1487
|
const isSelect = state.type === 'select';
|
|
1007
1488
|
|
|
1008
|
-
let preventDefault =
|
|
1489
|
+
let preventDefault = ignore;
|
|
1009
1490
|
|
|
1010
1491
|
switch (true) {
|
|
1011
|
-
case event.key === '
|
|
1012
|
-
|
|
1492
|
+
case event.key === ' ': {
|
|
1493
|
+
if (!state.disabled) {
|
|
1494
|
+
state.floatable.show();
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1013
1497
|
break;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
case event.key === 'Enter': {
|
|
1501
|
+
preventDefault = state.floatable.open;
|
|
1502
|
+
|
|
1503
|
+
if (preventDefault) {
|
|
1504
|
+
onEnter(state);
|
|
1505
|
+
} else if (!ignore && state.type === 'select') {
|
|
1506
|
+
state.floatable.show();
|
|
1507
|
+
} else if (!ignore) {
|
|
1508
|
+
state.internals.form?.requestSubmit();
|
|
1509
|
+
}
|
|
1014
1510
|
|
|
1015
|
-
case event.key === 'Escape':
|
|
1016
|
-
onEscape(state);
|
|
1017
1511
|
break;
|
|
1512
|
+
}
|
|
1018
1513
|
|
|
1019
|
-
case
|
|
1514
|
+
case event.key === 'Escape': {
|
|
1020
1515
|
preventDefault = false;
|
|
1021
1516
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1517
|
+
onEscape(state);
|
|
1518
|
+
|
|
1519
|
+
break;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
case !isSelect && KEYS_SELECTION.has(event.key): {
|
|
1523
|
+
if (!ignore && hasTextSelection(state.anchor.element)) {
|
|
1524
|
+
onSelection(state);
|
|
1024
1525
|
}
|
|
1025
1526
|
|
|
1026
1527
|
break;
|
|
1027
1528
|
}
|
|
1028
1529
|
|
|
1029
|
-
case KEYS_NAVIGATION.has(event.key):
|
|
1030
|
-
|
|
1530
|
+
case KEYS_NAVIGATION.has(event.key): {
|
|
1531
|
+
if (!state.disabled) {
|
|
1532
|
+
onNavigation(event, state);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1031
1535
|
break;
|
|
1536
|
+
}
|
|
1032
1537
|
|
|
1033
1538
|
default: {
|
|
1034
1539
|
preventDefault = false;
|
|
1035
1540
|
|
|
1036
1541
|
if (event.key === 'Tab') {
|
|
1037
|
-
onTab(
|
|
1542
|
+
onTab(state);
|
|
1038
1543
|
}
|
|
1039
1544
|
|
|
1040
1545
|
break;
|
|
@@ -1047,53 +1552,71 @@ function onInputKeydown(
|
|
|
1047
1552
|
}
|
|
1048
1553
|
}
|
|
1049
1554
|
|
|
1050
|
-
function
|
|
1555
|
+
function onKeydownSelection(
|
|
1051
1556
|
event: KeyboardEvent,
|
|
1052
|
-
combobox: OuiComboboxElement,
|
|
1053
1557
|
state: OuiComboboxState,
|
|
1558
|
+
item: HTMLElement,
|
|
1054
1559
|
): void {
|
|
1055
|
-
|
|
1056
|
-
|
|
1560
|
+
if (KEYS_ACTIVATION.has(event.key)) {
|
|
1561
|
+
event.preventDefault();
|
|
1057
1562
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1563
|
+
onRemoveSelection(state, item);
|
|
1564
|
+
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (state.disabled || state.selection == null) {
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
const {length} = state.selection.items;
|
|
1573
|
+
|
|
1574
|
+
const index = state.selection.items.findIndex(selection => selection.button === item);
|
|
1575
|
+
|
|
1576
|
+
let next: number | undefined;
|
|
1577
|
+
|
|
1578
|
+
switch (event.key) {
|
|
1579
|
+
case 'ArrowDown':
|
|
1580
|
+
state.anchor.element.focus();
|
|
1581
|
+
state.anchor.element.click();
|
|
1582
|
+
return;
|
|
1583
|
+
|
|
1584
|
+
case 'ArrowRight':
|
|
1585
|
+
case 'ArrowLeft':
|
|
1586
|
+
next = clamp(index + (event.key === 'ArrowRight' ? 1 : -1), 0, length - 1, true);
|
|
1587
|
+
break;
|
|
1588
|
+
|
|
1589
|
+
case 'End':
|
|
1590
|
+
next = length - 1;
|
|
1591
|
+
break;
|
|
1592
|
+
|
|
1593
|
+
case 'Home':
|
|
1594
|
+
next = 0;
|
|
1595
|
+
break;
|
|
1596
|
+
|
|
1597
|
+
default:
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
if (next == null) {
|
|
1062
1602
|
return;
|
|
1063
1603
|
}
|
|
1064
1604
|
|
|
1065
1605
|
event.preventDefault();
|
|
1066
|
-
event.stopPropagation();
|
|
1067
1606
|
|
|
1068
|
-
|
|
1069
|
-
state.
|
|
1070
|
-
|
|
1071
|
-
state.filter.value = filter.value;
|
|
1072
|
-
}
|
|
1607
|
+
for (let index = 0; index < length; index += 1) {
|
|
1608
|
+
const {button} = state.selection.items[index];
|
|
1609
|
+
const selected = index === next;
|
|
1073
1610
|
|
|
1074
|
-
|
|
1075
|
-
const next = filterOptions(state, options);
|
|
1611
|
+
button.tabIndex = selected ? 0 : -1;
|
|
1076
1612
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
setNext('remove', combobox, state, options, next);
|
|
1613
|
+
if (selected) {
|
|
1614
|
+
button.focus();
|
|
1615
|
+
}
|
|
1081
1616
|
}
|
|
1082
1617
|
}
|
|
1083
1618
|
|
|
1084
|
-
function
|
|
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 {
|
|
1619
|
+
function onNavigation(event: KeyboardEvent, state: OuiComboboxState): void {
|
|
1097
1620
|
let skip = false;
|
|
1098
1621
|
|
|
1099
1622
|
if (!state.floatable.open) {
|
|
@@ -1102,7 +1625,7 @@ function onNavigation(
|
|
|
1102
1625
|
state.floatable.show();
|
|
1103
1626
|
}
|
|
1104
1627
|
|
|
1105
|
-
const
|
|
1628
|
+
const visible = getVisible(state);
|
|
1106
1629
|
|
|
1107
1630
|
if (visible.length === 0) {
|
|
1108
1631
|
return;
|
|
@@ -1145,360 +1668,571 @@ function onNavigation(
|
|
|
1145
1668
|
}
|
|
1146
1669
|
}
|
|
1147
1670
|
|
|
1148
|
-
|
|
1671
|
+
let preview: OuiComboboxPreview;
|
|
1672
|
+
|
|
1673
|
+
if (state.type === 'autocomplete' && (state.autocomplete.both || state.autocomplete.inline)) {
|
|
1674
|
+
preview = 'set';
|
|
1675
|
+
} else {
|
|
1676
|
+
preview = 'none';
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
updatePreview(preview, state, next ?? state.active ?? visible[0]);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function onRemoveSelection(state: OuiComboboxState, item: HTMLElement): void {
|
|
1683
|
+
if (state.disabled || state.readonly) {
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
const option = state.options.selected.find(
|
|
1688
|
+
option => option.id === getData(item, 'option', false),
|
|
1689
|
+
);
|
|
1690
|
+
|
|
1691
|
+
if (option != null) {
|
|
1692
|
+
toggleOption(state, option);
|
|
1693
|
+
}
|
|
1149
1694
|
}
|
|
1150
1695
|
|
|
1151
|
-
function onSelection(
|
|
1152
|
-
state.filter.value =
|
|
1696
|
+
function onSelection(state: OuiComboboxState): void {
|
|
1697
|
+
state.filter.value =
|
|
1698
|
+
state.anchor.element === state.filter.element ? state.filter.element.value : '';
|
|
1153
1699
|
|
|
1154
1700
|
filterOptions(state);
|
|
1155
1701
|
|
|
1156
|
-
|
|
1702
|
+
toggleOption(state, state.active);
|
|
1157
1703
|
}
|
|
1158
1704
|
|
|
1159
|
-
function onTab(
|
|
1160
|
-
|
|
1161
|
-
state.
|
|
1162
|
-
|
|
1705
|
+
function onTab(state: OuiComboboxState): void {
|
|
1706
|
+
setTimeout(() => {
|
|
1707
|
+
if (state.host === document.activeElement) {
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
if (
|
|
1712
|
+
state.selection?.container.contains(document.activeElement) ||
|
|
1713
|
+
!state.host.contains(document.activeElement)
|
|
1714
|
+
) {
|
|
1715
|
+
state.floatable.hide();
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1163
1718
|
}
|
|
1164
1719
|
|
|
1165
|
-
function
|
|
1166
|
-
|
|
1167
|
-
state: OuiComboboxState,
|
|
1168
|
-
options?: HTMLElement[],
|
|
1169
|
-
next?: HTMLElement,
|
|
1170
|
-
): void {
|
|
1171
|
-
const filter = state.filter.element ?? state.anchor;
|
|
1720
|
+
function setDisabled(state: OuiComboboxState, value: boolean): void {
|
|
1721
|
+
state.filter.element.readOnly = state.readonly || value;
|
|
1172
1722
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1723
|
+
setProperties('disabled', state, value);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function setFilter(state: OuiComboboxState, value: unknown): void {
|
|
1727
|
+
if (typeof value !== 'string') {
|
|
1728
|
+
return;
|
|
1175
1729
|
}
|
|
1176
1730
|
|
|
1177
|
-
const
|
|
1731
|
+
const type: InputEvent['inputType'] =
|
|
1732
|
+
value.length < state.filter.element.value.length ? 'deleteContentBackward' : 'insertText';
|
|
1178
1733
|
|
|
1179
|
-
|
|
1180
|
-
? (state.active ?? all[0])
|
|
1181
|
-
: (next ?? filterOptions(state, options));
|
|
1734
|
+
state.filter.element.value = value;
|
|
1182
1735
|
|
|
1183
|
-
|
|
1736
|
+
onFilter(type, state);
|
|
1184
1737
|
}
|
|
1185
1738
|
|
|
1186
|
-
function
|
|
1187
|
-
|
|
1188
|
-
|
|
1739
|
+
function setLimit(type: 'max' | 'min', state: OuiComboboxState, value?: unknown): void {
|
|
1740
|
+
let actual = value;
|
|
1741
|
+
|
|
1742
|
+
if (typeof actual === 'string') {
|
|
1743
|
+
actual = Number.parseInt(actual, 10);
|
|
1744
|
+
}
|
|
1189
1745
|
|
|
1190
|
-
function setLimit(
|
|
1191
|
-
type: 'max' | 'min',
|
|
1192
|
-
combobox: OuiComboboxElement,
|
|
1193
|
-
state: OuiComboboxState,
|
|
1194
|
-
value?: unknown,
|
|
1195
|
-
): void {
|
|
1196
1746
|
const updateLimit =
|
|
1197
|
-
typeof
|
|
1747
|
+
typeof actual === 'number' && !Number.isNaN(actual) && actual >= 0
|
|
1748
|
+
? actual !== state[type]
|
|
1749
|
+
: actual == null;
|
|
1198
1750
|
|
|
1199
1751
|
if (updateLimit) {
|
|
1200
|
-
state[type] =
|
|
1752
|
+
state[type] = actual as number | undefined;
|
|
1201
1753
|
|
|
1202
|
-
setAttribute(
|
|
1754
|
+
setAttribute(state.host, type, actual);
|
|
1203
1755
|
|
|
1204
1756
|
updateValidity(state);
|
|
1205
1757
|
}
|
|
1206
1758
|
}
|
|
1207
1759
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1760
|
+
/**
|
|
1761
|
+
* Set combobox localizations for a specific language
|
|
1762
|
+
*
|
|
1763
|
+
* @param language Language code
|
|
1764
|
+
* @param localizations Localizations for language
|
|
1765
|
+
*/
|
|
1766
|
+
export function setLocalizations(
|
|
1767
|
+
language: string,
|
|
1768
|
+
localizations: Partial<OuiComboboxLocalizations>,
|
|
1769
|
+
): void;
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* Set combobox localizations for one or more languages
|
|
1773
|
+
*
|
|
1774
|
+
* @param localizations Localizations for one or more languages
|
|
1775
|
+
*/
|
|
1776
|
+
export function setLocalizations(
|
|
1777
|
+
localizations: Record<string, Partial<OuiComboboxLocalizations>>,
|
|
1778
|
+
): void;
|
|
1779
|
+
|
|
1780
|
+
export function setLocalizations(
|
|
1781
|
+
first: string | Record<string, Partial<OuiComboboxLocalizations>>,
|
|
1782
|
+
second?: Partial<OuiComboboxLocalizations>,
|
|
1214
1783
|
): void {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
next?.scrollIntoView(scrollOptions);
|
|
1224
|
-
|
|
1225
|
-
// TODO: offset scroll to prevent options under headers
|
|
1226
|
-
|
|
1227
|
-
setValue(update, combobox, state, next);
|
|
1784
|
+
updateLocalizations(
|
|
1785
|
+
typeof first === 'string'
|
|
1786
|
+
? {
|
|
1787
|
+
[first]: second!,
|
|
1788
|
+
}
|
|
1789
|
+
: first,
|
|
1790
|
+
);
|
|
1228
1791
|
}
|
|
1229
1792
|
|
|
1230
1793
|
function setProperties(
|
|
1231
1794
|
key: 'disabled' | 'readonly' | 'required',
|
|
1232
|
-
property: 'disabled' | 'readOnly' | 'required',
|
|
1233
|
-
combobox: OuiComboboxElement,
|
|
1234
1795
|
state: OuiComboboxState,
|
|
1235
1796
|
value: boolean,
|
|
1236
1797
|
): void {
|
|
1237
1798
|
state[key] = value;
|
|
1238
1799
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1800
|
+
const elements = [state.host, state.anchor.element, state.filter.element];
|
|
1801
|
+
const {length} = elements;
|
|
1241
1802
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
}
|
|
1803
|
+
for (let index = 0; index < length; index += 1) {
|
|
1804
|
+
const element = elements[index];
|
|
1245
1805
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1806
|
+
setAria(
|
|
1807
|
+
element,
|
|
1808
|
+
key,
|
|
1809
|
+
key !== 'readonly' || !(element instanceof HTMLButtonElement) ? value : undefined,
|
|
1810
|
+
);
|
|
1248
1811
|
}
|
|
1249
1812
|
}
|
|
1250
1813
|
|
|
1251
|
-
function setReadonly(
|
|
1252
|
-
|
|
1814
|
+
function setReadonly(state: OuiComboboxState, value: boolean): void {
|
|
1815
|
+
state.filter.element.readOnly = state.disabled || value;
|
|
1816
|
+
|
|
1817
|
+
setProperties('readonly', state, value);
|
|
1253
1818
|
}
|
|
1254
1819
|
|
|
1255
|
-
function setRequired(
|
|
1256
|
-
setProperties('required',
|
|
1820
|
+
function setRequired(state: OuiComboboxState, value: boolean): void {
|
|
1821
|
+
setProperties('required', state, value);
|
|
1257
1822
|
updateValidity(state);
|
|
1258
1823
|
}
|
|
1259
1824
|
|
|
1260
|
-
function setValue(
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
if (state.disabled || state.readonly || (state.type === 'select' && update !== 'set')) {
|
|
1825
|
+
function setValue(state: OuiComboboxState, value: unknown, emit?: boolean): void {
|
|
1826
|
+
if (state.autocomplete.none) {
|
|
1827
|
+
state.filter.element.value = getString(value);
|
|
1828
|
+
|
|
1829
|
+
onFilter('insertText', state, true, emit);
|
|
1830
|
+
|
|
1267
1831
|
return;
|
|
1268
1832
|
}
|
|
1269
1833
|
|
|
1270
|
-
|
|
1271
|
-
let shouldUpdate: boolean;
|
|
1272
|
-
let values: string[];
|
|
1273
|
-
|
|
1274
|
-
if (update === 'set' || !(state.anchor instanceof HTMLInputElement)) {
|
|
1275
|
-
added = updateSelected(update, state, next);
|
|
1834
|
+
const option = filterOption(state, value);
|
|
1276
1835
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1836
|
+
if (option != null) {
|
|
1837
|
+
setValues(state, [value], emit);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1280
1840
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1841
|
+
function setValues(state: OuiComboboxState, value: unknown, emit?: boolean): void {
|
|
1842
|
+
if (!Array.isArray(value)) {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1284
1845
|
|
|
1285
|
-
|
|
1846
|
+
const values = (state.multiple ? value : value.slice(0, 1)).map(value =>
|
|
1847
|
+
getString(value).toLocaleLowerCase(),
|
|
1848
|
+
);
|
|
1286
1849
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1850
|
+
if (values.length === 0) {
|
|
1851
|
+
values.push('');
|
|
1852
|
+
}
|
|
1289
1853
|
|
|
1290
|
-
|
|
1854
|
+
if (state.autocomplete.none) {
|
|
1855
|
+
setValue(state, values[0], emit);
|
|
1291
1856
|
|
|
1292
|
-
|
|
1293
|
-
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1294
1859
|
|
|
1295
|
-
|
|
1296
|
-
selected = next;
|
|
1860
|
+
const {length} = state.options.all;
|
|
1297
1861
|
|
|
1298
|
-
|
|
1299
|
-
}
|
|
1300
|
-
} else {
|
|
1301
|
-
values = [state.autocomplete.none ? input : ''];
|
|
1302
|
-
}
|
|
1862
|
+
const selected: HTMLElement[] = [];
|
|
1303
1863
|
|
|
1304
|
-
|
|
1864
|
+
for (let index = 0; index < length; index += 1) {
|
|
1865
|
+
const option = state.options.all[index];
|
|
1305
1866
|
|
|
1306
|
-
|
|
1867
|
+
if (values.includes(getData(option, 'value', false)?.toLocaleLowerCase() ?? '')) {
|
|
1868
|
+
selected.push(option);
|
|
1869
|
+
}
|
|
1307
1870
|
}
|
|
1308
1871
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1872
|
+
applySelection({
|
|
1873
|
+
emit,
|
|
1874
|
+
selected,
|
|
1875
|
+
state,
|
|
1876
|
+
added: true,
|
|
1877
|
+
option: selected[0] ?? state.options.all[0],
|
|
1878
|
+
values: state.autocomplete.none ? values : undefined,
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function toggleOption(state: OuiComboboxState, option?: HTMLElement): void {
|
|
1883
|
+
if (option == null || (!state.multiple && state.options.selected.includes(option))) {
|
|
1884
|
+
state.floatable.hide();
|
|
1312
1885
|
|
|
1313
|
-
if (next == null) {
|
|
1314
1886
|
return;
|
|
1315
1887
|
}
|
|
1316
1888
|
|
|
1317
|
-
|
|
1318
|
-
updateSelect(update, state, next, added);
|
|
1319
|
-
} else if (update !== 'remove') {
|
|
1320
|
-
updateAutoComplete(update, state, next);
|
|
1321
|
-
}
|
|
1889
|
+
const selected = state.options.selected.slice();
|
|
1322
1890
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1891
|
+
let added = true;
|
|
1892
|
+
|
|
1893
|
+
if (state.multiple) {
|
|
1894
|
+
const index = selected.indexOf(option);
|
|
1895
|
+
|
|
1896
|
+
if (index === -1) {
|
|
1897
|
+
selected.push(option);
|
|
1898
|
+
} else {
|
|
1899
|
+
added = false;
|
|
1900
|
+
|
|
1901
|
+
selected.splice(index, 1);
|
|
1902
|
+
}
|
|
1903
|
+
} else {
|
|
1904
|
+
selected.splice(0, selected.length, option);
|
|
1325
1905
|
}
|
|
1326
1906
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1907
|
+
applySelection({
|
|
1908
|
+
added,
|
|
1909
|
+
option,
|
|
1910
|
+
selected,
|
|
1911
|
+
state,
|
|
1912
|
+
values: state.autocomplete.none && option != null ? [option.textContent] : undefined,
|
|
1329
1913
|
});
|
|
1914
|
+
|
|
1915
|
+
if (!state.multiple) {
|
|
1916
|
+
state.floatable.hide();
|
|
1917
|
+
}
|
|
1330
1918
|
}
|
|
1331
1919
|
|
|
1332
|
-
function
|
|
1333
|
-
if (!
|
|
1920
|
+
function updateAnchor(state: OuiComboboxState): void {
|
|
1921
|
+
if (!state.multiple) {
|
|
1334
1922
|
return;
|
|
1335
1923
|
}
|
|
1336
1924
|
|
|
1337
|
-
const
|
|
1338
|
-
getString(value).toLocaleLowerCase(),
|
|
1339
|
-
);
|
|
1925
|
+
const localization = getLocalization('selected', state);
|
|
1340
1926
|
|
|
1341
|
-
|
|
1927
|
+
setAria(
|
|
1928
|
+
state.anchor.element,
|
|
1929
|
+
'label',
|
|
1930
|
+
typeof localization === 'function'
|
|
1931
|
+
? localization(state.options.selected.length)
|
|
1932
|
+
: localization.replace('{amount}', state.options.selected.length.toString()),
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1342
1935
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1936
|
+
function updateAutocomplete(
|
|
1937
|
+
preview: OuiComboboxPreview,
|
|
1938
|
+
state: OuiComboboxState,
|
|
1939
|
+
active?: HTMLElement,
|
|
1940
|
+
): void {
|
|
1941
|
+
if (state.multiple) {
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1345
1944
|
|
|
1346
|
-
|
|
1945
|
+
const content = active?.textContent ?? state.filter.element.value;
|
|
1347
1946
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1947
|
+
if (
|
|
1948
|
+
preview === 'typed' &&
|
|
1949
|
+
!content.toLocaleLowerCase().startsWith(state.filter.element.value.toLocaleLowerCase())
|
|
1950
|
+
) {
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1350
1953
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1954
|
+
if (state.anchor.element instanceof HTMLInputElement) {
|
|
1955
|
+
const contentLength = content.length;
|
|
1956
|
+
const filterLength = state.filter.element.value.length;
|
|
1354
1957
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1958
|
+
if (preview === 'set' || preview === 'typed') {
|
|
1959
|
+
state.anchor.element.value = content;
|
|
1357
1960
|
|
|
1358
|
-
|
|
1961
|
+
state.anchor.element.setSelectionRange(
|
|
1962
|
+
preview === 'typed' ? filterLength : contentLength,
|
|
1963
|
+
contentLength,
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
} else {
|
|
1967
|
+
state.anchor.element.textContent = content;
|
|
1359
1968
|
}
|
|
1969
|
+
}
|
|
1360
1970
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1971
|
+
function updateForm(state: OuiComboboxState, values: string[], emit?: boolean): void {
|
|
1972
|
+
let value: string | FormData;
|
|
1363
1973
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1974
|
+
if (state.multiple) {
|
|
1975
|
+
const {length} = values;
|
|
1366
1976
|
|
|
1367
|
-
|
|
1368
|
-
setAria(option, 'selected', true);
|
|
1977
|
+
value = new FormData();
|
|
1369
1978
|
|
|
1370
|
-
|
|
1979
|
+
for (let index = 0; index < length; index += 1) {
|
|
1980
|
+
(value as FormData).append(state.name, values[index]);
|
|
1981
|
+
}
|
|
1982
|
+
} else {
|
|
1983
|
+
value = values[0];
|
|
1371
1984
|
}
|
|
1372
1985
|
|
|
1373
|
-
state.
|
|
1986
|
+
state.values.current = values;
|
|
1987
|
+
|
|
1988
|
+
state.internals.setFormValue(value, JSON.stringify(values));
|
|
1374
1989
|
|
|
1375
|
-
|
|
1990
|
+
updateAnchor(state);
|
|
1991
|
+
updateValidity(state);
|
|
1376
1992
|
|
|
1377
|
-
|
|
1993
|
+
if (emit ?? true) {
|
|
1994
|
+
dispatch(state.host, 'change');
|
|
1995
|
+
}
|
|
1378
1996
|
}
|
|
1379
1997
|
|
|
1380
|
-
function
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
next: HTMLElement,
|
|
1998
|
+
function updateLocalizations(
|
|
1999
|
+
values: Record<string, Partial<Record<string, string | GenericCallback>>>,
|
|
2000
|
+
destination?: Record<string, Partial<OuiComboboxLocalizations>>,
|
|
1384
2001
|
): void {
|
|
1385
|
-
if (
|
|
2002
|
+
if (typeof values !== 'object' || values == null) {
|
|
1386
2003
|
return;
|
|
1387
2004
|
}
|
|
1388
2005
|
|
|
1389
|
-
const
|
|
2006
|
+
const localizations = destination ?? LOCALIZATIONS.globals;
|
|
1390
2007
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
2008
|
+
const languages = Object.keys(values);
|
|
2009
|
+
const languagesLength = languages.length;
|
|
2010
|
+
|
|
2011
|
+
let updated = false;
|
|
2012
|
+
|
|
2013
|
+
for (let languagesIndex = 0; languagesIndex < languagesLength; languagesIndex += 1) {
|
|
2014
|
+
const language = languages[languagesIndex];
|
|
2015
|
+
const languageValue = values[language];
|
|
2016
|
+
|
|
2017
|
+
if (typeof languageValue !== 'object' || languageValue == null) {
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
const normalizedLanguage = language.toLowerCase();
|
|
2022
|
+
|
|
2023
|
+
localizations[normalizedLanguage] ??= {};
|
|
2024
|
+
|
|
2025
|
+
const keys = Object.keys(languageValue);
|
|
2026
|
+
const keysLength = keys.length;
|
|
2027
|
+
|
|
2028
|
+
for (let keysIndex = 0; keysIndex < keysLength; keysIndex += 1) {
|
|
2029
|
+
const key = keys[keysIndex];
|
|
2030
|
+
const keyValue = languageValue[key];
|
|
2031
|
+
|
|
2032
|
+
const normalizedKey = key.toLowerCase() as keyof OuiComboboxLocalizations;
|
|
2033
|
+
|
|
2034
|
+
if (
|
|
2035
|
+
LOCALIZATIONS.keys.has(normalizedKey) &&
|
|
2036
|
+
(typeof keyValue === 'string' || typeof keyValue === 'function')
|
|
2037
|
+
) {
|
|
2038
|
+
updated = true;
|
|
2039
|
+
|
|
2040
|
+
localizations[normalizedLanguage][normalizedKey] = keyValue as never;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
if (!updated) {
|
|
1395
2046
|
return;
|
|
1396
2047
|
}
|
|
1397
2048
|
|
|
1398
|
-
const
|
|
2049
|
+
for (const state of states.set) {
|
|
2050
|
+
updateAnchor(state);
|
|
2051
|
+
updateMessage(state, getVisible(state).length);
|
|
2052
|
+
updateSelectionItems(state);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function updateMessage(state: OuiComboboxState, visible: number): void {
|
|
2057
|
+
setAttribute(state.message, ATTRIBUTE_HIDDEN, visible > 0 ? '' : undefined);
|
|
1399
2058
|
|
|
1400
|
-
if (
|
|
1401
|
-
|
|
2059
|
+
if (visible === 0) {
|
|
2060
|
+
const localization = getLocalization('empty', state);
|
|
1402
2061
|
|
|
1403
|
-
state.
|
|
2062
|
+
state.message.textContent = typeof localization === 'function' ? localization() : localization;
|
|
1404
2063
|
} else {
|
|
1405
|
-
|
|
2064
|
+
const localization = getLocalization('results', state);
|
|
2065
|
+
|
|
2066
|
+
state.message.textContent =
|
|
2067
|
+
typeof localization === 'function'
|
|
2068
|
+
? localization(visible)
|
|
2069
|
+
: localization.replace('{amount}', visible.toString());
|
|
1406
2070
|
}
|
|
2071
|
+
|
|
2072
|
+
state.message.hidden = false;
|
|
1407
2073
|
}
|
|
1408
2074
|
|
|
1409
|
-
function
|
|
1410
|
-
|
|
2075
|
+
function updatePreview(
|
|
2076
|
+
preview: OuiComboboxPreview,
|
|
2077
|
+
state: OuiComboboxState,
|
|
2078
|
+
active?: HTMLElement,
|
|
2079
|
+
): void {
|
|
2080
|
+
const {length} = state.options.all;
|
|
1411
2081
|
|
|
1412
|
-
|
|
2082
|
+
for (let index = 0; index < length; index += 1) {
|
|
2083
|
+
const option = state.options.all[index];
|
|
1413
2084
|
|
|
1414
|
-
|
|
1415
|
-
formData.append(state.name, value);
|
|
2085
|
+
setAttribute(option, ATTRIBUTE_OPTION_ACTIVE, option === active ? '' : undefined);
|
|
1416
2086
|
}
|
|
1417
2087
|
|
|
1418
|
-
state.
|
|
2088
|
+
state.active = active;
|
|
1419
2089
|
|
|
1420
|
-
state.
|
|
2090
|
+
setAria(state.filter.element, 'activedescendant', state.floatable.open ? active?.id : undefined);
|
|
1421
2091
|
|
|
1422
|
-
|
|
2092
|
+
if (state.floatable.open) {
|
|
2093
|
+
active?.scrollIntoView(scrollOptions);
|
|
2094
|
+
}
|
|
1423
2095
|
|
|
1424
|
-
|
|
2096
|
+
if (preview !== 'none') {
|
|
2097
|
+
updateAutocomplete(preview, state, active);
|
|
2098
|
+
}
|
|
1425
2099
|
}
|
|
1426
2100
|
|
|
1427
|
-
function
|
|
1428
|
-
update: OuiComboboxUpdate,
|
|
2101
|
+
function updateSelection(
|
|
1429
2102
|
state: OuiComboboxState,
|
|
1430
|
-
|
|
1431
|
-
added
|
|
2103
|
+
active: HTMLElement | undefined,
|
|
2104
|
+
added: boolean,
|
|
2105
|
+
selected?: HTMLElement[],
|
|
1432
2106
|
): void {
|
|
1433
|
-
if (
|
|
2107
|
+
if (state.selection == null) {
|
|
1434
2108
|
return;
|
|
1435
2109
|
}
|
|
1436
2110
|
|
|
1437
|
-
|
|
1438
|
-
state.anchor.textContent = next.textContent ?? '';
|
|
2111
|
+
let options = active == null ? [] : [active];
|
|
1439
2112
|
|
|
1440
|
-
|
|
2113
|
+
if (selected != null) {
|
|
2114
|
+
options = selected;
|
|
2115
|
+
|
|
2116
|
+
const {length} = state.selection.items;
|
|
2117
|
+
|
|
2118
|
+
for (let index = 0; index < length; index += 1) {
|
|
2119
|
+
state.selection.items[index].element.remove();
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
state.selection.items.splice(0);
|
|
1441
2123
|
}
|
|
1442
2124
|
|
|
1443
|
-
if (added
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
2125
|
+
if (added) {
|
|
2126
|
+
createSelectionItems({
|
|
2127
|
+
options,
|
|
2128
|
+
state,
|
|
2129
|
+
container: state.selection.container,
|
|
2130
|
+
items: state.selection.items,
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
return;
|
|
1447
2134
|
}
|
|
1448
|
-
}
|
|
1449
2135
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
next?: HTMLElement,
|
|
1454
|
-
): boolean | undefined {
|
|
1455
|
-
const [all] = getOptions(state);
|
|
2136
|
+
let current = state.selection.items.findIndex(
|
|
2137
|
+
item => getData(item.button, 'option', false) === active?.id,
|
|
2138
|
+
);
|
|
1456
2139
|
|
|
1457
|
-
|
|
1458
|
-
|
|
2140
|
+
if (current === -1) {
|
|
2141
|
+
return;
|
|
1459
2142
|
}
|
|
1460
2143
|
|
|
1461
|
-
|
|
2144
|
+
const item = state.selection.items[current];
|
|
2145
|
+
|
|
2146
|
+
item.element.remove();
|
|
2147
|
+
|
|
2148
|
+
state.selection.items.splice(current, 1);
|
|
2149
|
+
|
|
2150
|
+
if (state.selection.items.length === 0) {
|
|
2151
|
+
state.anchor.element.focus();
|
|
2152
|
+
|
|
1462
2153
|
return;
|
|
1463
2154
|
}
|
|
1464
2155
|
|
|
1465
|
-
|
|
1466
|
-
|
|
2156
|
+
const {length} = state.selection.items;
|
|
2157
|
+
|
|
2158
|
+
if (current < 0) {
|
|
2159
|
+
current = 0;
|
|
2160
|
+
} else if (current >= length) {
|
|
2161
|
+
current = length - 1;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
for (let index = 0; index < length; index += 1) {
|
|
2165
|
+
const {button} = state.selection.items[index];
|
|
2166
|
+
|
|
2167
|
+
button.tabIndex = index === current ? 0 : -1;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
state.selection.items[current].button.focus();
|
|
2171
|
+
}
|
|
1467
2172
|
|
|
2173
|
+
function updateSelectionItems(state: OuiComboboxState): void {
|
|
2174
|
+
if (state.selection == null) {
|
|
1468
2175
|
return;
|
|
1469
2176
|
}
|
|
1470
2177
|
|
|
1471
|
-
|
|
2178
|
+
const localization = getLocalization('remove', state);
|
|
2179
|
+
|
|
2180
|
+
const {length} = state.selection.items;
|
|
2181
|
+
|
|
2182
|
+
for (let index = 0; index < length; index += 1) {
|
|
2183
|
+
const item = state.selection.items[index];
|
|
2184
|
+
|
|
2185
|
+
const option = state.options.selected.find(
|
|
2186
|
+
option => option.id === getData(item.button, 'option', false),
|
|
2187
|
+
);
|
|
2188
|
+
|
|
2189
|
+
if (option == null) {
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
setAria(
|
|
2194
|
+
item.button,
|
|
2195
|
+
'label',
|
|
2196
|
+
typeof localization === 'function'
|
|
2197
|
+
? localization(option?.textContent ?? '', getData(option, 'value', false) ?? '')
|
|
2198
|
+
: localization.replace('{label}', option?.textContent ?? ''),
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function updateTabindices(state: OuiComboboxState, active?: HTMLElement): void {
|
|
2204
|
+
if (active == null || state.selection == null) {
|
|
1472
2205
|
return;
|
|
1473
2206
|
}
|
|
1474
2207
|
|
|
1475
|
-
|
|
1476
|
-
|
|
2208
|
+
const next = state.selection.items.find(
|
|
2209
|
+
item => getData(item.button, 'option', false) === active.id,
|
|
2210
|
+
);
|
|
1477
2211
|
|
|
2212
|
+
if (next == null) {
|
|
1478
2213
|
return;
|
|
1479
2214
|
}
|
|
1480
2215
|
|
|
1481
|
-
const
|
|
1482
|
-
const added = index === -1;
|
|
2216
|
+
const {length} = state.selection.items;
|
|
1483
2217
|
|
|
1484
|
-
|
|
1485
|
-
state.
|
|
1486
|
-
|
|
1487
|
-
|
|
2218
|
+
for (let index = 0; index < length; index += 1) {
|
|
2219
|
+
const item = state.selection.items[index];
|
|
2220
|
+
|
|
2221
|
+
item.button.tabIndex = item === next ? 0 : -1;
|
|
1488
2222
|
}
|
|
1489
2223
|
|
|
1490
|
-
|
|
2224
|
+
next.button.focus();
|
|
1491
2225
|
}
|
|
1492
2226
|
|
|
1493
2227
|
function updateValidity(state: OuiComboboxState): void {
|
|
1494
2228
|
if (state.autocomplete.none) {
|
|
1495
|
-
const valueMissing = state.required && state.
|
|
2229
|
+
const valueMissing = state.required && state.filter.element.value.length === 0;
|
|
2230
|
+
|
|
2231
|
+
const localization = getLocalization('input', state);
|
|
1496
2232
|
|
|
1497
2233
|
state.internals.setValidity(
|
|
1498
|
-
{
|
|
1499
|
-
|
|
1500
|
-
},
|
|
1501
|
-
INVALID_INPUT,
|
|
2234
|
+
{valueMissing},
|
|
2235
|
+
typeof localization === 'function' ? localization() : localization,
|
|
1502
2236
|
);
|
|
1503
2237
|
|
|
1504
2238
|
state.internals.checkValidity();
|
|
@@ -1506,41 +2240,66 @@ function updateValidity(state: OuiComboboxState): void {
|
|
|
1506
2240
|
return;
|
|
1507
2241
|
}
|
|
1508
2242
|
|
|
1509
|
-
|
|
1510
|
-
|
|
2243
|
+
let error: 'badInput' | 'rangeOverflow' | 'rangeUnderflow' | undefined;
|
|
2244
|
+
let message: string | undefined;
|
|
2245
|
+
|
|
2246
|
+
switch (true) {
|
|
2247
|
+
case state.required && state.options.selected.length === 0: {
|
|
2248
|
+
error = 'badInput';
|
|
2249
|
+
|
|
2250
|
+
const localization = getLocalization('input', state);
|
|
2251
|
+
|
|
2252
|
+
message = typeof localization === 'function' ? localization() : localization;
|
|
2253
|
+
|
|
2254
|
+
break;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
case state.multiple &&
|
|
2258
|
+
typeof state.max === 'number' &&
|
|
2259
|
+
state.options.selected.length > state.max: {
|
|
2260
|
+
error = 'rangeOverflow';
|
|
1511
2261
|
|
|
1512
|
-
|
|
1513
|
-
state.multiple && typeof state.min === 'number' && state.selected.length < state.min;
|
|
2262
|
+
const localization = getLocalization('max', state);
|
|
1514
2263
|
|
|
1515
|
-
|
|
2264
|
+
message =
|
|
2265
|
+
typeof localization === 'function'
|
|
2266
|
+
? localization(state.max)
|
|
2267
|
+
: localization.replace('{max}', state.max.toString());
|
|
2268
|
+
|
|
2269
|
+
break;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
case state.multiple &&
|
|
2273
|
+
typeof state.min === 'number' &&
|
|
2274
|
+
state.options.selected.length < state.min: {
|
|
2275
|
+
error = 'rangeUnderflow';
|
|
2276
|
+
|
|
2277
|
+
const localization = getLocalization('min', state);
|
|
2278
|
+
|
|
2279
|
+
message =
|
|
2280
|
+
typeof localization === 'function'
|
|
2281
|
+
? localization(state.min)
|
|
2282
|
+
: localization.replace('{min}', state.min.toString());
|
|
2283
|
+
|
|
2284
|
+
break;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
default:
|
|
2288
|
+
break;
|
|
2289
|
+
}
|
|
1516
2290
|
|
|
1517
2291
|
state.internals.setValidity(
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
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
|
-
),
|
|
2292
|
+
error == null
|
|
2293
|
+
? {}
|
|
2294
|
+
: {
|
|
2295
|
+
[error]: true,
|
|
2296
|
+
},
|
|
2297
|
+
message,
|
|
1535
2298
|
);
|
|
1536
2299
|
|
|
1537
2300
|
state.internals.checkValidity();
|
|
1538
2301
|
}
|
|
1539
2302
|
|
|
1540
|
-
function updateWidth(state: OuiComboboxState): void {
|
|
1541
|
-
setStyle(state.content, PROPERTY_STYLE, `${state.anchor.getBoundingClientRect().width}px`);
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
2303
|
// #endregion
|
|
1545
2304
|
|
|
1546
2305
|
// #region Variables
|
|
@@ -1551,18 +2310,18 @@ const ATTRIBUTE_ANCHOR = `${TAGNAME}-anchor`;
|
|
|
1551
2310
|
|
|
1552
2311
|
const ATTRIBUTE_CONTENT = `${TAGNAME}-content`;
|
|
1553
2312
|
|
|
1554
|
-
const ATTRIBUTE_EMPTY = `${TAGNAME}-empty`;
|
|
1555
|
-
|
|
1556
2313
|
const ATTRIBUTE_FILTER = `${TAGNAME}-filter`;
|
|
1557
2314
|
|
|
1558
2315
|
const ATTRIBUTE_FILTER_INPUT = `${ATTRIBUTE_FILTER}-input`;
|
|
1559
2316
|
|
|
1560
|
-
const ATTRIBUTE_FILTER_LABEL = `${ATTRIBUTE_FILTER}-label`;
|
|
1561
|
-
|
|
1562
2317
|
const ATTRIBUTE_HEADER = `${TAGNAME}-header`;
|
|
1563
2318
|
|
|
2319
|
+
const ATTRIBUTE_HIDDEN = `${TAGNAME}-hidden`;
|
|
2320
|
+
|
|
1564
2321
|
const ATTRIBUTE_LIST = `${TAGNAME}-list`;
|
|
1565
2322
|
|
|
2323
|
+
const ATTRIBUTE_MESSAGE = `${TAGNAME}-message`;
|
|
2324
|
+
|
|
1566
2325
|
const ATTRIBUTE_OPTION = `${TAGNAME}-option`;
|
|
1567
2326
|
|
|
1568
2327
|
const ATTRIBUTE_OPTION_ACTIVE = `${TAGNAME}-option-active`;
|
|
@@ -1573,45 +2332,62 @@ const ATTRIBUTE_SELECTION_ITEM = `${TAGNAME}-selection-item`;
|
|
|
1573
2332
|
|
|
1574
2333
|
const AUTOCOMPLETES = new Set<OuiComboboxAutoCompleteType>(['both', 'inline', 'list', 'none']);
|
|
1575
2334
|
|
|
1576
|
-
const
|
|
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})';
|
|
2335
|
+
const KEYS_ACTIVATION = new Set([' ', 'Backspace', 'Delete', 'Enter']);
|
|
1585
2336
|
|
|
1586
2337
|
const KEYS_NAVIGATION = new Set(['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp']);
|
|
1587
2338
|
|
|
1588
|
-
const KEYS_REMOVE = new Set(['Backspace', 'Delete']);
|
|
1589
|
-
|
|
1590
2339
|
const KEYS_SELECTION = new Set(['ArrowRight', 'End']);
|
|
1591
2340
|
|
|
2341
|
+
const LOCALIZATIONS: {
|
|
2342
|
+
defaults: OuiComboboxLocalizations;
|
|
2343
|
+
globals: Record<string, Partial<OuiComboboxLocalizations>>;
|
|
2344
|
+
keys: Set<keyof OuiComboboxLocalizations>;
|
|
2345
|
+
} = {
|
|
2346
|
+
defaults: {
|
|
2347
|
+
empty: 'No results found',
|
|
2348
|
+
input: 'Please enter a value',
|
|
2349
|
+
invalid: 'Please select an option from the list',
|
|
2350
|
+
max: max => `Please select fewer options (maximum is ${max})`,
|
|
2351
|
+
min: min => `Please select more options (minimum is ${min})`,
|
|
2352
|
+
remove: label => `Remove ${label}`,
|
|
2353
|
+
results: amount => `${amount} results available`,
|
|
2354
|
+
selected: amount => `${amount} selected`,
|
|
2355
|
+
},
|
|
2356
|
+
globals: {},
|
|
2357
|
+
keys: new Set<keyof OuiComboboxLocalizations>([
|
|
2358
|
+
'empty',
|
|
2359
|
+
'input',
|
|
2360
|
+
'invalid',
|
|
2361
|
+
'max',
|
|
2362
|
+
'min',
|
|
2363
|
+
'remove',
|
|
2364
|
+
'results',
|
|
2365
|
+
'selected',
|
|
2366
|
+
]),
|
|
2367
|
+
};
|
|
2368
|
+
|
|
1592
2369
|
const MESSAGE_LABEL = `<${TAGNAME}> must contain a <label> element for the <select> element`;
|
|
1593
2370
|
|
|
1594
2371
|
const MESSAGE_SELECT = `<${TAGNAME}> must contain a valid <select> element`;
|
|
1595
2372
|
|
|
1596
|
-
const PROPERTY_STYLE = '--oui-combobox-width';
|
|
1597
|
-
|
|
1598
2373
|
const SELECTOR_ANCHOR = `[${ATTRIBUTE_ANCHOR}]`;
|
|
1599
2374
|
|
|
1600
|
-
const
|
|
2375
|
+
const SELECTOR_SELECTION_ITEM = `[${ATTRIBUTE_SELECTION_ITEM}]`;
|
|
1601
2376
|
|
|
1602
2377
|
const SELECTOR_OPTION = `[${ATTRIBUTE_OPTION}]`;
|
|
1603
2378
|
|
|
1604
2379
|
const TYPES = new Set<OuiComboboxType>(['autocomplete', 'select']);
|
|
1605
2380
|
|
|
1606
|
-
const comboboxes = new WeakMap<HTMLElement, OuiComboboxElement>();
|
|
1607
|
-
|
|
1608
2381
|
const scrollOptions: ScrollIntoViewOptions = {
|
|
1609
|
-
behavior: '
|
|
2382
|
+
behavior: 'auto',
|
|
1610
2383
|
block: 'nearest',
|
|
1611
2384
|
inline: 'nearest',
|
|
1612
2385
|
};
|
|
1613
2386
|
|
|
1614
|
-
const states =
|
|
2387
|
+
const states = {
|
|
2388
|
+
map: new WeakMap<HTMLElement, OuiComboboxState>(),
|
|
2389
|
+
set: new Set<OuiComboboxState>(),
|
|
2390
|
+
};
|
|
1615
2391
|
|
|
1616
2392
|
let index = 0;
|
|
1617
2393
|
|
|
@@ -1619,13 +2395,13 @@ let index = 0;
|
|
|
1619
2395
|
|
|
1620
2396
|
// #region Initialization
|
|
1621
2397
|
|
|
1622
|
-
on(document, 'click',
|
|
2398
|
+
on(document, 'click', onClickDocument);
|
|
1623
2399
|
|
|
1624
|
-
on(document, '
|
|
2400
|
+
on(document, 'input', onInput, {
|
|
1625
2401
|
passive: false,
|
|
1626
2402
|
});
|
|
1627
2403
|
|
|
1628
|
-
on(document, '
|
|
2404
|
+
on(document, 'keydown', onKeydown, {
|
|
1629
2405
|
passive: false,
|
|
1630
2406
|
});
|
|
1631
2407
|
|