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