html-combobox-element 0.0.2 → 0.0.3
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/README.md +123 -0
- package/dist/cjs/combobox/Combobox.markup.js +178 -102
- package/dist/cjs/combobox/HTML.combobox.element.js +262 -113
- package/dist/cjs/combobox/HTML.combobox.option.element.js +70 -5
- package/dist/cjs/combobox/HTML.combobox.tag.element.js +26 -7
- package/dist/cjs/index.js +0 -66
- package/dist/esm/combobox/Combobox.markup.js +178 -102
- package/dist/esm/combobox/HTML.combobox.element.js +262 -113
- package/dist/esm/combobox/HTML.combobox.option.element.js +70 -5
- package/dist/esm/combobox/HTML.combobox.tag.element.js +26 -7
- package/dist/esm/index.js +0 -66
- package/dist/types/combobox/Combobox.markup.d.ts +13 -8
- package/dist/types/combobox/Combobox.markup.d.ts.map +1 -1
- package/dist/types/combobox/HTML.combobox.element.d.ts +10 -4
- package/dist/types/combobox/HTML.combobox.element.d.ts.map +1 -1
- package/dist/types/combobox/HTML.combobox.option.element.d.ts +11 -0
- package/dist/types/combobox/HTML.combobox.option.element.d.ts.map +1 -1
- package/dist/types/combobox/HTML.combobox.tag.element.d.ts +9 -1
- package/dist/types/combobox/HTML.combobox.tag.element.d.ts.map +1 -1
- package/dist/types/index.d.ts +102 -61
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +11 -3
- package/dist/types/types.d.ts +0 -198
|
@@ -3,7 +3,7 @@ import { HTMLComboboxOptionElement } from './HTML.combobox.option.element.js';
|
|
|
3
3
|
import { toBoolean } from './Boolean.attribute.value.normalizer.js';
|
|
4
4
|
export class HTMLComboboxElement extends HTMLElement {
|
|
5
5
|
static OWN_IDL = new Set(['required', 'disabled', 'clearable', 'multiple', 'filterable', 'searchable', 'value', 'placeholder', 'query']);
|
|
6
|
-
static observerOptions = { childList: true
|
|
6
|
+
static observerOptions = { childList: true };
|
|
7
7
|
static styleSheet = [new CSSStyleSheet];
|
|
8
8
|
static formAssociated = true;
|
|
9
9
|
shadowRoot;
|
|
@@ -11,41 +11,136 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
11
11
|
#observer;
|
|
12
12
|
#markup;
|
|
13
13
|
#values = new Set;
|
|
14
|
+
#prevValue = '';
|
|
15
|
+
#changed = false;
|
|
14
16
|
constructor() {
|
|
15
17
|
super();
|
|
16
18
|
this.#internals = this.attachInternals();
|
|
17
19
|
this.#internals.role = "combobox";
|
|
18
20
|
this.#internals.ariaHasPopup = "dialog";
|
|
19
|
-
this.shadowRoot = this.attachShadow({ mode: '
|
|
21
|
+
this.shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: true }); //delegatesFocus: true
|
|
20
22
|
this.#markup = new ComboboxMarkup(this.shadowRoot, this.#internals);
|
|
21
|
-
this.shadowRoot.
|
|
23
|
+
this.shadowRoot.appendChild(ComboboxMarkup.markup.content.cloneNode(true));
|
|
22
24
|
this.shadowRoot.adoptedStyleSheets = HTMLComboboxElement.styleSheet;
|
|
23
25
|
this.#observer = new MutationObserver(this.#onOptionsChanges);
|
|
26
|
+
this.#markup.connect();
|
|
24
27
|
}
|
|
25
28
|
// Lifecycle callbacks
|
|
26
29
|
connectedCallback() {
|
|
27
|
-
this
|
|
30
|
+
for (const child of Array.from(this.children)) {
|
|
31
|
+
if (HTMLComboboxElement.#isOptionLike(child)) {
|
|
32
|
+
this.#markup.optionsContainer?.appendChild(child);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
this.#markup.onHostConnected();
|
|
36
|
+
this.shadowRoot.addEventListener('optionselectedstatechange', this.#onOptionSelectedStateChanges);
|
|
37
|
+
this.shadowRoot.addEventListener('tagcleared', this.#onClearTag);
|
|
28
38
|
this.#initialAttributesSynchronization();
|
|
29
|
-
this.#
|
|
30
|
-
this
|
|
31
|
-
this.#markup.clearAllButton.addEventListener('click', this.#
|
|
39
|
+
this.#observer.observe(this.#markup.optionsContainer, HTMLComboboxElement.observerOptions);
|
|
40
|
+
this.addEventListener('keydown', this.#onKeyDown);
|
|
41
|
+
this.#markup.clearAllButton.addEventListener('click', this.#onClearAll);
|
|
32
42
|
this.#markup.searchInput.addEventListener('input', this.#onInput);
|
|
43
|
+
this.#markup.dropdown.addEventListener('toggle', this.#onPickerToggle);
|
|
44
|
+
this.#setValidityAndFormValue();
|
|
45
|
+
// Options/optgroups can end up as direct light-DOM children of <combo-box> without ever
|
|
46
|
+
// going through insertBefore/appendChild below: parsed straight from static HTML markup,
|
|
47
|
+
// or - notably - Solid's compiler, which renders static JSX subtrees via a single
|
|
48
|
+
// <template> + cloneNode(true), never calling any child-mutation method at all. Sweep
|
|
49
|
+
// whatever is already here once on connect and move it into the real options container.
|
|
50
|
+
}
|
|
51
|
+
static #isOptionLike(node) {
|
|
52
|
+
return node instanceof HTMLComboboxOptionElement || node instanceof HTMLOptGroupElement;
|
|
53
|
+
}
|
|
54
|
+
// Overwritten child-mutation methods.
|
|
55
|
+
//
|
|
56
|
+
// <box-option>/<optgroup> children don't actually live under <combo-box> itself - they're
|
|
57
|
+
// redirected into #markup.optionsContainer inside the shadow root, which is what the
|
|
58
|
+
// dropdown renders from and what #observer watches. Every native method a framework's
|
|
59
|
+
// renderer or plain user code might use to add/move/remove children needs to funnel through
|
|
60
|
+
// the same redirect, since browsers implement each of these independently (overriding one,
|
|
61
|
+
// e.g. insertBefore, does not make the others call it under the hood):
|
|
62
|
+
// - React calls appendChild for a plain append and insertBefore only when reordering.
|
|
63
|
+
// - Vue and Preact always call insertBefore, with a null reference for a plain append.
|
|
64
|
+
// - Solid mostly bypasses all of them for static subtrees (see connectedCallback above),
|
|
65
|
+
// but dynamic option lists still go through insert()/insertBefore().
|
|
66
|
+
insertBefore = (node, referenceNode) => {
|
|
67
|
+
if (HTMLComboboxElement.#isOptionLike(node)) {
|
|
68
|
+
this.#markup.optionsContainer?.insertBefore(node, referenceNode);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
super.insertBefore(node, referenceNode);
|
|
72
|
+
}
|
|
73
|
+
return node;
|
|
74
|
+
};
|
|
75
|
+
appendChild = (node) => {
|
|
76
|
+
if (HTMLComboboxElement.#isOptionLike(node)) {
|
|
77
|
+
this.#markup.optionsContainer?.appendChild(node);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
super.appendChild(node);
|
|
81
|
+
}
|
|
82
|
+
return node;
|
|
83
|
+
};
|
|
84
|
+
append = (...nodes) => {
|
|
85
|
+
nodes.forEach(node => this.appendChild(typeof node === 'string' ? new Text(node) : node));
|
|
86
|
+
};
|
|
87
|
+
prepend = (...nodes) => {
|
|
88
|
+
[...nodes].reverse().forEach(node => {
|
|
89
|
+
const n = typeof node === 'string' ? new Text(node) : node;
|
|
90
|
+
if (HTMLComboboxElement.#isOptionLike(n)) {
|
|
91
|
+
this.#markup.optionsContainer?.insertBefore(n, this.#markup.optionsContainer.firstChild);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
super.insertBefore(n, this.firstChild);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
replaceChildren = (...nodes) => {
|
|
99
|
+
this.#markup.optionsContainer?.replaceChildren();
|
|
100
|
+
super.replaceChildren();
|
|
101
|
+
this.append(...nodes);
|
|
102
|
+
};
|
|
103
|
+
removeChild = (child) => {
|
|
104
|
+
if (child.parentNode === this.#markup.optionsContainer) {
|
|
105
|
+
this.#markup.optionsContainer.removeChild(child);
|
|
106
|
+
}
|
|
107
|
+
else if (child.parentNode === this) {
|
|
108
|
+
super.removeChild(child);
|
|
109
|
+
}
|
|
110
|
+
return child;
|
|
111
|
+
};
|
|
112
|
+
replaceChild = (newChild, oldChild) => {
|
|
113
|
+
if (oldChild.parentNode === this.#markup.optionsContainer) {
|
|
114
|
+
this.#markup.optionsContainer.replaceChild(newChild, oldChild);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
super.replaceChild(newChild, oldChild);
|
|
118
|
+
}
|
|
119
|
+
return oldChild;
|
|
120
|
+
};
|
|
121
|
+
get changed() {
|
|
122
|
+
return this.#changed;
|
|
33
123
|
}
|
|
34
124
|
disconnectedCallback() {
|
|
35
125
|
this.#observer.disconnect();
|
|
36
126
|
this.#markup.disconnect();
|
|
37
|
-
this
|
|
127
|
+
this.removeEventListener('keydown', this.#onKeyDown);
|
|
128
|
+
this.#markup.clearAllButton.removeEventListener('click', this.#onClearAll);
|
|
38
129
|
this.#markup.searchInput.removeEventListener('input', this.#onInput);
|
|
130
|
+
this.#markup.dropdown.removeEventListener('toggle', this.#onPickerToggle);
|
|
131
|
+
this.shadowRoot.removeEventListener('optionselectedstatechange', this.#onOptionSelectedStateChanges);
|
|
132
|
+
this.shadowRoot.removeEventListener('tagcleared', this.#onClearTag);
|
|
39
133
|
}
|
|
40
134
|
formResetCallback() {
|
|
41
|
-
this.#values = new Set;
|
|
42
135
|
this.selectedOptions.forEach(option => option.selected = false);
|
|
43
|
-
this.#markup.tagsContainer
|
|
44
|
-
this
|
|
45
|
-
this.
|
|
136
|
+
this.#markup.tagsContainer?.replaceChildren();
|
|
137
|
+
this.dispatchEvent(new Event('reset'));
|
|
138
|
+
requestAnimationFrame(() => this.#markup.searchInput?.focus());
|
|
46
139
|
}
|
|
47
|
-
formDisabledCallback(
|
|
48
|
-
|
|
140
|
+
formDisabledCallback() {
|
|
141
|
+
// toDo
|
|
142
|
+
// when inside fieldset, this callback is only invoked for the first time
|
|
143
|
+
// isDisabled: boolean
|
|
49
144
|
}
|
|
50
145
|
// Instance properties
|
|
51
146
|
// <combo-box> specific properties
|
|
@@ -62,22 +157,17 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
62
157
|
value = '';
|
|
63
158
|
value = String(value);
|
|
64
159
|
super.setAttribute('query', value);
|
|
65
|
-
|
|
66
|
-
this.#markup.searchInput.value = value;
|
|
67
|
-
}
|
|
160
|
+
this.#markup?.setInputValue(value);
|
|
68
161
|
}
|
|
69
162
|
get placeholder() {
|
|
70
163
|
return this.getAttribute('placeholder');
|
|
71
164
|
}
|
|
72
165
|
set placeholder(value) {
|
|
73
166
|
if (value == null)
|
|
74
|
-
value = '';
|
|
167
|
+
value = ' ';
|
|
75
168
|
value = String(value);
|
|
76
169
|
super.setAttribute('placeholder', value);
|
|
77
|
-
|
|
78
|
-
this.#markup.placeholder.innerText = value;
|
|
79
|
-
this.#markup.searchInput.placeholder = value;
|
|
80
|
-
}
|
|
170
|
+
this.#markup?.setPlaceholder(value);
|
|
81
171
|
}
|
|
82
172
|
get clearable() {
|
|
83
173
|
return this.hasAttribute('clearable');
|
|
@@ -103,7 +193,9 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
103
193
|
}
|
|
104
194
|
set disabled(value) {
|
|
105
195
|
this.#internals.ariaDisabled = String(value);
|
|
106
|
-
|
|
196
|
+
value = toBoolean(value);
|
|
197
|
+
super.toggleAttribute('disabled', value);
|
|
198
|
+
super.toggleAttribute('inert', value);
|
|
107
199
|
}
|
|
108
200
|
get form() {
|
|
109
201
|
return this.#internals.form;
|
|
@@ -154,34 +246,14 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
154
246
|
set value(value) {
|
|
155
247
|
if (this.value === value || typeof value !== 'string')
|
|
156
248
|
return;
|
|
157
|
-
const
|
|
158
|
-
this.#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
values.length = 1;
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
values.length = 0;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
for (const key of values) {
|
|
172
|
-
const option = this.#markup.getOptionByValue(key);
|
|
173
|
-
if (option)
|
|
174
|
-
this.#selectOption(option);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
for (const key of prevValue) {
|
|
178
|
-
if (this.#values.has(key))
|
|
179
|
-
continue;
|
|
180
|
-
const option = this.#markup.getOptionByValue(key);
|
|
181
|
-
const tag = this.#markup.getTagByValue(key);
|
|
182
|
-
tag?.remove();
|
|
183
|
-
option?.toggleAttribute('selected', false);
|
|
184
|
-
}
|
|
249
|
+
const nextValues = new Set(value.split(',').filter(Boolean));
|
|
250
|
+
this.#markup.options.forEach(option => {
|
|
251
|
+
const optionValue = option.value;
|
|
252
|
+
// toggling `selected` drives #onOptionSelectedStateChanges, which is the
|
|
253
|
+
// single source of truth for #values and tags - don't pre-assign #values
|
|
254
|
+
// here, or the dedup logic there ends up deselecting the option it's in
|
|
255
|
+
// the middle of selecting (see: stale tag left behind on next selection)
|
|
256
|
+
option.selected = nextValues.has(optionValue);
|
|
185
257
|
});
|
|
186
258
|
}
|
|
187
259
|
// Instance methods
|
|
@@ -228,91 +300,168 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
228
300
|
}
|
|
229
301
|
}
|
|
230
302
|
// Internal
|
|
231
|
-
#onInput = (event) => {
|
|
232
|
-
if (!this.searchable && this.filterable) {
|
|
233
|
-
if (event.target && event.target instanceof HTMLInputElement) {
|
|
234
|
-
this.#markup.sort(event.target.value);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
303
|
#onOptionsChanges = (records) => {
|
|
239
304
|
records.forEach(record => {
|
|
240
|
-
record.
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
305
|
+
record.removedNodes.forEach(option => {
|
|
306
|
+
if (option instanceof HTMLComboboxOptionElement) {
|
|
307
|
+
const value = option.value;
|
|
308
|
+
// mode is searchable,
|
|
309
|
+
// user type "a", select a result,
|
|
310
|
+
// then types "b". Here previous options will be replaced with new ones,
|
|
311
|
+
// this.#values may contain "a", but there is no relevant option yet,
|
|
312
|
+
// so just wait
|
|
313
|
+
if (this.searchable && this.#internals.ariaExpanded === 'true') {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// options was removed, but values still include this value,
|
|
317
|
+
// just clear, this is like a reset was called
|
|
318
|
+
if (this.#values.has(value) && !this.#markup.optionsContainer?.querySelector(`[value="${value}"]`)) {
|
|
319
|
+
this.#values.delete(value);
|
|
320
|
+
const control = this.#markup.getTagByValue(value);
|
|
321
|
+
control?.remove();
|
|
250
322
|
}
|
|
251
|
-
}
|
|
252
|
-
if (node instanceof HTMLComboboxOptionElement || node instanceof HTMLOptGroupElement) {
|
|
253
|
-
this.#markup.optionsContainer.append(node);
|
|
254
323
|
}
|
|
255
324
|
});
|
|
256
325
|
});
|
|
257
|
-
this.#markup.invalidateOptionsCache();
|
|
258
|
-
this.#setValidityAndFormValue();
|
|
259
326
|
};
|
|
260
|
-
#
|
|
261
|
-
|
|
262
|
-
return;
|
|
327
|
+
#onOptionSelectedStateChanges = (event) => {
|
|
328
|
+
const option = event.target;
|
|
263
329
|
const value = option.value;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
330
|
+
if (option.selected) {
|
|
331
|
+
if (!this.multiple) {
|
|
332
|
+
this.#values.forEach(value => {
|
|
333
|
+
const prevOption = this.#markup.getOptionByValue(value);
|
|
334
|
+
if (prevOption) {
|
|
335
|
+
prevOption.selected = false;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const tag = this.#markup.getTagByValue(value);
|
|
339
|
+
this.#values.delete(value);
|
|
340
|
+
tag?.remove();
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (!this.#values.has(value) || !this.#markup.getTagByValue(value)) {
|
|
345
|
+
this.#values.add(value);
|
|
346
|
+
this.#markup.createAndAppendTag(option);
|
|
347
|
+
this.#setValidityAndFormValue();
|
|
348
|
+
this.dispatchEvent(new Event('change'));
|
|
349
|
+
}
|
|
270
350
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (option) {
|
|
277
|
-
if (this.#values.has(option.value))
|
|
278
|
-
return;
|
|
279
|
-
if (!this.multiple) {
|
|
280
|
-
event.stopPropagation();
|
|
281
|
-
this.#values.forEach(value => {
|
|
282
|
-
this.#markup.getTagByValue(value)?.remove();
|
|
283
|
-
this.#markup.getOptionByValue(value)?.toggleAttribute('selected', false);
|
|
284
|
-
});
|
|
285
|
-
this.#values.clear();
|
|
286
|
-
this.#markup.tagsContainer.replaceChildren();
|
|
287
|
-
}
|
|
288
|
-
this.#selectOption(option);
|
|
351
|
+
else {
|
|
352
|
+
if (this.#values.has(value)) {
|
|
353
|
+
this.#values.delete(value);
|
|
354
|
+
const control = this.#markup.getTagByValue(value);
|
|
355
|
+
control?.remove();
|
|
289
356
|
this.#setValidityAndFormValue();
|
|
290
357
|
this.dispatchEvent(new Event('change'));
|
|
291
358
|
}
|
|
359
|
+
else {
|
|
360
|
+
const tag = this.#markup.getTagByValue(value);
|
|
361
|
+
tag?.remove();
|
|
362
|
+
}
|
|
292
363
|
}
|
|
293
364
|
};
|
|
294
365
|
#onClearTag = (event) => {
|
|
295
366
|
const target = event.target;
|
|
296
|
-
if (target
|
|
297
|
-
const
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
const option = this.#markup.getOptionByValue(value);
|
|
301
|
-
option.removeAttribute('selected');
|
|
302
|
-
this.#values.delete(value);
|
|
303
|
-
tag.remove();
|
|
304
|
-
this.#setValidityAndFormValue();
|
|
305
|
-
this.dispatchEvent(new Event('change'));
|
|
367
|
+
if (target) {
|
|
368
|
+
const option = this.#markup.getOptionByValue(target.value);
|
|
369
|
+
if (option) {
|
|
370
|
+
option.selected = false;
|
|
306
371
|
}
|
|
372
|
+
else {
|
|
373
|
+
const tag = this.#markup.getTagByValue(target.value);
|
|
374
|
+
this.#values.delete(target.value);
|
|
375
|
+
tag?.remove();
|
|
376
|
+
}
|
|
377
|
+
requestAnimationFrame(() => {
|
|
378
|
+
if (this.#values.size) {
|
|
379
|
+
this.focus();
|
|
380
|
+
}
|
|
381
|
+
else if (this.searchable || this.filterable) {
|
|
382
|
+
this.#markup.searchInput?.focus();
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
this.focus();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
307
388
|
}
|
|
308
389
|
};
|
|
309
|
-
#
|
|
390
|
+
#onClearAll = () => {
|
|
310
391
|
this.formResetCallback();
|
|
311
392
|
};
|
|
393
|
+
#onPickerToggle = (event) => {
|
|
394
|
+
const state = Reflect.get(event, 'newState');
|
|
395
|
+
const oldState = Reflect.get(event, 'oldState');
|
|
396
|
+
if (state === oldState)
|
|
397
|
+
return;
|
|
398
|
+
if (state === 'open') {
|
|
399
|
+
this.#prevValue = this.value;
|
|
400
|
+
this.#changed = true;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
this.#changed = this.#prevValue !== this.value;
|
|
404
|
+
this.dispatchEvent(new Event('close'));
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
#onKeyDown = (event) => {
|
|
408
|
+
if (this.#internals.ariaExpanded !== 'true')
|
|
409
|
+
return;
|
|
410
|
+
const activeElement = this.shadowRoot.activeElement;
|
|
411
|
+
if (!activeElement) {
|
|
412
|
+
// ??? how we should react ???
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (activeElement === this.#markup.searchInput && event.key === 'Enter') {
|
|
416
|
+
event.stopPropagation();
|
|
417
|
+
if (!this.searchable && this.filterable) {
|
|
418
|
+
console.log('sss');
|
|
419
|
+
this.#markup.sort(this.#markup.searchInput.value);
|
|
420
|
+
}
|
|
421
|
+
if (this.searchable) {
|
|
422
|
+
this.dispatchEvent(new Event('search'));
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (event.key === 'Enter' && activeElement instanceof HTMLComboboxOptionElement) {
|
|
427
|
+
if (!activeElement.selected) {
|
|
428
|
+
event.stopPropagation();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (event.key === 'Enter' && activeElement.part.contains('clear-tag')) {
|
|
433
|
+
// @ts-ignore
|
|
434
|
+
// this.#onClearTag(event);
|
|
435
|
+
// event.stopPropagation();
|
|
436
|
+
// event.preventDefault()
|
|
437
|
+
// @ts-ignore
|
|
438
|
+
activeElement.click();
|
|
439
|
+
// event.stopPropagation();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (event.key === 'Escape' || event.key === 'Enter') {
|
|
443
|
+
this.#markup.closeDropdown();
|
|
444
|
+
this.blur();
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
#onInput = () => {
|
|
448
|
+
this.query = this.#markup.searchInput.value;
|
|
449
|
+
if (!this.searchable && this.filterable) {
|
|
450
|
+
this.#markup.sort(this.#markup.searchInput?.value || '');
|
|
451
|
+
}
|
|
452
|
+
};
|
|
312
453
|
#setValidityAndFormValue() {
|
|
454
|
+
this.#markup.clearAllButton?.toggleAttribute('disabled', !Boolean(this.#values.size));
|
|
455
|
+
let empty = false;
|
|
456
|
+
if (this.#values.size === 0)
|
|
457
|
+
empty = true;
|
|
458
|
+
if (this.#values.size > 0 && !this.clearable && !this.multiple && (this.searchable || this.filterable)) {
|
|
459
|
+
empty = true;
|
|
460
|
+
}
|
|
461
|
+
this.#markup.searchInput?.toggleAttribute('autofocus', empty);
|
|
313
462
|
this.#internals.setFormValue(this.value);
|
|
314
463
|
if (this.required && this.value === '') {
|
|
315
|
-
this.#internals.setValidity({ valueMissing: true });
|
|
464
|
+
this.#internals.setValidity({ valueMissing: true }, 'Value missing');
|
|
316
465
|
}
|
|
317
466
|
else {
|
|
318
467
|
this.#internals.setValidity({});
|
|
@@ -346,7 +495,7 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
346
495
|
}
|
|
347
496
|
}
|
|
348
497
|
}
|
|
349
|
-
document.addEventListener('keypress', (
|
|
498
|
+
document.addEventListener('keypress', () => {
|
|
350
499
|
if (document.activeElement instanceof HTMLComboboxElement) {
|
|
351
500
|
const maybeHost = document.activeElement.shadowRoot.activeElement;
|
|
352
501
|
if (maybeHost instanceof HTMLComboboxOptionElement) {
|
|
@@ -1,15 +1,56 @@
|
|
|
1
1
|
import { toBoolean } from './Boolean.attribute.value.normalizer.js';
|
|
2
2
|
export class HTMLComboboxOptionElement extends HTMLElement {
|
|
3
|
-
static OWN_IDL = new Set(['value', 'label', 'selected']);
|
|
3
|
+
static OWN_IDL = new Set(['value', 'label', 'selected', 'disabled']);
|
|
4
|
+
static EventInit = { bubbles: true, cancelable: true, composed: true };
|
|
5
|
+
static captureFocus(event) {
|
|
6
|
+
if (event.relatedTarget && event.relatedTarget instanceof HTMLComboboxOptionElement) {
|
|
7
|
+
event.stopPropagation();
|
|
8
|
+
event.preventDefault();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
static onNavigate(event) {
|
|
12
|
+
if (!event.target ||
|
|
13
|
+
!(event.target instanceof HTMLComboboxOptionElement) ||
|
|
14
|
+
event.key === 'Tab' ||
|
|
15
|
+
event.key === 'Enter')
|
|
16
|
+
return;
|
|
17
|
+
event.preventDefault();
|
|
18
|
+
if (event.key === 'ArrowDown') {
|
|
19
|
+
const next = event.target.nextElementSibling;
|
|
20
|
+
if (next && next instanceof HTMLComboboxOptionElement) {
|
|
21
|
+
next.focus();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (event.key === 'ArrowUp') {
|
|
25
|
+
const prev = event.target.previousElementSibling;
|
|
26
|
+
if (prev && prev instanceof HTMLComboboxOptionElement) {
|
|
27
|
+
prev.focus();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
4
31
|
connectedCallback() {
|
|
32
|
+
this.addEventListener('keydown', HTMLComboboxOptionElement.onNavigate);
|
|
33
|
+
this.addEventListener('focusout', HTMLComboboxOptionElement.captureFocus);
|
|
34
|
+
this.addEventListener('click', this.#toggleSelection);
|
|
5
35
|
this.#initialAttributesSynchronization();
|
|
6
36
|
this.part.add('option');
|
|
7
37
|
super.setAttribute('tabindex', "0");
|
|
8
38
|
super.setAttribute('role', "option");
|
|
9
|
-
if (!this.value)
|
|
10
|
-
this.value = this.
|
|
11
|
-
|
|
39
|
+
if (!this.value)
|
|
40
|
+
this.value = this.label;
|
|
41
|
+
if (!this.textContent)
|
|
42
|
+
this.textContent = this.value;
|
|
43
|
+
this.dispatchEvent(new Event('optionselectedstatechange', HTMLComboboxOptionElement.EventInit));
|
|
12
44
|
}
|
|
45
|
+
disconnectedCallback() {
|
|
46
|
+
this.removeEventListener('click', this.#toggleSelection);
|
|
47
|
+
this.removeEventListener('keydown', HTMLComboboxOptionElement.onNavigate);
|
|
48
|
+
this.removeEventListener('focusout', HTMLComboboxOptionElement.captureFocus);
|
|
49
|
+
}
|
|
50
|
+
#toggleSelection = (event) => {
|
|
51
|
+
event.stopPropagation();
|
|
52
|
+
this.selected = !this.selected;
|
|
53
|
+
};
|
|
13
54
|
get value() {
|
|
14
55
|
return this.getAttribute('value');
|
|
15
56
|
}
|
|
@@ -30,7 +71,26 @@ export class HTMLComboboxOptionElement extends HTMLElement {
|
|
|
30
71
|
return this.hasAttribute('selected');
|
|
31
72
|
}
|
|
32
73
|
set selected(value) {
|
|
33
|
-
|
|
74
|
+
const prev = this.selected;
|
|
75
|
+
const force = toBoolean(value);
|
|
76
|
+
if (prev === force) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
super.toggleAttribute('selected', force);
|
|
80
|
+
this.part.toggle('selected', force);
|
|
81
|
+
// console.log('this.isConnected', this.isConnected)
|
|
82
|
+
if (this.isConnected) {
|
|
83
|
+
this.dispatchEvent(new Event('optionselectedstatechange', HTMLComboboxOptionElement.EventInit));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
get disabled() {
|
|
87
|
+
return this.hasAttribute('disabled');
|
|
88
|
+
}
|
|
89
|
+
set disabled(value) {
|
|
90
|
+
const force = toBoolean(value);
|
|
91
|
+
super.toggleAttribute('disabled', force);
|
|
92
|
+
this.part.toggle('disabled', force);
|
|
93
|
+
super.toggleAttribute('inert', force);
|
|
34
94
|
}
|
|
35
95
|
#initialAttributesSynchronization() {
|
|
36
96
|
for (const key of HTMLComboboxOptionElement.OWN_IDL) {
|
|
@@ -53,6 +113,11 @@ export class HTMLComboboxOptionElement extends HTMLElement {
|
|
|
53
113
|
super.removeAttribute(name);
|
|
54
114
|
}
|
|
55
115
|
}
|
|
116
|
+
toggleAttribute(name, force) {
|
|
117
|
+
if (name === 'selected')
|
|
118
|
+
this.part.toggle(name, force);
|
|
119
|
+
return super.toggleAttribute(name, force);
|
|
120
|
+
}
|
|
56
121
|
}
|
|
57
122
|
if (!window.customElements.get('box-option')) {
|
|
58
123
|
window.customElements.define('box-option', HTMLComboboxOptionElement);
|
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
export class HTMLComboboxTagElement extends HTMLElement {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
static EventInit = { bubbles: true, cancelable: true, composed: true };
|
|
3
|
+
get value() {
|
|
4
|
+
return this.getAttribute('value') || '';
|
|
4
5
|
}
|
|
5
6
|
connectedCallback() {
|
|
6
7
|
this.part.add('tag');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
const control = this.querySelector('[part*="clear-tag"]');
|
|
9
|
+
if (!this.hasAttribute('visible')) {
|
|
10
|
+
super.setAttribute('tabindex', '0');
|
|
11
|
+
}
|
|
12
|
+
control?.addEventListener('focusout', HTMLComboboxTagElement.captureFocus);
|
|
13
|
+
control?.addEventListener('click', HTMLComboboxTagElement.onClearTag);
|
|
14
|
+
const root = this.getRootNode();
|
|
15
|
+
if (root?.host.hasAttribute('multiple')) {
|
|
16
|
+
if (!control) {
|
|
17
|
+
throw new Error(`A <button> with part="clear-tag" is required for <combo-box> with multiple attribute`);
|
|
12
18
|
}
|
|
13
19
|
}
|
|
14
20
|
}
|
|
21
|
+
disconnectedCallback() {
|
|
22
|
+
const control = this.querySelector('[part*="clear-tag"]');
|
|
23
|
+
control?.removeEventListener('focusout', HTMLComboboxTagElement.captureFocus);
|
|
24
|
+
control?.removeEventListener('click', HTMLComboboxTagElement.onClearTag);
|
|
25
|
+
}
|
|
26
|
+
static onClearTag(event) {
|
|
27
|
+
const target = event.target;
|
|
28
|
+
target?.parentElement?.dispatchEvent(new Event('tagcleared', HTMLComboboxTagElement.EventInit));
|
|
29
|
+
}
|
|
30
|
+
static captureFocus(event) {
|
|
31
|
+
event.stopPropagation();
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
}
|
|
15
34
|
}
|
|
16
35
|
if (!window.customElements.get('box-tag')) {
|
|
17
36
|
window.customElements.define('box-tag', HTMLComboboxTagElement);
|