@oscarpalmer/oui 0.31.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/badge.css +1 -5
- package/dist/css/combobox.css +164 -28
- package/dist/js/index.d.mts +643 -14
- package/dist/js/index.mjs +4875 -18
- package/package.json +5 -5
- package/src/css/combobox.scss +171 -25
- package/src/css/mixins.scss +8 -16
- package/src/js/combobox.ts +1682 -751
- package/src/js/internal/navigable.ts +10 -3
- package/src/js/menu.ts +1 -1
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,418 +320,668 @@ 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
|
+
}
|
|
203
351
|
|
|
204
|
-
|
|
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);
|
|
403
|
+
|
|
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
|
-
type
|
|
209
|
-
|
|
412
|
+
type OuiComboboxFilter = {
|
|
413
|
+
element: HTMLInputElement;
|
|
414
|
+
value: string;
|
|
210
415
|
};
|
|
211
416
|
|
|
212
|
-
type
|
|
417
|
+
type OuiComboboxGroup = {
|
|
418
|
+
element: HTMLElement;
|
|
419
|
+
header: HTMLElement;
|
|
420
|
+
options: HTMLElement[];
|
|
421
|
+
};
|
|
422
|
+
|
|
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
|
+
};
|
|
213
455
|
|
|
214
456
|
type OuiComboboxState = {
|
|
215
457
|
active?: HTMLElement;
|
|
216
|
-
anchor:
|
|
458
|
+
anchor: OuiComboboxAnchor;
|
|
217
459
|
autocomplete: OuiComboboxAutoComplete;
|
|
218
460
|
content: HTMLElement;
|
|
219
461
|
disabled: boolean;
|
|
220
|
-
|
|
221
|
-
filter: string;
|
|
462
|
+
filter: OuiComboboxFilter;
|
|
222
463
|
floatable: OuiFloatable;
|
|
464
|
+
groups?: OuiComboboxGroup[];
|
|
465
|
+
host: OuiComboboxElement;
|
|
223
466
|
id: string;
|
|
224
467
|
internals: ElementInternals;
|
|
225
|
-
label:
|
|
468
|
+
label: OuiComboboxLabel;
|
|
226
469
|
listeners: RemovableEventListener[];
|
|
470
|
+
localizations: Record<string, Partial<OuiComboboxLocalizations>>;
|
|
227
471
|
max?: number;
|
|
228
472
|
min?: number;
|
|
473
|
+
message: HTMLElement;
|
|
229
474
|
name: string;
|
|
230
475
|
multiple: boolean;
|
|
476
|
+
options: OuiComboboxOptions;
|
|
231
477
|
readonly: boolean;
|
|
232
478
|
required: boolean;
|
|
233
479
|
select: HTMLSelectElement;
|
|
234
|
-
|
|
235
|
-
touched: boolean;
|
|
480
|
+
selection?: OuiComboboxSelection;
|
|
236
481
|
type: OuiComboboxType;
|
|
237
|
-
values:
|
|
482
|
+
values: OuiComboboxValues;
|
|
238
483
|
};
|
|
239
484
|
|
|
240
485
|
type OuiComboboxType = 'autocomplete' | 'select';
|
|
241
486
|
|
|
242
|
-
type
|
|
487
|
+
type OuiComboboxValues = {
|
|
488
|
+
current: string[];
|
|
489
|
+
initial: string[];
|
|
490
|
+
};
|
|
243
491
|
|
|
244
492
|
// #endregion
|
|
245
493
|
|
|
246
494
|
// #region Functions
|
|
247
495
|
|
|
248
|
-
function
|
|
249
|
-
state
|
|
250
|
-
options?: HTMLElement[],
|
|
251
|
-
clear?: boolean,
|
|
252
|
-
): HTMLElement | undefined {
|
|
253
|
-
if (clear || state.autocomplete.none) {
|
|
254
|
-
state.filter = '';
|
|
255
|
-
}
|
|
496
|
+
function applySelection(parameters: ApplySelectionParameters): void {
|
|
497
|
+
const {added, emit, option, selected, state} = parameters;
|
|
256
498
|
|
|
257
|
-
const
|
|
499
|
+
const {length} = state.options.all;
|
|
258
500
|
|
|
259
|
-
|
|
260
|
-
|
|
501
|
+
for (let index = 0; index < length; index += 1) {
|
|
502
|
+
const option = state.options.all[index];
|
|
261
503
|
|
|
262
|
-
|
|
504
|
+
setAria(option, 'selected', (selected ?? state.options.selected).includes(option));
|
|
505
|
+
}
|
|
263
506
|
|
|
264
|
-
|
|
265
|
-
|
|
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) ?? ''));
|
|
266
512
|
|
|
267
|
-
|
|
513
|
+
if (values.length === 0) {
|
|
514
|
+
values.push('');
|
|
515
|
+
}
|
|
268
516
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
517
|
+
if (selected != null) {
|
|
518
|
+
state.options.selected = selected;
|
|
272
519
|
}
|
|
273
520
|
|
|
274
|
-
|
|
521
|
+
const update =
|
|
522
|
+
values.length === state.values.current.length
|
|
523
|
+
? values.some(value => !state.values.current.includes(value))
|
|
524
|
+
: true;
|
|
275
525
|
|
|
276
|
-
if (
|
|
277
|
-
return
|
|
526
|
+
if (!update) {
|
|
527
|
+
return;
|
|
278
528
|
}
|
|
529
|
+
|
|
530
|
+
updateForm(state, values, emit);
|
|
531
|
+
updatePreview('set', state, option);
|
|
532
|
+
updateSelection(state, option, added, selected);
|
|
279
533
|
}
|
|
280
534
|
|
|
281
|
-
function
|
|
282
|
-
const
|
|
283
|
-
const {length} = options;
|
|
535
|
+
function attachCombobox(state: OuiComboboxState): void {
|
|
536
|
+
const {anchor, content, filter, label, listeners, select} = state;
|
|
284
537
|
|
|
285
|
-
|
|
286
|
-
|
|
538
|
+
label.element.htmlFor = filter.element.id;
|
|
539
|
+
label.element.id = `${state.id}_label`;
|
|
287
540
|
|
|
288
|
-
|
|
289
|
-
option.id = `${prefix}_option_${index}`;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
541
|
+
select.hidden = true;
|
|
293
542
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
id: string,
|
|
297
|
-
label: HTMLLabelElement,
|
|
298
|
-
select: HTMLSelectElement,
|
|
299
|
-
autocomplete: OuiComboboxAutoComplete,
|
|
300
|
-
multiple: boolean,
|
|
301
|
-
): HTMLElement {
|
|
302
|
-
const anchor = type === 'autocomplete' ? getInput(id, select) : getBox(id, select, multiple);
|
|
543
|
+
select.insertAdjacentElement('afterend', anchor.container ?? anchor.element);
|
|
544
|
+
(anchor.container ?? anchor.element).insertAdjacentElement('afterend', content);
|
|
303
545
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
labelledby: label.id,
|
|
546
|
+
select.remove();
|
|
547
|
+
|
|
548
|
+
state.floatable = createEmbeddedFloatable(anchor.container ?? anchor.element, content, {
|
|
549
|
+
position: 'below-start',
|
|
550
|
+
reusable: false,
|
|
310
551
|
});
|
|
311
552
|
|
|
312
|
-
|
|
553
|
+
state.floatable.update();
|
|
313
554
|
|
|
314
|
-
|
|
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);
|
|
315
566
|
|
|
316
|
-
|
|
317
|
-
|
|
567
|
+
setAria(
|
|
568
|
+
filter.element,
|
|
569
|
+
'activedescendant',
|
|
570
|
+
state.floatable.open ? state.active?.id : undefined,
|
|
571
|
+
);
|
|
318
572
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
): Value {
|
|
325
|
-
const value = getAttribute(element, attribute);
|
|
573
|
+
if (state.floatable.open) {
|
|
574
|
+
filter.element.focus();
|
|
575
|
+
}
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
326
578
|
|
|
327
|
-
|
|
579
|
+
states.map.set(state.host, state);
|
|
580
|
+
states.map.set(state.content, state);
|
|
581
|
+
states.set.add(state);
|
|
328
582
|
}
|
|
329
583
|
|
|
330
|
-
function
|
|
331
|
-
|
|
584
|
+
function createAnchor(parameters: CreatAnchorParameters): CreateAnchor {
|
|
585
|
+
const {autocomplete, id, label, select, type} = parameters;
|
|
332
586
|
|
|
333
|
-
|
|
334
|
-
|
|
587
|
+
let container: HTMLElement | undefined;
|
|
588
|
+
let element: HTMLElement;
|
|
589
|
+
let selection: OuiComboboxSelection | undefined;
|
|
590
|
+
|
|
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
|
+
});
|
|
335
599
|
} else {
|
|
336
|
-
|
|
600
|
+
({container, element, selection} = createButton(parameters));
|
|
337
601
|
}
|
|
338
602
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
inline: value === 'inline',
|
|
343
|
-
list: value === 'list',
|
|
344
|
-
none: value === 'none',
|
|
345
|
-
};
|
|
603
|
+
setAttribute(container ?? element, ATTRIBUTE_ANCHOR, '');
|
|
604
|
+
|
|
605
|
+
return {container, element, selection};
|
|
346
606
|
}
|
|
347
607
|
|
|
348
|
-
function
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
608
|
+
function createButton(parameters: CreatAnchorParameters): CreateAnchor {
|
|
609
|
+
const {id, label, multiple, select} = parameters;
|
|
610
|
+
|
|
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
|
+
},
|
|
353
622
|
});
|
|
354
623
|
|
|
355
|
-
if (multiple) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
{
|
|
359
|
-
// tabIndex: 0, TODO: handle navigation
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
[ATTRIBUTE_SELECTION]: '',
|
|
363
|
-
},
|
|
364
|
-
);
|
|
624
|
+
if (!multiple) {
|
|
625
|
+
return {element};
|
|
626
|
+
}
|
|
365
627
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
628
|
+
const container = createElement('div');
|
|
629
|
+
const selection = createSelection(id, select);
|
|
369
630
|
|
|
370
|
-
|
|
371
|
-
}
|
|
631
|
+
container.append(selection.container, element);
|
|
372
632
|
|
|
373
|
-
return
|
|
633
|
+
return {container, element, selection};
|
|
374
634
|
}
|
|
375
635
|
|
|
376
|
-
function
|
|
377
|
-
id
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
): [HTMLElement, HTMLElement[], HTMLElement] {
|
|
383
|
-
const content = createElement(
|
|
384
|
-
'div',
|
|
385
|
-
{
|
|
386
|
-
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,
|
|
387
642
|
},
|
|
388
|
-
{
|
|
643
|
+
attribute: {
|
|
389
644
|
[ATTRIBUTE_CONTENT]: '',
|
|
390
645
|
popover: '',
|
|
391
|
-
tabindex: -1,
|
|
392
|
-
'aria-labelledby': label.id,
|
|
393
|
-
},
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
const list = createElement(
|
|
397
|
-
'ul',
|
|
398
|
-
{
|
|
399
|
-
role: 'listbox',
|
|
400
646
|
},
|
|
401
|
-
{
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
'aria-multiselectable': multiple,
|
|
647
|
+
property: {
|
|
648
|
+
id: `${id}_content`,
|
|
649
|
+
tabIndex: -1,
|
|
405
650
|
},
|
|
406
|
-
);
|
|
651
|
+
});
|
|
407
652
|
|
|
408
|
-
const
|
|
653
|
+
const filter = createFilter({id, label, type, autocomplete: autocomplete.value});
|
|
654
|
+
const list = createList({autocomplete, id, label, multiple, select});
|
|
655
|
+
const message = createMessage();
|
|
409
656
|
|
|
410
|
-
|
|
657
|
+
if (filter != null) {
|
|
658
|
+
element.append(filter.container);
|
|
659
|
+
}
|
|
411
660
|
|
|
412
|
-
|
|
413
|
-
const option = options[index];
|
|
661
|
+
element.append(list.element, message);
|
|
414
662
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
{
|
|
418
|
-
id: option.id,
|
|
419
|
-
textContent: option.textContent,
|
|
420
|
-
},
|
|
421
|
-
{
|
|
422
|
-
[ATTRIBUTE_OPTION]: '',
|
|
423
|
-
[ATTRIBUTE_OPTION_ACTIVE]: index === 0 ? '' : undefined,
|
|
424
|
-
role: 'option',
|
|
425
|
-
value: option.value,
|
|
426
|
-
'aria-selected': !autocomplete.none && option.selected,
|
|
427
|
-
},
|
|
428
|
-
);
|
|
663
|
+
return {element, filter, list, message};
|
|
664
|
+
}
|
|
429
665
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
666
|
+
function createFilter(parameters: CreateFilterParameters): CreateFilter | undefined {
|
|
667
|
+
const {autocomplete, id, label, type} = parameters;
|
|
433
668
|
|
|
434
|
-
|
|
669
|
+
if (type === 'autocomplete') {
|
|
670
|
+
return;
|
|
435
671
|
}
|
|
436
672
|
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
hidden: true,
|
|
441
|
-
role: 'status',
|
|
442
|
-
textContent: 'No results found',
|
|
673
|
+
const container = createElement('div', {
|
|
674
|
+
attribute: {
|
|
675
|
+
[ATTRIBUTE_FILTER]: '',
|
|
443
676
|
},
|
|
444
|
-
|
|
445
|
-
[ATTRIBUTE_EMPTY]: '',
|
|
446
|
-
'aria-live': 'polite',
|
|
447
|
-
},
|
|
448
|
-
);
|
|
449
|
-
|
|
450
|
-
content.append(list, empty);
|
|
451
|
-
|
|
452
|
-
return [content, selected, empty];
|
|
453
|
-
}
|
|
677
|
+
});
|
|
454
678
|
|
|
455
|
-
|
|
456
|
-
if (values.length === 0 || !state.multiple) {
|
|
457
|
-
return values[0];
|
|
458
|
-
}
|
|
679
|
+
const element = createInput({autocomplete, id, label, type, value: ''});
|
|
459
680
|
|
|
460
|
-
|
|
681
|
+
setAttribute(element, ATTRIBUTE_FILTER_INPUT, '');
|
|
461
682
|
|
|
462
|
-
|
|
463
|
-
formValues.push(`${state.name}=${value}`);
|
|
464
|
-
}
|
|
683
|
+
container.append(element);
|
|
465
684
|
|
|
466
|
-
return
|
|
685
|
+
return {container, element};
|
|
467
686
|
}
|
|
468
687
|
|
|
469
|
-
function
|
|
470
|
-
|
|
688
|
+
function createGroup(parameters: CreateGroupParameters): OuiComboboxGroup {
|
|
689
|
+
const {autocomplete, group, groupOptions, selected} = parameters;
|
|
471
690
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
691
|
+
const element = createElement('li', {
|
|
692
|
+
aria: {
|
|
693
|
+
label: group.label,
|
|
694
|
+
},
|
|
695
|
+
property: {
|
|
696
|
+
role: 'group',
|
|
697
|
+
},
|
|
698
|
+
});
|
|
480
699
|
|
|
481
|
-
const
|
|
482
|
-
|
|
700
|
+
const list = createElement('ul', {
|
|
701
|
+
property: {
|
|
702
|
+
role: 'none',
|
|
703
|
+
},
|
|
704
|
+
});
|
|
483
705
|
|
|
484
|
-
|
|
485
|
-
}
|
|
706
|
+
const header = createHeader(group);
|
|
486
707
|
|
|
487
|
-
|
|
488
|
-
index += 1;
|
|
708
|
+
element.append(header, list);
|
|
489
709
|
|
|
490
|
-
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
|
+
};
|
|
491
721
|
}
|
|
492
722
|
|
|
493
|
-
function
|
|
494
|
-
return createElement(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
id,
|
|
498
|
-
value: select.selectedOptions[0]?.textContent ?? '',
|
|
723
|
+
function createHeader(group: HTMLOptGroupElement): HTMLElement {
|
|
724
|
+
return createElement('div', {
|
|
725
|
+
aria: {
|
|
726
|
+
hidden: true,
|
|
499
727
|
},
|
|
500
|
-
{
|
|
501
|
-
[
|
|
502
|
-
autocomplete: 'off',
|
|
503
|
-
type: 'text',
|
|
728
|
+
attribute: {
|
|
729
|
+
[ATTRIBUTE_HEADER]: '',
|
|
504
730
|
},
|
|
505
|
-
|
|
731
|
+
property: {
|
|
732
|
+
id: group.id,
|
|
733
|
+
textContent: group.label,
|
|
734
|
+
},
|
|
735
|
+
});
|
|
506
736
|
}
|
|
507
737
|
|
|
508
|
-
function
|
|
509
|
-
|
|
510
|
-
value: unknown,
|
|
511
|
-
): [HTMLElement | undefined, HTMLElement[]] {
|
|
512
|
-
const [all] = getOptions(state);
|
|
513
|
-
const valueAsString = getString(value).toLocaleLowerCase();
|
|
738
|
+
function createInput(parameters: CreateInputParameters): HTMLInputElement {
|
|
739
|
+
const {autocomplete, id, label, type, value} = parameters;
|
|
514
740
|
|
|
515
|
-
|
|
741
|
+
return createElement('input', {
|
|
742
|
+
aria: {
|
|
743
|
+
autocomplete: autocomplete,
|
|
744
|
+
controls: `${id}_list`,
|
|
745
|
+
expanded: type === 'select',
|
|
746
|
+
haspopup: 'listbox',
|
|
747
|
+
labelledby: label.id,
|
|
748
|
+
},
|
|
749
|
+
attribute: {
|
|
750
|
+
autocorrect: 'off',
|
|
751
|
+
autocomplete: 'off',
|
|
752
|
+
form: '',
|
|
753
|
+
spellcheck: 'false',
|
|
754
|
+
type: 'text',
|
|
755
|
+
},
|
|
756
|
+
property: {
|
|
757
|
+
id,
|
|
758
|
+
value,
|
|
759
|
+
role: 'combobox',
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
}
|
|
516
763
|
|
|
517
|
-
|
|
764
|
+
function createList(parameters: CreateListParameters): CreateList {
|
|
765
|
+
const {autocomplete, id, label, multiple, select} = parameters;
|
|
518
766
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
767
|
+
const element = createElement('ul', {
|
|
768
|
+
aria: {
|
|
769
|
+
labelledby: label.id,
|
|
770
|
+
multiselectable: multiple,
|
|
771
|
+
},
|
|
772
|
+
attribute: {
|
|
773
|
+
[ATTRIBUTE_LIST]: '',
|
|
774
|
+
tabindex: -1,
|
|
775
|
+
},
|
|
776
|
+
property: {
|
|
777
|
+
id: `${id}_list`,
|
|
778
|
+
role: 'listbox',
|
|
779
|
+
},
|
|
780
|
+
});
|
|
522
781
|
|
|
523
|
-
|
|
782
|
+
const children = [...select.children];
|
|
783
|
+
const childrenLength = children.length;
|
|
784
|
+
|
|
785
|
+
const options: HTMLElement[] = [];
|
|
786
|
+
const selected: HTMLElement[] = [];
|
|
787
|
+
|
|
788
|
+
let groups: OuiComboboxGroup[] | undefined;
|
|
789
|
+
|
|
790
|
+
for (let childIndex = 0; childIndex < childrenLength; childIndex += 1) {
|
|
791
|
+
const child = children[childIndex];
|
|
792
|
+
|
|
793
|
+
if (child instanceof HTMLOptionElement) {
|
|
794
|
+
const option = createOption({
|
|
795
|
+
autocomplete,
|
|
796
|
+
index: childIndex,
|
|
797
|
+
option: child,
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
element.append(option);
|
|
801
|
+
options.push(option);
|
|
802
|
+
|
|
803
|
+
if (child.selected) {
|
|
804
|
+
selected.push(option);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
continue;
|
|
524
808
|
}
|
|
809
|
+
|
|
810
|
+
if (!(child instanceof HTMLOptGroupElement)) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const group = createGroup({
|
|
815
|
+
autocomplete,
|
|
816
|
+
selected,
|
|
817
|
+
group: child,
|
|
818
|
+
groupOptions: [...child.children] as HTMLOptionElement[],
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
groups ??= [];
|
|
822
|
+
|
|
823
|
+
element.append(group.element, ...group.options);
|
|
824
|
+
groups.push(group);
|
|
825
|
+
options.push(...group.options);
|
|
525
826
|
}
|
|
526
827
|
|
|
527
|
-
return
|
|
828
|
+
return {element, groups, options, selected};
|
|
528
829
|
}
|
|
529
830
|
|
|
530
|
-
function
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
831
|
+
function createMessage(): HTMLElement {
|
|
832
|
+
return createElement('div', {
|
|
833
|
+
aria: {
|
|
834
|
+
live: 'polite',
|
|
835
|
+
},
|
|
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: {
|
|
854
|
+
[ATTRIBUTE_OPTION]: '',
|
|
855
|
+
},
|
|
856
|
+
property: {
|
|
857
|
+
id: option.id,
|
|
858
|
+
role: 'option',
|
|
859
|
+
textContent: option.textContent,
|
|
860
|
+
},
|
|
861
|
+
});
|
|
534
862
|
|
|
535
|
-
|
|
863
|
+
setData(item, {
|
|
864
|
+
group,
|
|
865
|
+
value: option.value,
|
|
866
|
+
});
|
|
536
867
|
|
|
537
|
-
return
|
|
868
|
+
return item;
|
|
538
869
|
}
|
|
539
870
|
|
|
540
|
-
function
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
871
|
+
function createOptions(parameters: CreateOptionsParameters): HTMLElement[] {
|
|
872
|
+
const {autocomplete, group, list, options, selected} = parameters;
|
|
873
|
+
|
|
874
|
+
const items: HTMLElement[] = [];
|
|
875
|
+
|
|
876
|
+
for (let index = 0; index < options.length; index += 1) {
|
|
877
|
+
const option = options[index];
|
|
878
|
+
|
|
879
|
+
const item = createOption({autocomplete, group, index, option});
|
|
549
880
|
|
|
550
|
-
|
|
881
|
+
items.push(item);
|
|
551
882
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
return element === next;
|
|
883
|
+
if (option.selected) {
|
|
884
|
+
selected.push(item);
|
|
555
885
|
}
|
|
556
886
|
|
|
557
|
-
|
|
887
|
+
list.append(item);
|
|
558
888
|
}
|
|
559
889
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
890
|
+
return items;
|
|
891
|
+
}
|
|
563
892
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
893
|
+
function createSelection(id: string, select: HTMLSelectElement): OuiComboboxSelection {
|
|
894
|
+
const container = createElement('ul', {
|
|
895
|
+
attribute: {
|
|
896
|
+
[ATTRIBUTE_SELECTION]: '',
|
|
897
|
+
},
|
|
898
|
+
property: {
|
|
899
|
+
id: `${id}_selection`,
|
|
900
|
+
},
|
|
901
|
+
});
|
|
568
902
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
903
|
+
const options = [...select.selectedOptions];
|
|
904
|
+
|
|
905
|
+
const items: OuiComboboxSelectionItem[] = [];
|
|
906
|
+
|
|
907
|
+
createSelectionItems({container, items, options});
|
|
908
|
+
|
|
909
|
+
const {button} = items[0] ?? {};
|
|
572
910
|
|
|
573
|
-
|
|
911
|
+
if (button != null) {
|
|
912
|
+
button.tabIndex = 0;
|
|
574
913
|
}
|
|
575
914
|
|
|
576
|
-
return
|
|
915
|
+
return {
|
|
916
|
+
items,
|
|
917
|
+
container: container,
|
|
918
|
+
};
|
|
577
919
|
}
|
|
578
920
|
|
|
579
|
-
function
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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,
|
|
584
935
|
},
|
|
585
|
-
{
|
|
586
|
-
'
|
|
936
|
+
attribute: {
|
|
937
|
+
[ATTRIBUTE_SELECTION_ITEM]: '',
|
|
938
|
+
tabindex: -1,
|
|
587
939
|
},
|
|
588
|
-
|
|
940
|
+
data: {
|
|
941
|
+
option: option.id,
|
|
942
|
+
},
|
|
943
|
+
});
|
|
589
944
|
|
|
590
|
-
const
|
|
591
|
-
|
|
945
|
+
const icon = createElement('span', {
|
|
946
|
+
aria: {
|
|
947
|
+
hidden: true,
|
|
948
|
+
},
|
|
949
|
+
property: {
|
|
950
|
+
innerHTML: '×',
|
|
951
|
+
},
|
|
592
952
|
});
|
|
593
953
|
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
{
|
|
598
|
-
[ATTRIBUTE_SELECTION_ITEM]: '',
|
|
599
|
-
option: option.id,
|
|
954
|
+
const label = createElement('span', {
|
|
955
|
+
property: {
|
|
956
|
+
textContent: option.textContent,
|
|
600
957
|
},
|
|
601
|
-
);
|
|
958
|
+
});
|
|
602
959
|
|
|
603
|
-
|
|
960
|
+
button.append(icon, label);
|
|
961
|
+
element.append(button);
|
|
604
962
|
|
|
605
|
-
return
|
|
963
|
+
return {
|
|
964
|
+
button,
|
|
965
|
+
element,
|
|
966
|
+
};
|
|
606
967
|
}
|
|
607
968
|
|
|
608
|
-
function
|
|
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 {
|
|
609
985
|
const select = element.querySelector('select');
|
|
610
986
|
|
|
611
987
|
if (select == null || select.id === '') {
|
|
@@ -618,233 +994,340 @@ function getState(element: OuiComboboxElement, internals: ElementInternals): Oui
|
|
|
618
994
|
throw new Error(MESSAGE_LABEL);
|
|
619
995
|
}
|
|
620
996
|
|
|
621
|
-
select.hidden = true;
|
|
622
|
-
|
|
623
997
|
element.id = select.id;
|
|
624
998
|
|
|
625
999
|
setAttribute(element, 'name', select.name === '' ? select.id : select.name);
|
|
626
1000
|
|
|
627
|
-
const disabled = element
|
|
1001
|
+
const disabled = getBooleanAria(element, 'disabled') || select.disabled;
|
|
628
1002
|
const multiple = element.multiple || select.multiple;
|
|
629
|
-
const readonly = element
|
|
630
|
-
const required = element
|
|
1003
|
+
const readonly = getBooleanAria(element, 'readonly');
|
|
1004
|
+
const required = getBooleanAria(element, 'required') || select.required;
|
|
631
1005
|
|
|
632
1006
|
const id = getId();
|
|
633
1007
|
const type = multiple ? 'select' : getAttributeValue(element, 'type', TYPES, 'select');
|
|
634
1008
|
|
|
635
1009
|
const autocomplete = getAutoComplete(element, type === 'select');
|
|
636
1010
|
|
|
1011
|
+
const labelId = label.id;
|
|
1012
|
+
|
|
637
1013
|
label.htmlFor = id;
|
|
638
1014
|
label.id = `${id}_label`;
|
|
639
1015
|
|
|
640
|
-
generateIds(
|
|
641
|
-
|
|
642
|
-
const anchor = getAnchor(type, id, label, select, autocomplete, multiple);
|
|
1016
|
+
generateIds(id, select);
|
|
643
1017
|
|
|
644
|
-
const
|
|
1018
|
+
const anchor = createAnchor({autocomplete, id, label, multiple, select, type});
|
|
1019
|
+
const content = createContent({autocomplete, id, label, multiple, select, type});
|
|
645
1020
|
|
|
646
1021
|
const values =
|
|
647
|
-
selected.length === 0
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
position: 'below-start',
|
|
651
|
-
reusable: false,
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
floatable.update();
|
|
655
|
-
|
|
656
|
-
select.insertAdjacentElement('afterend', anchor);
|
|
657
|
-
anchor.insertAdjacentElement('afterend', content);
|
|
658
|
-
|
|
659
|
-
select.remove();
|
|
1022
|
+
content.list.selected.length === 0
|
|
1023
|
+
? ['']
|
|
1024
|
+
: content.list.selected.map(option => getData(option, 'value', false) ?? '');
|
|
660
1025
|
|
|
661
1026
|
const state: OuiComboboxState = {
|
|
662
|
-
anchor,
|
|
663
1027
|
autocomplete,
|
|
664
|
-
content,
|
|
665
1028
|
disabled,
|
|
666
|
-
empty,
|
|
667
|
-
floatable,
|
|
668
1029
|
id,
|
|
669
1030
|
internals,
|
|
670
|
-
label,
|
|
671
1031
|
multiple,
|
|
672
1032
|
readonly,
|
|
673
1033
|
required,
|
|
674
1034
|
select,
|
|
675
|
-
selected,
|
|
676
1035
|
type,
|
|
677
|
-
|
|
678
|
-
|
|
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,
|
|
1052
|
+
},
|
|
679
1053
|
listeners: [],
|
|
1054
|
+
localizations: {},
|
|
1055
|
+
message: content.message,
|
|
680
1056
|
name: element.name,
|
|
681
|
-
|
|
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
|
+
},
|
|
682
1066
|
};
|
|
683
1067
|
|
|
684
|
-
state
|
|
685
|
-
on(anchor, 'blur', () => {
|
|
686
|
-
state.touched = true;
|
|
1068
|
+
attachCombobox(state);
|
|
687
1069
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
on(content, 'toggle', () => {
|
|
694
|
-
setAria(anchor, {
|
|
695
|
-
activedescendant: floatable.open ? state.active?.id : undefined,
|
|
696
|
-
expanded: floatable.open,
|
|
697
|
-
});
|
|
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);
|
|
698
1075
|
|
|
699
|
-
|
|
700
|
-
updateWidth(state);
|
|
701
|
-
}
|
|
702
|
-
}),
|
|
703
|
-
);
|
|
1076
|
+
filterOptions(state);
|
|
704
1077
|
|
|
705
|
-
|
|
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);
|
|
706
1084
|
|
|
707
1085
|
return state;
|
|
708
1086
|
}
|
|
709
1087
|
|
|
710
|
-
function
|
|
711
|
-
|
|
712
|
-
}
|
|
1088
|
+
function detachCombobox(state: OuiComboboxState): void {
|
|
1089
|
+
const {content, floatable, host, label, select} = state;
|
|
713
1090
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
)
|
|
718
|
-
|
|
719
|
-
return;
|
|
1091
|
+
const listeners = state.listeners.splice(0);
|
|
1092
|
+
const {length} = listeners;
|
|
1093
|
+
|
|
1094
|
+
for (let index = 0; index < length; index += 1) {
|
|
1095
|
+
listeners[index]();
|
|
720
1096
|
}
|
|
721
1097
|
|
|
722
|
-
|
|
723
|
-
const state = states.get(combobox!);
|
|
1098
|
+
floatable.destroy();
|
|
724
1099
|
|
|
725
|
-
|
|
726
|
-
|
|
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
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function filterOptions(state: OuiComboboxState): HTMLElement | undefined {
|
|
1118
|
+
function filter(options: HTMLElement[]): boolean {
|
|
1119
|
+
const {length} = options;
|
|
1120
|
+
|
|
1121
|
+
let count = 0;
|
|
1122
|
+
|
|
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
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
return count === 0;
|
|
727
1137
|
}
|
|
728
1138
|
|
|
729
|
-
const
|
|
1139
|
+
const value = state.filter.element.value.toLocaleLowerCase();
|
|
1140
|
+
|
|
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];
|
|
730
1150
|
|
|
731
|
-
|
|
732
|
-
|
|
1151
|
+
element.hidden = filter(options);
|
|
1152
|
+
}
|
|
733
1153
|
}
|
|
1154
|
+
|
|
1155
|
+
updateMessage(state, visible.length);
|
|
1156
|
+
|
|
1157
|
+
return state.active != null && visible.includes(state.active) ? state.active : visible[0];
|
|
734
1158
|
}
|
|
735
1159
|
|
|
736
|
-
function
|
|
737
|
-
event: KeyboardEvent,
|
|
738
|
-
combobox: OuiComboboxElement,
|
|
1160
|
+
function filterOption(
|
|
739
1161
|
state: OuiComboboxState,
|
|
740
|
-
|
|
741
|
-
|
|
1162
|
+
value: unknown,
|
|
1163
|
+
textContent?: boolean,
|
|
1164
|
+
): HTMLElement | undefined {
|
|
1165
|
+
const filterTextContent = textContent ?? false;
|
|
742
1166
|
|
|
743
|
-
|
|
1167
|
+
const filter = getString(value).toLocaleLowerCase();
|
|
744
1168
|
|
|
745
|
-
|
|
746
|
-
case event.key === ' ' && state.type === 'select':
|
|
747
|
-
case event.key === 'Enter':
|
|
748
|
-
onEnter(combobox, state);
|
|
749
|
-
break;
|
|
1169
|
+
filterOptions(state);
|
|
750
1170
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
break;
|
|
1171
|
+
const {length} = state.options.all;
|
|
1172
|
+
let match: HTMLElement | undefined;
|
|
754
1173
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
break;
|
|
1174
|
+
for (let index = 0; index < length; index += 1) {
|
|
1175
|
+
const option = state.options.all[index];
|
|
758
1176
|
|
|
759
|
-
|
|
760
|
-
|
|
1177
|
+
const value = (
|
|
1178
|
+
filterTextContent ? option.textContent : (getData(option, 'value', false) ?? '')
|
|
1179
|
+
).toLocaleLowerCase();
|
|
761
1180
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
}
|
|
1181
|
+
if (value === filter) {
|
|
1182
|
+
match = option;
|
|
765
1183
|
|
|
766
1184
|
break;
|
|
767
1185
|
}
|
|
1186
|
+
}
|
|
768
1187
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
break;
|
|
1188
|
+
return match;
|
|
1189
|
+
}
|
|
772
1190
|
|
|
773
|
-
|
|
774
|
-
|
|
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
|
+
);
|
|
775
1202
|
|
|
776
|
-
|
|
777
|
-
onTab(combobox, state);
|
|
778
|
-
}
|
|
1203
|
+
const {length} = elements;
|
|
779
1204
|
|
|
780
|
-
|
|
1205
|
+
for (let index = 0; index < length; index += 1) {
|
|
1206
|
+
const element = elements[index];
|
|
1207
|
+
|
|
1208
|
+
if (element.id === '') {
|
|
1209
|
+
element.id = `${prefix}${element instanceof HTMLOptGroupElement ? '_group_' : '_option_'}${index}`;
|
|
781
1210
|
}
|
|
782
1211
|
}
|
|
1212
|
+
}
|
|
783
1213
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1214
|
+
function getAttributeValue<Value>(
|
|
1215
|
+
element: HTMLElement,
|
|
1216
|
+
attribute: string,
|
|
1217
|
+
possible: Set<Value>,
|
|
1218
|
+
defaultValue: Value,
|
|
1219
|
+
): Value {
|
|
1220
|
+
const value = getAttribute(element, attribute);
|
|
1221
|
+
|
|
1222
|
+
return possible.has(value as Value) ? (value as Value) : defaultValue;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function getAutoComplete(element: OuiComboboxElement, select: boolean): OuiComboboxAutoComplete {
|
|
1226
|
+
let value: OuiComboboxAutoCompleteType;
|
|
1227
|
+
|
|
1228
|
+
if (select) {
|
|
1229
|
+
value = 'list';
|
|
1230
|
+
} else {
|
|
1231
|
+
value = getAttributeValue(element, 'autocomplete', AUTOCOMPLETES, 'none');
|
|
787
1232
|
}
|
|
1233
|
+
|
|
1234
|
+
return {
|
|
1235
|
+
value,
|
|
1236
|
+
both: value === 'both',
|
|
1237
|
+
inline: value === 'inline',
|
|
1238
|
+
list: value === 'list',
|
|
1239
|
+
none: value === 'none',
|
|
1240
|
+
};
|
|
788
1241
|
}
|
|
789
1242
|
|
|
790
|
-
function
|
|
791
|
-
|
|
792
|
-
combobox: OuiComboboxElement,
|
|
793
|
-
state: OuiComboboxState,
|
|
794
|
-
): void {
|
|
795
|
-
const typed = event.key.length === 1 && EXPRESSION_TYPED.test(event.key);
|
|
1243
|
+
function getBooleanAria(element: HTMLElement, attribute: AnyAriaBooleanAttribute): boolean {
|
|
1244
|
+
const value = getAria(element, attribute);
|
|
796
1245
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
event.preventDefault();
|
|
800
|
-
event.stopPropagation();
|
|
1246
|
+
return value === true || value === 'true';
|
|
1247
|
+
}
|
|
801
1248
|
|
|
802
|
-
|
|
803
|
-
|
|
1249
|
+
function getId(): string {
|
|
1250
|
+
index += 1;
|
|
804
1251
|
|
|
805
|
-
|
|
806
|
-
|
|
1252
|
+
return `oui_combobox_${index}`;
|
|
1253
|
+
}
|
|
807
1254
|
|
|
808
|
-
|
|
1255
|
+
function getLanguage(state: OuiComboboxState): string {
|
|
1256
|
+
return (
|
|
1257
|
+
state.host.closest('[lang]')?.getAttribute('lang') ?? document.documentElement.lang
|
|
1258
|
+
).toLowerCase();
|
|
1259
|
+
}
|
|
809
1260
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1261
|
+
function getLocalization<Key extends keyof OuiComboboxLocalizations>(
|
|
1262
|
+
key: Key,
|
|
1263
|
+
state: OuiComboboxState,
|
|
1264
|
+
): OuiComboboxLocalizations[Key] {
|
|
1265
|
+
const language = getLanguage(state);
|
|
813
1266
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
(KEYS_REMOVE.has(event.key) || state.anchor.value.length < state.filter.length)
|
|
817
|
-
) {
|
|
818
|
-
state.filter = state.anchor.value;
|
|
1267
|
+
const allLocalizations = [state.localizations, LOCALIZATIONS.globals];
|
|
1268
|
+
const {length} = allLocalizations;
|
|
819
1269
|
|
|
820
|
-
|
|
821
|
-
|
|
1270
|
+
let localization: OuiComboboxLocalizations[Key] | undefined;
|
|
1271
|
+
let shortLanguage: string | undefined;
|
|
822
1272
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1273
|
+
for (let index = 0; index < length; index += 1) {
|
|
1274
|
+
const localizations = allLocalizations[index];
|
|
1275
|
+
|
|
1276
|
+
localization = localizations?.[language]?.[key];
|
|
827
1277
|
|
|
828
|
-
|
|
829
|
-
onRemove(combobox, state);
|
|
1278
|
+
if (localization != null) {
|
|
830
1279
|
break;
|
|
1280
|
+
}
|
|
831
1281
|
|
|
832
|
-
|
|
833
|
-
|
|
1282
|
+
shortLanguage ??= language.split('-')[0];
|
|
1283
|
+
|
|
1284
|
+
if (shortLanguage === language) {
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
localization = localizations?.[shortLanguage]?.[key];
|
|
834
1289
|
|
|
1290
|
+
if (localization != null) {
|
|
835
1291
|
break;
|
|
836
1292
|
}
|
|
837
1293
|
}
|
|
838
1294
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1295
|
+
return localization ?? LOCALIZATIONS.defaults[key];
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function getVisible(state: OuiComboboxState): HTMLElement[] {
|
|
1299
|
+
return state.options.all.filter(option => !option.hidden);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function hasTextSelection(element: HTMLElement): element is HTMLInputElement {
|
|
1303
|
+
return element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function onClickContent(event: PointerEvent, content: HTMLElement): void {
|
|
1307
|
+
const state = states.map.get(content);
|
|
1308
|
+
|
|
1309
|
+
if (state == null) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
if (state.disabled) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const option = findAncestor(event, SELECTOR_OPTION);
|
|
1318
|
+
|
|
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);
|
|
842
1325
|
}
|
|
843
1326
|
}
|
|
844
1327
|
|
|
845
|
-
function
|
|
1328
|
+
function onClickDocument(event: PointerEvent): void {
|
|
846
1329
|
const combobox = findAncestor(event, TAGNAME);
|
|
847
|
-
const state = states.get(combobox!);
|
|
1330
|
+
const state = states.map.get(combobox!);
|
|
848
1331
|
|
|
849
1332
|
if (combobox == null || state == null) {
|
|
850
1333
|
return;
|
|
@@ -853,10 +1336,12 @@ function onClick(event: PointerEvent): void {
|
|
|
853
1336
|
const label = findAncestor(event, 'label');
|
|
854
1337
|
|
|
855
1338
|
if (label != null) {
|
|
856
|
-
if (label === state.label && combobox.type === 'select') {
|
|
857
|
-
state.anchor.focus();
|
|
1339
|
+
if (label === state.label.element && combobox.type === 'select') {
|
|
1340
|
+
state.anchor.element.focus();
|
|
858
1341
|
|
|
859
|
-
state.
|
|
1342
|
+
if (!state.disabled) {
|
|
1343
|
+
state.floatable.show();
|
|
1344
|
+
}
|
|
860
1345
|
}
|
|
861
1346
|
|
|
862
1347
|
return;
|
|
@@ -865,72 +1350,273 @@ function onClick(event: PointerEvent): void {
|
|
|
865
1350
|
const selection = findAncestor(event, `[${ATTRIBUTE_SELECTION_ITEM}]`);
|
|
866
1351
|
|
|
867
1352
|
if (selection != null) {
|
|
868
|
-
const option = state.selected.find(
|
|
1353
|
+
const option = state.options.selected.find(
|
|
1354
|
+
option => option.id === getData(selection, 'option', false),
|
|
1355
|
+
);
|
|
869
1356
|
|
|
870
|
-
|
|
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
|
+
}
|
|
871
1364
|
|
|
872
1365
|
return;
|
|
873
1366
|
}
|
|
874
1367
|
|
|
875
1368
|
const anchor = findAncestor(event, SELECTOR_ANCHOR);
|
|
876
1369
|
|
|
877
|
-
if (anchor != null) {
|
|
1370
|
+
if (anchor != null && !state.disabled) {
|
|
878
1371
|
state.floatable.show();
|
|
879
1372
|
|
|
880
1373
|
return;
|
|
881
1374
|
}
|
|
882
1375
|
}
|
|
883
1376
|
|
|
884
|
-
function
|
|
885
|
-
|
|
886
|
-
|
|
1377
|
+
function onEnter(state: OuiComboboxState): void {
|
|
1378
|
+
if (state.disabled || state.readonly) {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (hasTextSelection(state.anchor.element)) {
|
|
1383
|
+
onSelection(state);
|
|
1384
|
+
} else {
|
|
1385
|
+
toggleOption(state, state.active);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function onEscape(state: OuiComboboxState): void {
|
|
1390
|
+
state.floatable.hide();
|
|
1391
|
+
}
|
|
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) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const diff = state.filter.value !== state.filter.element.value;
|
|
1404
|
+
|
|
1405
|
+
if (!state.autocomplete.inline && state.filter.element.value.length > 0 && !(manual ?? false)) {
|
|
1406
|
+
state.floatable.show();
|
|
1407
|
+
}
|
|
1408
|
+
|
|
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';
|
|
1419
|
+
}
|
|
1420
|
+
|
|
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
|
+
}
|
|
1428
|
+
|
|
1429
|
+
if (diff) {
|
|
1430
|
+
state.filter.value = state.filter.element.value;
|
|
1431
|
+
|
|
1432
|
+
dispatch(state.host, 'input');
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
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
|
+
) {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
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 {
|
|
1467
|
+
const combobox = findAncestor(event, TAGNAME);
|
|
1468
|
+
const state = states.map.get(combobox!);
|
|
887
1469
|
|
|
888
1470
|
if (combobox == null || state == null) {
|
|
889
1471
|
return;
|
|
890
1472
|
}
|
|
891
1473
|
|
|
892
|
-
const
|
|
1474
|
+
const item = findAncestor(event, SELECTOR_SELECTION_ITEM);
|
|
1475
|
+
|
|
1476
|
+
if (item instanceof HTMLElement) {
|
|
1477
|
+
onKeydownSelection(event, state, item);
|
|
1478
|
+
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
onKeydownFilter(event, state);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function onKeydownFilter(event: KeyboardEvent, state: OuiComboboxState): void {
|
|
1486
|
+
const ignore = state.disabled || state.readonly;
|
|
1487
|
+
const isSelect = state.type === 'select';
|
|
1488
|
+
|
|
1489
|
+
let preventDefault = ignore;
|
|
1490
|
+
|
|
1491
|
+
switch (true) {
|
|
1492
|
+
case event.key === ' ': {
|
|
1493
|
+
if (!state.disabled) {
|
|
1494
|
+
state.floatable.show();
|
|
1495
|
+
}
|
|
1496
|
+
|
|
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
|
+
}
|
|
1510
|
+
|
|
1511
|
+
break;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
case event.key === 'Escape': {
|
|
1515
|
+
preventDefault = false;
|
|
1516
|
+
|
|
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);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
break;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
case KEYS_NAVIGATION.has(event.key): {
|
|
1531
|
+
if (!state.disabled) {
|
|
1532
|
+
onNavigation(event, state);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
break;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
default: {
|
|
1539
|
+
preventDefault = false;
|
|
1540
|
+
|
|
1541
|
+
if (event.key === 'Tab') {
|
|
1542
|
+
onTab(state);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
break;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
if (preventDefault) {
|
|
1550
|
+
event.preventDefault();
|
|
1551
|
+
event.stopPropagation();
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function onKeydownSelection(
|
|
1556
|
+
event: KeyboardEvent,
|
|
1557
|
+
state: OuiComboboxState,
|
|
1558
|
+
item: HTMLElement,
|
|
1559
|
+
): void {
|
|
1560
|
+
if (KEYS_ACTIVATION.has(event.key)) {
|
|
1561
|
+
event.preventDefault();
|
|
1562
|
+
|
|
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;
|
|
893
1583
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1584
|
+
case 'ArrowRight':
|
|
1585
|
+
case 'ArrowLeft':
|
|
1586
|
+
next = clamp(index + (event.key === 'ArrowRight' ? 1 : -1), 0, length - 1, true);
|
|
1587
|
+
break;
|
|
897
1588
|
|
|
898
|
-
|
|
899
|
-
|
|
1589
|
+
case 'End':
|
|
1590
|
+
next = length - 1;
|
|
1591
|
+
break;
|
|
900
1592
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1593
|
+
case 'Home':
|
|
1594
|
+
next = 0;
|
|
1595
|
+
break;
|
|
904
1596
|
|
|
905
|
-
|
|
1597
|
+
default:
|
|
1598
|
+
break;
|
|
906
1599
|
}
|
|
907
1600
|
|
|
908
|
-
if (
|
|
909
|
-
state.floatable.show();
|
|
910
|
-
|
|
1601
|
+
if (next == null) {
|
|
911
1602
|
return;
|
|
912
1603
|
}
|
|
913
1604
|
|
|
914
|
-
|
|
915
|
-
}
|
|
1605
|
+
event.preventDefault();
|
|
916
1606
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1607
|
+
for (let index = 0; index < length; index += 1) {
|
|
1608
|
+
const {button} = state.selection.items[index];
|
|
1609
|
+
const selected = index === next;
|
|
920
1610
|
|
|
921
|
-
|
|
922
|
-
onAnchor(event, onAnchorKeydown);
|
|
923
|
-
}
|
|
1611
|
+
button.tabIndex = selected ? 0 : -1;
|
|
924
1612
|
|
|
925
|
-
|
|
926
|
-
|
|
1613
|
+
if (selected) {
|
|
1614
|
+
button.focus();
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
927
1617
|
}
|
|
928
1618
|
|
|
929
|
-
function onNavigation(
|
|
930
|
-
event: KeyboardEvent,
|
|
931
|
-
combobox: OuiComboboxElement,
|
|
932
|
-
state: OuiComboboxState,
|
|
933
|
-
): void {
|
|
1619
|
+
function onNavigation(event: KeyboardEvent, state: OuiComboboxState): void {
|
|
934
1620
|
let skip = false;
|
|
935
1621
|
|
|
936
1622
|
if (!state.floatable.open) {
|
|
@@ -939,7 +1625,7 @@ function onNavigation(
|
|
|
939
1625
|
state.floatable.show();
|
|
940
1626
|
}
|
|
941
1627
|
|
|
942
|
-
const
|
|
1628
|
+
const visible = getVisible(state);
|
|
943
1629
|
|
|
944
1630
|
if (visible.length === 0) {
|
|
945
1631
|
return;
|
|
@@ -982,376 +1668,571 @@ function onNavigation(
|
|
|
982
1668
|
}
|
|
983
1669
|
}
|
|
984
1670
|
|
|
985
|
-
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function onRemove(combobox: OuiComboboxElement, state: OuiComboboxState): void {
|
|
989
|
-
const [all] = getOptions(state);
|
|
1671
|
+
let preview: OuiComboboxPreview;
|
|
990
1672
|
|
|
991
|
-
|
|
1673
|
+
if (state.type === 'autocomplete' && (state.autocomplete.both || state.autocomplete.inline)) {
|
|
1674
|
+
preview = 'set';
|
|
1675
|
+
} else {
|
|
1676
|
+
preview = 'none';
|
|
1677
|
+
}
|
|
992
1678
|
|
|
993
|
-
|
|
1679
|
+
updatePreview(preview, state, next ?? state.active ?? visible[0]);
|
|
994
1680
|
}
|
|
995
1681
|
|
|
996
|
-
function
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
): void {
|
|
1001
|
-
const [all] = getOptions(state);
|
|
1002
|
-
|
|
1003
|
-
const key = event.key.toLocaleLowerCase();
|
|
1682
|
+
function onRemoveSelection(state: OuiComboboxState, item: HTMLElement): void {
|
|
1683
|
+
if (state.disabled || state.readonly) {
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1004
1686
|
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1687
|
+
const option = state.options.selected.find(
|
|
1688
|
+
option => option.id === getData(item, 'option', false),
|
|
1689
|
+
);
|
|
1008
1690
|
|
|
1009
|
-
if (
|
|
1010
|
-
|
|
1691
|
+
if (option != null) {
|
|
1692
|
+
toggleOption(state, option);
|
|
1011
1693
|
}
|
|
1012
1694
|
}
|
|
1013
1695
|
|
|
1014
|
-
function onSelection(
|
|
1015
|
-
state.filter
|
|
1696
|
+
function onSelection(state: OuiComboboxState): void {
|
|
1697
|
+
state.filter.value =
|
|
1698
|
+
state.anchor.element === state.filter.element ? state.filter.element.value : '';
|
|
1016
1699
|
|
|
1017
1700
|
filterOptions(state);
|
|
1018
1701
|
|
|
1019
|
-
|
|
1702
|
+
toggleOption(state, state.active);
|
|
1020
1703
|
}
|
|
1021
1704
|
|
|
1022
|
-
function onTab(
|
|
1023
|
-
|
|
1024
|
-
state.
|
|
1025
|
-
|
|
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
|
+
});
|
|
1026
1718
|
}
|
|
1027
1719
|
|
|
1028
|
-
function
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1720
|
+
function setDisabled(state: OuiComboboxState, value: boolean): void {
|
|
1721
|
+
state.filter.element.readOnly = state.readonly || value;
|
|
1722
|
+
|
|
1723
|
+
setProperties('disabled', state, value);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function setFilter(state: OuiComboboxState, value: unknown): void {
|
|
1727
|
+
if (typeof value !== 'string') {
|
|
1728
|
+
return;
|
|
1035
1729
|
}
|
|
1036
1730
|
|
|
1037
|
-
const
|
|
1731
|
+
const type: InputEvent['inputType'] =
|
|
1732
|
+
value.length < state.filter.element.value.length ? 'deleteContentBackward' : 'insertText';
|
|
1038
1733
|
|
|
1039
|
-
|
|
1734
|
+
state.filter.element.value = value;
|
|
1040
1735
|
|
|
1041
|
-
|
|
1736
|
+
onFilter(type, state);
|
|
1042
1737
|
}
|
|
1043
1738
|
|
|
1044
|
-
function
|
|
1045
|
-
|
|
1739
|
+
function setLimit(type: 'max' | 'min', state: OuiComboboxState, value?: unknown): void {
|
|
1740
|
+
let actual = value;
|
|
1046
1741
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
if (state.anchor instanceof HTMLInputElement) {
|
|
1051
|
-
state.anchor.readOnly = value;
|
|
1742
|
+
if (typeof actual === 'string') {
|
|
1743
|
+
actual = Number.parseInt(actual, 10);
|
|
1052
1744
|
}
|
|
1053
|
-
}
|
|
1054
1745
|
|
|
1055
|
-
function setLimit(
|
|
1056
|
-
type: 'max' | 'min',
|
|
1057
|
-
combobox: OuiComboboxElement,
|
|
1058
|
-
state: OuiComboboxState,
|
|
1059
|
-
value?: number,
|
|
1060
|
-
): void {
|
|
1061
1746
|
const updateLimit =
|
|
1062
|
-
typeof
|
|
1747
|
+
typeof actual === 'number' && !Number.isNaN(actual) && actual >= 0
|
|
1748
|
+
? actual !== state[type]
|
|
1749
|
+
: actual == null;
|
|
1063
1750
|
|
|
1064
1751
|
if (updateLimit) {
|
|
1065
|
-
state[type] =
|
|
1752
|
+
state[type] = actual as number | undefined;
|
|
1066
1753
|
|
|
1067
|
-
setAttribute(
|
|
1754
|
+
setAttribute(state.host, type, actual);
|
|
1068
1755
|
|
|
1069
1756
|
updateValidity(state);
|
|
1070
1757
|
}
|
|
1071
1758
|
}
|
|
1072
1759
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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>,
|
|
1783
|
+
): void {
|
|
1784
|
+
updateLocalizations(
|
|
1785
|
+
typeof first === 'string'
|
|
1786
|
+
? {
|
|
1787
|
+
[first]: second!,
|
|
1788
|
+
}
|
|
1789
|
+
: first,
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
function setProperties(
|
|
1794
|
+
key: 'disabled' | 'readonly' | 'required',
|
|
1076
1795
|
state: OuiComboboxState,
|
|
1077
|
-
|
|
1078
|
-
next?: HTMLElement,
|
|
1796
|
+
value: boolean,
|
|
1079
1797
|
): void {
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1798
|
+
state[key] = value;
|
|
1799
|
+
|
|
1800
|
+
const elements = [state.host, state.anchor.element, state.filter.element];
|
|
1801
|
+
const {length} = elements;
|
|
1802
|
+
|
|
1803
|
+
for (let index = 0; index < length; index += 1) {
|
|
1804
|
+
const element = elements[index];
|
|
1083
1805
|
|
|
1084
|
-
|
|
1806
|
+
setAria(
|
|
1807
|
+
element,
|
|
1808
|
+
key,
|
|
1809
|
+
key !== 'readonly' || !(element instanceof HTMLButtonElement) ? value : undefined,
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1085
1813
|
|
|
1086
|
-
|
|
1814
|
+
function setReadonly(state: OuiComboboxState, value: boolean): void {
|
|
1815
|
+
state.filter.element.readOnly = state.disabled || value;
|
|
1087
1816
|
|
|
1088
|
-
|
|
1817
|
+
setProperties('readonly', state, value);
|
|
1818
|
+
}
|
|
1089
1819
|
|
|
1090
|
-
|
|
1820
|
+
function setRequired(state: OuiComboboxState, value: boolean): void {
|
|
1821
|
+
setProperties('required', state, value);
|
|
1822
|
+
updateValidity(state);
|
|
1091
1823
|
}
|
|
1092
1824
|
|
|
1093
|
-
function
|
|
1094
|
-
state.
|
|
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
|
+
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1095
1833
|
|
|
1096
|
-
|
|
1097
|
-
setProperty(combobox, 'readonly', value);
|
|
1834
|
+
const option = filterOption(state, value);
|
|
1098
1835
|
|
|
1099
|
-
if (
|
|
1100
|
-
state
|
|
1836
|
+
if (option != null) {
|
|
1837
|
+
setValues(state, [value], emit);
|
|
1101
1838
|
}
|
|
1102
1839
|
}
|
|
1103
1840
|
|
|
1104
|
-
function
|
|
1105
|
-
|
|
1841
|
+
function setValues(state: OuiComboboxState, value: unknown, emit?: boolean): void {
|
|
1842
|
+
if (!Array.isArray(value)) {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1106
1845
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1846
|
+
const values = (state.multiple ? value : value.slice(0, 1)).map(value =>
|
|
1847
|
+
getString(value).toLocaleLowerCase(),
|
|
1848
|
+
);
|
|
1109
1849
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1850
|
+
if (values.length === 0) {
|
|
1851
|
+
values.push('');
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
if (state.autocomplete.none) {
|
|
1855
|
+
setValue(state, values[0], emit);
|
|
1112
1856
|
|
|
1113
|
-
function setValue(
|
|
1114
|
-
update: OuiComboboxUpdate,
|
|
1115
|
-
combobox: OuiComboboxElement,
|
|
1116
|
-
state: OuiComboboxState,
|
|
1117
|
-
next?: HTMLElement,
|
|
1118
|
-
): void {
|
|
1119
|
-
if (state.disabled || state.readonly || (state.type === 'select' && update !== 'set')) {
|
|
1120
1857
|
return;
|
|
1121
1858
|
}
|
|
1122
1859
|
|
|
1123
|
-
|
|
1124
|
-
let shouldUpdate: boolean;
|
|
1125
|
-
let values: string[];
|
|
1860
|
+
const {length} = state.options.all;
|
|
1126
1861
|
|
|
1127
|
-
|
|
1128
|
-
added = updateSelected(update, state, next);
|
|
1862
|
+
const selected: HTMLElement[] = [];
|
|
1129
1863
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
const [all] = getOptions(state);
|
|
1864
|
+
for (let index = 0; index < length; index += 1) {
|
|
1865
|
+
const option = state.options.all[index];
|
|
1133
1866
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1867
|
+
if (values.includes(getData(option, 'value', false)?.toLocaleLowerCase() ?? '')) {
|
|
1868
|
+
selected.push(option);
|
|
1136
1869
|
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
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
|
+
}
|
|
1137
1881
|
|
|
1138
|
-
|
|
1882
|
+
function toggleOption(state: OuiComboboxState, option?: HTMLElement): void {
|
|
1883
|
+
if (option == null || (!state.multiple && state.options.selected.includes(option))) {
|
|
1884
|
+
state.floatable.hide();
|
|
1139
1885
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1142
1888
|
|
|
1143
|
-
|
|
1889
|
+
const selected = state.options.selected.slice();
|
|
1144
1890
|
|
|
1145
|
-
|
|
1146
|
-
values = [value];
|
|
1891
|
+
let added = true;
|
|
1147
1892
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1893
|
+
if (state.multiple) {
|
|
1894
|
+
const index = selected.indexOf(option);
|
|
1150
1895
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1896
|
+
if (index === -1) {
|
|
1897
|
+
selected.push(option);
|
|
1153
1898
|
} else {
|
|
1154
|
-
|
|
1899
|
+
added = false;
|
|
1900
|
+
|
|
1901
|
+
selected.splice(index, 1);
|
|
1155
1902
|
}
|
|
1903
|
+
} else {
|
|
1904
|
+
selected.splice(0, selected.length, option);
|
|
1905
|
+
}
|
|
1156
1906
|
|
|
1157
|
-
|
|
1907
|
+
applySelection({
|
|
1908
|
+
added,
|
|
1909
|
+
option,
|
|
1910
|
+
selected,
|
|
1911
|
+
state,
|
|
1912
|
+
values: state.autocomplete.none && option != null ? [option.textContent] : undefined,
|
|
1913
|
+
});
|
|
1158
1914
|
|
|
1159
|
-
|
|
1915
|
+
if (!state.multiple) {
|
|
1916
|
+
state.floatable.hide();
|
|
1160
1917
|
}
|
|
1918
|
+
}
|
|
1161
1919
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1920
|
+
function updateAnchor(state: OuiComboboxState): void {
|
|
1921
|
+
if (!state.multiple) {
|
|
1922
|
+
return;
|
|
1164
1923
|
}
|
|
1165
1924
|
|
|
1166
|
-
|
|
1925
|
+
const localization = getLocalization('selected', state);
|
|
1926
|
+
|
|
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
|
+
}
|
|
1935
|
+
|
|
1936
|
+
function updateAutocomplete(
|
|
1937
|
+
preview: OuiComboboxPreview,
|
|
1938
|
+
state: OuiComboboxState,
|
|
1939
|
+
active?: HTMLElement,
|
|
1940
|
+
): void {
|
|
1941
|
+
if (state.multiple) {
|
|
1167
1942
|
return;
|
|
1168
1943
|
}
|
|
1169
1944
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1945
|
+
const content = active?.textContent ?? state.filter.element.value;
|
|
1946
|
+
|
|
1947
|
+
if (
|
|
1948
|
+
preview === 'typed' &&
|
|
1949
|
+
!content.toLocaleLowerCase().startsWith(state.filter.element.value.toLocaleLowerCase())
|
|
1950
|
+
) {
|
|
1951
|
+
return;
|
|
1174
1952
|
}
|
|
1175
1953
|
|
|
1176
|
-
if (
|
|
1177
|
-
|
|
1954
|
+
if (state.anchor.element instanceof HTMLInputElement) {
|
|
1955
|
+
const contentLength = content.length;
|
|
1956
|
+
const filterLength = state.filter.element.value.length;
|
|
1957
|
+
|
|
1958
|
+
if (preview === 'set' || preview === 'typed') {
|
|
1959
|
+
state.anchor.element.value = content;
|
|
1960
|
+
|
|
1961
|
+
state.anchor.element.setSelectionRange(
|
|
1962
|
+
preview === 'typed' ? filterLength : contentLength,
|
|
1963
|
+
contentLength,
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
} else {
|
|
1967
|
+
state.anchor.element.textContent = content;
|
|
1178
1968
|
}
|
|
1969
|
+
}
|
|
1179
1970
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1971
|
+
function updateForm(state: OuiComboboxState, values: string[], emit?: boolean): void {
|
|
1972
|
+
let value: string | FormData;
|
|
1973
|
+
|
|
1974
|
+
if (state.multiple) {
|
|
1975
|
+
const {length} = values;
|
|
1976
|
+
|
|
1977
|
+
value = new FormData();
|
|
1978
|
+
|
|
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];
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
state.values.current = values;
|
|
1987
|
+
|
|
1988
|
+
state.internals.setFormValue(value, JSON.stringify(values));
|
|
1989
|
+
|
|
1990
|
+
updateAnchor(state);
|
|
1991
|
+
updateValidity(state);
|
|
1992
|
+
|
|
1993
|
+
if (emit ?? true) {
|
|
1994
|
+
dispatch(state.host, 'change');
|
|
1995
|
+
}
|
|
1183
1996
|
}
|
|
1184
1997
|
|
|
1185
|
-
function
|
|
1186
|
-
|
|
1998
|
+
function updateLocalizations(
|
|
1999
|
+
values: Record<string, Partial<Record<string, string | GenericCallback>>>,
|
|
2000
|
+
destination?: Record<string, Partial<OuiComboboxLocalizations>>,
|
|
2001
|
+
): void {
|
|
2002
|
+
if (typeof values !== 'object' || values == null) {
|
|
1187
2003
|
return;
|
|
1188
2004
|
}
|
|
1189
2005
|
|
|
1190
|
-
const
|
|
1191
|
-
getString(value).toLocaleLowerCase(),
|
|
1192
|
-
);
|
|
2006
|
+
const localizations = destination ?? LOCALIZATIONS.globals;
|
|
1193
2007
|
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
const selected: HTMLElement[] = [];
|
|
1197
|
-
const values: string[] = [];
|
|
2008
|
+
const languages = Object.keys(values);
|
|
2009
|
+
const languagesLength = languages.length;
|
|
1198
2010
|
|
|
1199
|
-
let
|
|
2011
|
+
let updated = false;
|
|
1200
2012
|
|
|
1201
|
-
for (
|
|
1202
|
-
const
|
|
2013
|
+
for (let languagesIndex = 0; languagesIndex < languagesLength; languagesIndex += 1) {
|
|
2014
|
+
const language = languages[languagesIndex];
|
|
2015
|
+
const languageValue = values[language];
|
|
1203
2016
|
|
|
1204
|
-
if (
|
|
2017
|
+
if (typeof languageValue !== 'object' || languageValue == null) {
|
|
1205
2018
|
continue;
|
|
1206
2019
|
}
|
|
1207
2020
|
|
|
1208
|
-
|
|
1209
|
-
values.push(value);
|
|
2021
|
+
const normalizedLanguage = language.toLowerCase();
|
|
1210
2022
|
|
|
1211
|
-
|
|
1212
|
-
|
|
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];
|
|
1213
2031
|
|
|
1214
|
-
|
|
1215
|
-
setAria(option, 'selected', false);
|
|
2032
|
+
const normalizedKey = key.toLowerCase() as keyof OuiComboboxLocalizations;
|
|
1216
2033
|
|
|
1217
|
-
|
|
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
|
+
}
|
|
1218
2043
|
}
|
|
1219
2044
|
|
|
1220
|
-
|
|
1221
|
-
|
|
2045
|
+
if (!updated) {
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
1222
2048
|
|
|
1223
|
-
|
|
2049
|
+
for (const state of states.set) {
|
|
2050
|
+
updateAnchor(state);
|
|
2051
|
+
updateMessage(state, getVisible(state).length);
|
|
2052
|
+
updateSelectionItems(state);
|
|
1224
2053
|
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function updateMessage(state: OuiComboboxState, visible: number): void {
|
|
2057
|
+
setAttribute(state.message, ATTRIBUTE_HIDDEN, visible > 0 ? '' : undefined);
|
|
2058
|
+
|
|
2059
|
+
if (visible === 0) {
|
|
2060
|
+
const localization = getLocalization('empty', state);
|
|
1225
2061
|
|
|
1226
|
-
|
|
2062
|
+
state.message.textContent = typeof localization === 'function' ? localization() : localization;
|
|
2063
|
+
} else {
|
|
2064
|
+
const localization = getLocalization('results', state);
|
|
1227
2065
|
|
|
1228
|
-
|
|
2066
|
+
state.message.textContent =
|
|
2067
|
+
typeof localization === 'function'
|
|
2068
|
+
? localization(visible)
|
|
2069
|
+
: localization.replace('{amount}', visible.toString());
|
|
2070
|
+
}
|
|
1229
2071
|
|
|
1230
|
-
|
|
2072
|
+
state.message.hidden = false;
|
|
1231
2073
|
}
|
|
1232
2074
|
|
|
1233
|
-
function
|
|
1234
|
-
|
|
2075
|
+
function updatePreview(
|
|
2076
|
+
preview: OuiComboboxPreview,
|
|
1235
2077
|
state: OuiComboboxState,
|
|
1236
|
-
|
|
2078
|
+
active?: HTMLElement,
|
|
1237
2079
|
): void {
|
|
1238
|
-
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
2080
|
+
const {length} = state.options.all;
|
|
1241
2081
|
|
|
1242
|
-
|
|
2082
|
+
for (let index = 0; index < length; index += 1) {
|
|
2083
|
+
const option = state.options.all[index];
|
|
1243
2084
|
|
|
1244
|
-
|
|
1245
|
-
update === 'typed' &&
|
|
1246
|
-
!content.toLocaleLowerCase().startsWith(state.filter.toLocaleLowerCase())
|
|
1247
|
-
) {
|
|
1248
|
-
return;
|
|
2085
|
+
setAttribute(option, ATTRIBUTE_OPTION_ACTIVE, option === active ? '' : undefined);
|
|
1249
2086
|
}
|
|
1250
2087
|
|
|
1251
|
-
|
|
2088
|
+
state.active = active;
|
|
1252
2089
|
|
|
1253
|
-
|
|
1254
|
-
state.anchor.value = content;
|
|
2090
|
+
setAria(state.filter.element, 'activedescendant', state.floatable.open ? active?.id : undefined);
|
|
1255
2091
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
2092
|
+
if (state.floatable.open) {
|
|
2093
|
+
active?.scrollIntoView(scrollOptions);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
if (preview !== 'none') {
|
|
2097
|
+
updateAutocomplete(preview, state, active);
|
|
1259
2098
|
}
|
|
1260
2099
|
}
|
|
1261
2100
|
|
|
1262
|
-
function
|
|
1263
|
-
|
|
2101
|
+
function updateSelection(
|
|
2102
|
+
state: OuiComboboxState,
|
|
2103
|
+
active: HTMLElement | undefined,
|
|
2104
|
+
added: boolean,
|
|
2105
|
+
selected?: HTMLElement[],
|
|
2106
|
+
): void {
|
|
2107
|
+
if (state.selection == null) {
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
1264
2110
|
|
|
1265
|
-
|
|
2111
|
+
let options = active == null ? [] : [active];
|
|
1266
2112
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
}
|
|
2113
|
+
if (selected != null) {
|
|
2114
|
+
options = selected;
|
|
1270
2115
|
|
|
1271
|
-
|
|
2116
|
+
const {length} = state.selection.items;
|
|
1272
2117
|
|
|
1273
|
-
|
|
2118
|
+
for (let index = 0; index < length; index += 1) {
|
|
2119
|
+
state.selection.items[index].element.remove();
|
|
2120
|
+
}
|
|
1274
2121
|
|
|
1275
|
-
|
|
2122
|
+
state.selection.items.splice(0);
|
|
2123
|
+
}
|
|
1276
2124
|
|
|
1277
|
-
|
|
1278
|
-
|
|
2125
|
+
if (added) {
|
|
2126
|
+
createSelectionItems({
|
|
2127
|
+
options,
|
|
2128
|
+
state,
|
|
2129
|
+
container: state.selection.container,
|
|
2130
|
+
items: state.selection.items,
|
|
2131
|
+
});
|
|
1279
2132
|
|
|
1280
|
-
function updateSelect(
|
|
1281
|
-
update: OuiComboboxUpdate,
|
|
1282
|
-
state: OuiComboboxState,
|
|
1283
|
-
next: HTMLElement,
|
|
1284
|
-
added?: boolean,
|
|
1285
|
-
): void {
|
|
1286
|
-
if (update !== 'set') {
|
|
1287
2133
|
return;
|
|
1288
2134
|
}
|
|
1289
2135
|
|
|
1290
|
-
|
|
1291
|
-
|
|
2136
|
+
let current = state.selection.items.findIndex(
|
|
2137
|
+
item => getData(item.button, 'option', false) === active?.id,
|
|
2138
|
+
);
|
|
1292
2139
|
|
|
2140
|
+
if (current === -1) {
|
|
1293
2141
|
return;
|
|
1294
2142
|
}
|
|
1295
2143
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
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
|
+
|
|
2153
|
+
return;
|
|
1300
2154
|
}
|
|
1301
|
-
}
|
|
1302
2155
|
|
|
1303
|
-
|
|
1304
|
-
update: OuiComboboxUpdate,
|
|
1305
|
-
state: OuiComboboxState,
|
|
1306
|
-
next?: HTMLElement,
|
|
1307
|
-
): boolean | undefined {
|
|
1308
|
-
const [all] = getOptions(state);
|
|
2156
|
+
const {length} = state.selection.items;
|
|
1309
2157
|
|
|
1310
|
-
|
|
1311
|
-
|
|
2158
|
+
if (current < 0) {
|
|
2159
|
+
current = 0;
|
|
2160
|
+
} else if (current >= length) {
|
|
2161
|
+
current = length - 1;
|
|
1312
2162
|
}
|
|
1313
2163
|
|
|
1314
|
-
|
|
1315
|
-
|
|
2164
|
+
for (let index = 0; index < length; index += 1) {
|
|
2165
|
+
const {button} = state.selection.items[index];
|
|
2166
|
+
|
|
2167
|
+
button.tabIndex = index === current ? 0 : -1;
|
|
1316
2168
|
}
|
|
1317
2169
|
|
|
1318
|
-
|
|
1319
|
-
|
|
2170
|
+
state.selection.items[current].button.focus();
|
|
2171
|
+
}
|
|
1320
2172
|
|
|
2173
|
+
function updateSelectionItems(state: OuiComboboxState): void {
|
|
2174
|
+
if (state.selection == null) {
|
|
1321
2175
|
return;
|
|
1322
2176
|
}
|
|
1323
2177
|
|
|
1324
|
-
|
|
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) {
|
|
1325
2205
|
return;
|
|
1326
2206
|
}
|
|
1327
2207
|
|
|
1328
|
-
|
|
1329
|
-
|
|
2208
|
+
const next = state.selection.items.find(
|
|
2209
|
+
item => getData(item.button, 'option', false) === active.id,
|
|
2210
|
+
);
|
|
1330
2211
|
|
|
2212
|
+
if (next == null) {
|
|
1331
2213
|
return;
|
|
1332
2214
|
}
|
|
1333
2215
|
|
|
1334
|
-
const
|
|
1335
|
-
const added = index === -1;
|
|
2216
|
+
const {length} = state.selection.items;
|
|
1336
2217
|
|
|
1337
|
-
|
|
1338
|
-
state.
|
|
1339
|
-
|
|
1340
|
-
|
|
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;
|
|
1341
2222
|
}
|
|
1342
2223
|
|
|
1343
|
-
|
|
2224
|
+
next.button.focus();
|
|
1344
2225
|
}
|
|
1345
2226
|
|
|
1346
2227
|
function updateValidity(state: OuiComboboxState): void {
|
|
1347
2228
|
if (state.autocomplete.none) {
|
|
1348
|
-
const valueMissing = state.required && state.
|
|
2229
|
+
const valueMissing = state.required && state.filter.element.value.length === 0;
|
|
2230
|
+
|
|
2231
|
+
const localization = getLocalization('input', state);
|
|
1349
2232
|
|
|
1350
2233
|
state.internals.setValidity(
|
|
1351
|
-
{
|
|
1352
|
-
|
|
1353
|
-
},
|
|
1354
|
-
INVALID_INPUT,
|
|
2234
|
+
{valueMissing},
|
|
2235
|
+
typeof localization === 'function' ? localization() : localization,
|
|
1355
2236
|
);
|
|
1356
2237
|
|
|
1357
2238
|
state.internals.checkValidity();
|
|
@@ -1359,41 +2240,66 @@ function updateValidity(state: OuiComboboxState): void {
|
|
|
1359
2240
|
return;
|
|
1360
2241
|
}
|
|
1361
2242
|
|
|
1362
|
-
|
|
1363
|
-
|
|
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';
|
|
2261
|
+
|
|
2262
|
+
const localization = getLocalization('max', state);
|
|
2263
|
+
|
|
2264
|
+
message =
|
|
2265
|
+
typeof localization === 'function'
|
|
2266
|
+
? localization(state.max)
|
|
2267
|
+
: localization.replace('{max}', state.max.toString());
|
|
1364
2268
|
|
|
1365
|
-
|
|
1366
|
-
|
|
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
|
+
}
|
|
1367
2286
|
|
|
1368
|
-
|
|
2287
|
+
default:
|
|
2288
|
+
break;
|
|
2289
|
+
}
|
|
1369
2290
|
|
|
1370
2291
|
state.internals.setValidity(
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
[
|
|
1378
|
-
badInput ? INVALID_BAD : undefined,
|
|
1379
|
-
rangeOverflow
|
|
1380
|
-
? getString(INVALID_OVERFLOW).replace('{max}', state.max!.toString())
|
|
1381
|
-
: undefined,
|
|
1382
|
-
rangeUnderflow
|
|
1383
|
-
? getString(INVALID_UNDERFLOW).replace('{min}', state.min!.toString())
|
|
1384
|
-
: undefined,
|
|
1385
|
-
],
|
|
1386
|
-
'; ',
|
|
1387
|
-
),
|
|
2292
|
+
error == null
|
|
2293
|
+
? {}
|
|
2294
|
+
: {
|
|
2295
|
+
[error]: true,
|
|
2296
|
+
},
|
|
2297
|
+
message,
|
|
1388
2298
|
);
|
|
1389
2299
|
|
|
1390
2300
|
state.internals.checkValidity();
|
|
1391
2301
|
}
|
|
1392
2302
|
|
|
1393
|
-
function updateWidth(state: OuiComboboxState): void {
|
|
1394
|
-
setStyle(state.content, PROPERTY_STYLE, `${state.anchor.getBoundingClientRect().width}px`);
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
2303
|
// #endregion
|
|
1398
2304
|
|
|
1399
2305
|
// #region Variables
|
|
@@ -1404,10 +2310,18 @@ const ATTRIBUTE_ANCHOR = `${TAGNAME}-anchor`;
|
|
|
1404
2310
|
|
|
1405
2311
|
const ATTRIBUTE_CONTENT = `${TAGNAME}-content`;
|
|
1406
2312
|
|
|
1407
|
-
const
|
|
2313
|
+
const ATTRIBUTE_FILTER = `${TAGNAME}-filter`;
|
|
2314
|
+
|
|
2315
|
+
const ATTRIBUTE_FILTER_INPUT = `${ATTRIBUTE_FILTER}-input`;
|
|
2316
|
+
|
|
2317
|
+
const ATTRIBUTE_HEADER = `${TAGNAME}-header`;
|
|
2318
|
+
|
|
2319
|
+
const ATTRIBUTE_HIDDEN = `${TAGNAME}-hidden`;
|
|
1408
2320
|
|
|
1409
2321
|
const ATTRIBUTE_LIST = `${TAGNAME}-list`;
|
|
1410
2322
|
|
|
2323
|
+
const ATTRIBUTE_MESSAGE = `${TAGNAME}-message`;
|
|
2324
|
+
|
|
1411
2325
|
const ATTRIBUTE_OPTION = `${TAGNAME}-option`;
|
|
1412
2326
|
|
|
1413
2327
|
const ATTRIBUTE_OPTION_ACTIVE = `${TAGNAME}-option-active`;
|
|
@@ -1418,45 +2332,62 @@ const ATTRIBUTE_SELECTION_ITEM = `${TAGNAME}-selection-item`;
|
|
|
1418
2332
|
|
|
1419
2333
|
const AUTOCOMPLETES = new Set<OuiComboboxAutoCompleteType>(['both', 'inline', 'list', 'none']);
|
|
1420
2334
|
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
const INVALID_BAD = 'Please select an option from the list';
|
|
1424
|
-
|
|
1425
|
-
const INVALID_INPUT = 'Please enter a value';
|
|
1426
|
-
|
|
1427
|
-
const INVALID_OVERFLOW = 'Please select fewer options (maximum is {max})';
|
|
1428
|
-
|
|
1429
|
-
const INVALID_UNDERFLOW = 'Please select more options (minimum is {min})';
|
|
1430
|
-
|
|
1431
|
-
const KEYS_ABSOLUTE = new Set(['End', 'Home']);
|
|
2335
|
+
const KEYS_ACTIVATION = new Set([' ', 'Backspace', 'Delete', 'Enter']);
|
|
1432
2336
|
|
|
1433
2337
|
const KEYS_NAVIGATION = new Set(['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp']);
|
|
1434
2338
|
|
|
1435
|
-
const KEYS_REMOVE = new Set(['Backspace', 'Delete']);
|
|
1436
|
-
|
|
1437
2339
|
const KEYS_SELECTION = new Set(['ArrowRight', 'End']);
|
|
1438
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
|
+
|
|
1439
2369
|
const MESSAGE_LABEL = `<${TAGNAME}> must contain a <label> element for the <select> element`;
|
|
1440
2370
|
|
|
1441
2371
|
const MESSAGE_SELECT = `<${TAGNAME}> must contain a valid <select> element`;
|
|
1442
2372
|
|
|
1443
|
-
const PROPERTY_STYLE = '--oui-combobox-width';
|
|
1444
|
-
|
|
1445
2373
|
const SELECTOR_ANCHOR = `[${ATTRIBUTE_ANCHOR}]`;
|
|
1446
2374
|
|
|
2375
|
+
const SELECTOR_SELECTION_ITEM = `[${ATTRIBUTE_SELECTION_ITEM}]`;
|
|
2376
|
+
|
|
1447
2377
|
const SELECTOR_OPTION = `[${ATTRIBUTE_OPTION}]`;
|
|
1448
2378
|
|
|
1449
2379
|
const TYPES = new Set<OuiComboboxType>(['autocomplete', 'select']);
|
|
1450
2380
|
|
|
1451
|
-
const comboboxes = new WeakMap<HTMLElement, OuiComboboxElement>();
|
|
1452
|
-
|
|
1453
2381
|
const scrollOptions: ScrollIntoViewOptions = {
|
|
1454
|
-
behavior: '
|
|
2382
|
+
behavior: 'auto',
|
|
1455
2383
|
block: 'nearest',
|
|
1456
2384
|
inline: 'nearest',
|
|
1457
2385
|
};
|
|
1458
2386
|
|
|
1459
|
-
const states =
|
|
2387
|
+
const states = {
|
|
2388
|
+
map: new WeakMap<HTMLElement, OuiComboboxState>(),
|
|
2389
|
+
set: new Set<OuiComboboxState>(),
|
|
2390
|
+
};
|
|
1460
2391
|
|
|
1461
2392
|
let index = 0;
|
|
1462
2393
|
|
|
@@ -1464,13 +2395,13 @@ let index = 0;
|
|
|
1464
2395
|
|
|
1465
2396
|
// #region Initialization
|
|
1466
2397
|
|
|
1467
|
-
on(document, 'click',
|
|
2398
|
+
on(document, 'click', onClickDocument);
|
|
1468
2399
|
|
|
1469
|
-
on(document, '
|
|
2400
|
+
on(document, 'input', onInput, {
|
|
1470
2401
|
passive: false,
|
|
1471
2402
|
});
|
|
1472
2403
|
|
|
1473
|
-
on(document, '
|
|
2404
|
+
on(document, 'keydown', onKeydown, {
|
|
1474
2405
|
passive: false,
|
|
1475
2406
|
});
|
|
1476
2407
|
|