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