html-combobox-element 0.0.2-beta.1 → 0.0.2-beta.11
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 +1 -0
- package/combobox-skill/SKILL.md +43 -0
- package/combobox-skill/custom-box-tag.md +17 -0
- package/combobox-skill/customization.md +23 -0
- package/combobox-skill/legacy-react-restrictions.md +14 -0
- package/combobox-skill/share-styles.md +17 -0
- package/dist/cjs/combobox/Combobox.markup.js +228 -118
- package/dist/cjs/combobox/HTML.combobox.element.js +192 -112
- package/dist/cjs/combobox/HTML.combobox.option.element.js +86 -5
- package/dist/cjs/combobox/HTML.combobox.tag.element.js +27 -7
- package/dist/cjs/index.js +0 -62
- package/dist/esm/combobox/Combobox.markup.js +228 -118
- package/dist/esm/combobox/HTML.combobox.element.js +192 -112
- package/dist/esm/combobox/HTML.combobox.option.element.js +86 -5
- package/dist/esm/combobox/HTML.combobox.tag.element.js +27 -7
- package/dist/esm/index.js +0 -62
- package/dist/types/combobox/Combobox.markup.d.ts +14 -8
- package/dist/types/combobox/Combobox.markup.d.ts.map +1 -1
- package/dist/types/combobox/HTML.combobox.element.d.ts +5 -3
- 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/package.json +3 -2
|
@@ -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,85 @@ 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
|
-
|
|
23
|
+
// console.log(ComboboxMarkup.markup)
|
|
24
|
+
this.shadowRoot.appendChild(ComboboxMarkup.markup.content.cloneNode(true));
|
|
25
|
+
// this.shadowRoot.innerHTML = ComboboxMarkup.template;
|
|
22
26
|
this.shadowRoot.adoptedStyleSheets = HTMLComboboxElement.styleSheet;
|
|
23
27
|
this.#observer = new MutationObserver(this.#onOptionsChanges);
|
|
28
|
+
this.#markup.connect();
|
|
24
29
|
}
|
|
30
|
+
// static c = false
|
|
25
31
|
// Lifecycle callbacks
|
|
26
32
|
connectedCallback() {
|
|
27
|
-
|
|
33
|
+
// super.setAttribute('tabindex', '0');
|
|
34
|
+
this.#markup.onHostConnected();
|
|
35
|
+
this.shadowRoot.addEventListener('optionselectedstatechange', this.#onOptionSelectedStateChanges);
|
|
36
|
+
this.shadowRoot.addEventListener('tagcleared', this.#onClearTag);
|
|
37
|
+
// this.#onOptionsChanges([{ addedNodes: Array.from(this.children) }] as unknown as MutationRecord[]);
|
|
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
|
+
// if (!HTMLComboboxElement.c) {
|
|
46
|
+
// HTMLComboboxElement.c = true;
|
|
47
|
+
// this.showPicker()
|
|
48
|
+
// }
|
|
49
|
+
}
|
|
50
|
+
insertBefore = (node, referenceNode) => {
|
|
51
|
+
// Since we just move options to the #markup.optionsContainer, they are no more part of this children
|
|
52
|
+
// next time, when someone is calling this(combo-box).insertBefore, an error will be thrown:
|
|
53
|
+
// Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
|
|
54
|
+
// Here we overwrite this method, and if new node is HTMLComboboxOptionElement,
|
|
55
|
+
// we just delegate this job to the real option parent: optionsContainer
|
|
56
|
+
if (node instanceof HTMLComboboxOptionElement || node instanceof HTMLOptGroupElement) {
|
|
57
|
+
this.#markup.optionsContainer?.insertBefore(node, referenceNode);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
super.insertBefore(node, referenceNode);
|
|
61
|
+
}
|
|
62
|
+
return node;
|
|
63
|
+
};
|
|
64
|
+
appendChild(node) {
|
|
65
|
+
return this.insertBefore(node, null);
|
|
66
|
+
}
|
|
67
|
+
removeChild(child) {
|
|
68
|
+
// console.log(child, 'removed')
|
|
69
|
+
return child;
|
|
70
|
+
}
|
|
71
|
+
get changed() {
|
|
72
|
+
return this.#changed;
|
|
33
73
|
}
|
|
34
74
|
disconnectedCallback() {
|
|
35
75
|
this.#observer.disconnect();
|
|
36
76
|
this.#markup.disconnect();
|
|
37
|
-
this
|
|
77
|
+
this.removeEventListener('keydown', this.#onKeyDown);
|
|
78
|
+
this.#markup.clearAllButton.removeEventListener('click', this.#onClearAll);
|
|
38
79
|
this.#markup.searchInput.removeEventListener('input', this.#onInput);
|
|
80
|
+
this.#markup.dropdown.removeEventListener('toggle', this.#onPickerToggle);
|
|
81
|
+
this.shadowRoot.removeEventListener('optionselectedstatechange', this.#onOptionSelectedStateChanges);
|
|
82
|
+
this.shadowRoot.removeEventListener('tagcleared', this.#onClearTag);
|
|
39
83
|
}
|
|
40
84
|
formResetCallback() {
|
|
41
|
-
this.#values = new Set;
|
|
42
85
|
this.selectedOptions.forEach(option => option.selected = false);
|
|
43
|
-
this
|
|
44
|
-
this.#
|
|
45
|
-
this.dispatchEvent(new Event('change'));
|
|
86
|
+
this.dispatchEvent(new Event('reset'));
|
|
87
|
+
requestAnimationFrame(() => this.#markup.searchInput?.focus());
|
|
46
88
|
}
|
|
47
|
-
formDisabledCallback(
|
|
48
|
-
|
|
89
|
+
formDisabledCallback() {
|
|
90
|
+
// toDo
|
|
91
|
+
// when inside fieldset, this callback is only invoked for the first time
|
|
92
|
+
// isDisabled: boolean
|
|
49
93
|
}
|
|
50
94
|
// Instance properties
|
|
51
95
|
// <combo-box> specific properties
|
|
@@ -62,22 +106,17 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
62
106
|
value = '';
|
|
63
107
|
value = String(value);
|
|
64
108
|
super.setAttribute('query', value);
|
|
65
|
-
|
|
66
|
-
this.#markup.searchInput.value = value;
|
|
67
|
-
}
|
|
109
|
+
this.#markup?.setInputValue(value);
|
|
68
110
|
}
|
|
69
111
|
get placeholder() {
|
|
70
112
|
return this.getAttribute('placeholder');
|
|
71
113
|
}
|
|
72
114
|
set placeholder(value) {
|
|
73
115
|
if (value == null)
|
|
74
|
-
value = '';
|
|
116
|
+
value = ' ';
|
|
75
117
|
value = String(value);
|
|
76
118
|
super.setAttribute('placeholder', value);
|
|
77
|
-
|
|
78
|
-
this.#markup.placeholder.innerText = value;
|
|
79
|
-
this.#markup.searchInput.placeholder = value;
|
|
80
|
-
}
|
|
119
|
+
this.#markup?.setPlaceholder(value);
|
|
81
120
|
}
|
|
82
121
|
get clearable() {
|
|
83
122
|
return this.hasAttribute('clearable');
|
|
@@ -103,7 +142,9 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
103
142
|
}
|
|
104
143
|
set disabled(value) {
|
|
105
144
|
this.#internals.ariaDisabled = String(value);
|
|
106
|
-
|
|
145
|
+
value = toBoolean(value);
|
|
146
|
+
super.toggleAttribute('disabled', value);
|
|
147
|
+
super.toggleAttribute('inert', value);
|
|
107
148
|
}
|
|
108
149
|
get form() {
|
|
109
150
|
return this.#internals.form;
|
|
@@ -154,34 +195,11 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
154
195
|
set value(value) {
|
|
155
196
|
if (this.value === value || typeof value !== 'string')
|
|
156
197
|
return;
|
|
157
|
-
const prevValue = new Set(this.#values);
|
|
158
|
-
this.#values = new Set;
|
|
159
198
|
const values = value.split(',').filter(Boolean);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (this.#values.size === 0) {
|
|
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
|
-
}
|
|
199
|
+
this.#values = new Set(values);
|
|
200
|
+
this.#markup.options.forEach(option => {
|
|
201
|
+
const value = option.value;
|
|
202
|
+
option.selected = this.#values.has(value);
|
|
185
203
|
});
|
|
186
204
|
}
|
|
187
205
|
// Instance methods
|
|
@@ -228,91 +246,152 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
228
246
|
}
|
|
229
247
|
}
|
|
230
248
|
// 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
249
|
#onOptionsChanges = (records) => {
|
|
250
|
+
// console.log(records)
|
|
239
251
|
records.forEach(record => {
|
|
240
|
-
record.
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
this.#selectOption(node);
|
|
249
|
-
}
|
|
252
|
+
record.removedNodes.forEach(option => {
|
|
253
|
+
if (option instanceof HTMLComboboxOptionElement) {
|
|
254
|
+
// console.log(e)
|
|
255
|
+
const value = option.value;
|
|
256
|
+
if (this.#values.has(value)) {
|
|
257
|
+
this.#values.delete(value);
|
|
258
|
+
const control = this.#markup.getTagByValue(value);
|
|
259
|
+
control?.remove();
|
|
250
260
|
}
|
|
251
261
|
}
|
|
252
|
-
if (node instanceof HTMLComboboxOptionElement || node instanceof HTMLOptGroupElement) {
|
|
253
|
-
this.#markup.optionsContainer.append(node);
|
|
254
|
-
}
|
|
255
262
|
});
|
|
263
|
+
// record.removedNodes.forEach(node => {
|
|
264
|
+
// console.log(node);
|
|
265
|
+
// })
|
|
256
266
|
});
|
|
257
|
-
this.#markup.invalidateOptionsCache();
|
|
258
|
-
this.#setValidityAndFormValue();
|
|
259
267
|
};
|
|
260
|
-
#
|
|
261
|
-
|
|
262
|
-
return;
|
|
268
|
+
#onOptionSelectedStateChanges = (event) => {
|
|
269
|
+
const option = event.target;
|
|
263
270
|
const value = option.value;
|
|
264
|
-
|
|
265
|
-
option.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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);
|
|
271
|
+
// console.log(event, value, option)
|
|
272
|
+
if (option.selected) {
|
|
273
|
+
if (!this.multiple) {
|
|
274
|
+
this.#values.forEach(value => {
|
|
275
|
+
const prevOption = this.#markup.getOptionByValue(value);
|
|
276
|
+
if (prevOption) {
|
|
277
|
+
prevOption.selected = false;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (!this.#values.has(value)) {
|
|
282
|
+
this.#values.add(value);
|
|
283
|
+
this.#markup.createAndAppendTag(option);
|
|
289
284
|
this.#setValidityAndFormValue();
|
|
290
285
|
this.dispatchEvent(new Event('change'));
|
|
291
286
|
}
|
|
292
287
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const target = event.target;
|
|
296
|
-
if (target && target instanceof HTMLElement) {
|
|
297
|
-
const tag = target.closest('box-tag');
|
|
298
|
-
if (tag) {
|
|
299
|
-
const value = tag.getAttribute('value');
|
|
300
|
-
const option = this.#markup.getOptionByValue(value);
|
|
301
|
-
option.removeAttribute('selected');
|
|
288
|
+
else {
|
|
289
|
+
if (this.#values.has(value)) {
|
|
302
290
|
this.#values.delete(value);
|
|
303
|
-
|
|
291
|
+
const control = this.#markup.getTagByValue(value);
|
|
292
|
+
control?.remove();
|
|
304
293
|
this.#setValidityAndFormValue();
|
|
305
294
|
this.dispatchEvent(new Event('change'));
|
|
306
295
|
}
|
|
307
296
|
}
|
|
308
297
|
};
|
|
309
|
-
#
|
|
298
|
+
#onClearTag = (event) => {
|
|
299
|
+
const target = event.target;
|
|
300
|
+
if (target) {
|
|
301
|
+
const option = this.#markup.getOptionByValue(target.value);
|
|
302
|
+
if (option)
|
|
303
|
+
option.selected = false;
|
|
304
|
+
requestAnimationFrame(() => {
|
|
305
|
+
if (this.#values.size) {
|
|
306
|
+
this.focus();
|
|
307
|
+
}
|
|
308
|
+
else if (this.searchable || this.filterable) {
|
|
309
|
+
this.#markup.searchInput?.focus();
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
this.focus();
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
#onClearAll = () => {
|
|
310
318
|
this.formResetCallback();
|
|
311
319
|
};
|
|
320
|
+
#onPickerToggle = (event) => {
|
|
321
|
+
const state = Reflect.get(event, 'newState');
|
|
322
|
+
const oldState = Reflect.get(event, 'oldState');
|
|
323
|
+
if (state === oldState)
|
|
324
|
+
return;
|
|
325
|
+
// console.log(event)
|
|
326
|
+
if (state === 'open') {
|
|
327
|
+
this.#prevValue = this.value;
|
|
328
|
+
this.#changed = true;
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
this.#changed = this.#prevValue !== this.value;
|
|
332
|
+
this.dispatchEvent(new Event('close'));
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
#onKeyDown = (event) => {
|
|
336
|
+
if (this.#internals.ariaExpanded !== 'true')
|
|
337
|
+
return;
|
|
338
|
+
const activeElement = this.shadowRoot.activeElement;
|
|
339
|
+
if (!activeElement) {
|
|
340
|
+
// ??? how we should react ???
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (activeElement === this.#markup.searchInput && event.key === 'Enter') {
|
|
344
|
+
event.stopPropagation();
|
|
345
|
+
if (!this.searchable && this.filterable) {
|
|
346
|
+
console.log('sss');
|
|
347
|
+
this.#markup.sort(this.#markup.searchInput.value);
|
|
348
|
+
}
|
|
349
|
+
if (this.searchable) {
|
|
350
|
+
this.dispatchEvent(new Event('search'));
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (event.key === 'Enter' && activeElement instanceof HTMLComboboxOptionElement) {
|
|
355
|
+
if (!activeElement.selected) {
|
|
356
|
+
event.stopPropagation();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (event.key === 'Enter' && activeElement.part.contains('clear-tag')) {
|
|
361
|
+
// @ts-ignore
|
|
362
|
+
// this.#onClearTag(event);
|
|
363
|
+
// event.stopPropagation();
|
|
364
|
+
// event.preventDefault()
|
|
365
|
+
// @ts-ignore
|
|
366
|
+
activeElement.click();
|
|
367
|
+
// event.stopPropagation();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (event.key === 'Escape' || event.key === 'Enter') {
|
|
371
|
+
this.#markup.closeDropdown();
|
|
372
|
+
this.blur();
|
|
373
|
+
}
|
|
374
|
+
// console.log(event, this.#internals.ariaExpanded)
|
|
375
|
+
// if (event.key !== 'Enter') return;
|
|
376
|
+
};
|
|
377
|
+
#onInput = () => {
|
|
378
|
+
this.query = this.#markup.searchInput.value;
|
|
379
|
+
if (!this.searchable && this.filterable) {
|
|
380
|
+
this.#markup.sort(this.#markup.searchInput?.value || '');
|
|
381
|
+
}
|
|
382
|
+
};
|
|
312
383
|
#setValidityAndFormValue() {
|
|
384
|
+
this.#markup.clearAllButton?.toggleAttribute('disabled', !Boolean(this.#values.size));
|
|
385
|
+
let empty = false;
|
|
386
|
+
if (this.#values.size === 0)
|
|
387
|
+
empty = true;
|
|
388
|
+
if (this.#values.size > 0 && !this.clearable && !this.multiple && (this.searchable || this.filterable)) {
|
|
389
|
+
empty = true;
|
|
390
|
+
}
|
|
391
|
+
this.#markup.searchInput?.toggleAttribute('autofocus', empty);
|
|
313
392
|
this.#internals.setFormValue(this.value);
|
|
314
393
|
if (this.required && this.value === '') {
|
|
315
|
-
this.#internals.setValidity({ valueMissing: true });
|
|
394
|
+
this.#internals.setValidity({ valueMissing: true }, 'Value missing');
|
|
316
395
|
}
|
|
317
396
|
else {
|
|
318
397
|
this.#internals.setValidity({});
|
|
@@ -320,6 +399,7 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
320
399
|
}
|
|
321
400
|
#initialAttributesSynchronization() {
|
|
322
401
|
for (const key of HTMLComboboxElement.OWN_IDL) {
|
|
402
|
+
// @ts-ignore
|
|
323
403
|
this[key] = this.getAttribute(key);
|
|
324
404
|
}
|
|
325
405
|
}
|
|
@@ -1,15 +1,67 @@
|
|
|
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
|
+
// event.stopPropagation();
|
|
13
|
+
// const target = event.target as HTMLComboboxOptionElement;
|
|
14
|
+
// if (event.key === 'Enter') {
|
|
15
|
+
// event.preventDefault();
|
|
16
|
+
// target.click();
|
|
17
|
+
// return;
|
|
18
|
+
// }
|
|
19
|
+
// if (event.key === 'Tab') return;
|
|
20
|
+
// event.preventDefault();
|
|
21
|
+
// console.log(event.target)
|
|
22
|
+
if (!event.target ||
|
|
23
|
+
!(event.target instanceof HTMLComboboxOptionElement) ||
|
|
24
|
+
event.key === 'Tab' ||
|
|
25
|
+
event.key === 'Enter')
|
|
26
|
+
return;
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
if (event.key === 'ArrowDown') {
|
|
29
|
+
const next = event.target.nextElementSibling;
|
|
30
|
+
if (next && next instanceof HTMLComboboxOptionElement) {
|
|
31
|
+
next.focus();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (event.key === 'ArrowUp') {
|
|
35
|
+
const prev = event.target.previousElementSibling;
|
|
36
|
+
if (prev && prev instanceof HTMLComboboxOptionElement) {
|
|
37
|
+
prev.focus();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
4
41
|
connectedCallback() {
|
|
42
|
+
this.addEventListener('keydown', HTMLComboboxOptionElement.onNavigate);
|
|
43
|
+
this.addEventListener('focusout', HTMLComboboxOptionElement.captureFocus);
|
|
44
|
+
this.addEventListener('click', this.#toggleSelection);
|
|
5
45
|
this.#initialAttributesSynchronization();
|
|
6
46
|
this.part.add('option');
|
|
7
47
|
super.setAttribute('tabindex', "0");
|
|
8
48
|
super.setAttribute('role', "option");
|
|
9
|
-
if (!this.value)
|
|
10
|
-
this.value = this.
|
|
11
|
-
|
|
49
|
+
if (!this.value)
|
|
50
|
+
this.value = this.label;
|
|
51
|
+
if (!this.textContent)
|
|
52
|
+
this.textContent = this.value;
|
|
53
|
+
this.dispatchEvent(new Event('optionselectedstatechange', HTMLComboboxOptionElement.EventInit));
|
|
12
54
|
}
|
|
55
|
+
disconnectedCallback() {
|
|
56
|
+
this.removeEventListener('click', this.#toggleSelection);
|
|
57
|
+
this.removeEventListener('keydown', HTMLComboboxOptionElement.onNavigate);
|
|
58
|
+
this.removeEventListener('focusout', HTMLComboboxOptionElement.captureFocus);
|
|
59
|
+
}
|
|
60
|
+
#toggleSelection = (event) => {
|
|
61
|
+
// console.log('#toggleSelection', this.value)
|
|
62
|
+
event.stopPropagation();
|
|
63
|
+
this.selected = !this.selected;
|
|
64
|
+
};
|
|
13
65
|
get value() {
|
|
14
66
|
return this.getAttribute('value');
|
|
15
67
|
}
|
|
@@ -30,15 +82,38 @@ export class HTMLComboboxOptionElement extends HTMLElement {
|
|
|
30
82
|
return this.hasAttribute('selected');
|
|
31
83
|
}
|
|
32
84
|
set selected(value) {
|
|
33
|
-
|
|
85
|
+
const prev = this.selected;
|
|
86
|
+
const force = toBoolean(value);
|
|
87
|
+
if (prev === force) {
|
|
88
|
+
// console.log(prev, value, this.value)
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
super.toggleAttribute('selected', force);
|
|
92
|
+
this.part.toggle('selected', force);
|
|
93
|
+
// this.dispatchEvent(new Event('optionselectedstatechange', HTMLComboboxOptionElement.EventInit));
|
|
94
|
+
if (this.isConnected) {
|
|
95
|
+
// console.log('new optionselectedstatechange', this.value)
|
|
96
|
+
this.dispatchEvent(new Event('optionselectedstatechange', HTMLComboboxOptionElement.EventInit));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
get disabled() {
|
|
100
|
+
return this.hasAttribute('disabled');
|
|
101
|
+
}
|
|
102
|
+
set disabled(value) {
|
|
103
|
+
const force = toBoolean(value);
|
|
104
|
+
super.toggleAttribute('disabled', force);
|
|
105
|
+
this.part.toggle('disabled', force);
|
|
106
|
+
super.toggleAttribute('inert', force);
|
|
34
107
|
}
|
|
35
108
|
#initialAttributesSynchronization() {
|
|
36
109
|
for (const key of HTMLComboboxOptionElement.OWN_IDL) {
|
|
110
|
+
// @ts-ignore
|
|
37
111
|
this[key] = this.getAttribute(key);
|
|
38
112
|
}
|
|
39
113
|
}
|
|
40
114
|
setAttribute(name, value) {
|
|
41
115
|
if (HTMLComboboxOptionElement.OWN_IDL.has(name)) {
|
|
116
|
+
// @ts-ignore
|
|
42
117
|
this[name] = value;
|
|
43
118
|
}
|
|
44
119
|
else {
|
|
@@ -47,12 +122,18 @@ export class HTMLComboboxOptionElement extends HTMLElement {
|
|
|
47
122
|
}
|
|
48
123
|
removeAttribute(name) {
|
|
49
124
|
if (HTMLComboboxOptionElement.OWN_IDL.has(name)) {
|
|
125
|
+
// @ts-ignore
|
|
50
126
|
this[name] = null;
|
|
51
127
|
}
|
|
52
128
|
else {
|
|
53
129
|
super.removeAttribute(name);
|
|
54
130
|
}
|
|
55
131
|
}
|
|
132
|
+
toggleAttribute(name, force) {
|
|
133
|
+
if (name === 'selected')
|
|
134
|
+
this.part.toggle(name, force);
|
|
135
|
+
return super.toggleAttribute(name, force);
|
|
136
|
+
}
|
|
56
137
|
}
|
|
57
138
|
if (!window.customElements.get('box-option')) {
|
|
58
139
|
window.customElements.define('box-option', HTMLComboboxOptionElement);
|
|
@@ -1,17 +1,37 @@
|
|
|
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
|
+
// @ts-ignore
|
|
10
|
+
if (!this.hasAttribute('visible')) {
|
|
11
|
+
super.setAttribute('tabindex', '0');
|
|
12
|
+
}
|
|
13
|
+
control?.addEventListener('focusout', HTMLComboboxTagElement.captureFocus);
|
|
14
|
+
control?.addEventListener('click', HTMLComboboxTagElement.onClearTag);
|
|
15
|
+
const root = this.getRootNode();
|
|
16
|
+
if (root instanceof ShadowRoot) {
|
|
17
|
+
if (root.host.hasAttribute('multiple') && !control) {
|
|
18
|
+
throw new Error(`A <button> with part="clear-tag" is required for <combo-box> with multiple attribute`);
|
|
12
19
|
}
|
|
13
20
|
}
|
|
14
21
|
}
|
|
22
|
+
disconnectedCallback() {
|
|
23
|
+
const control = this.querySelector('[part*="clear-tag"]');
|
|
24
|
+
control?.removeEventListener('focusout', HTMLComboboxTagElement.captureFocus);
|
|
25
|
+
control?.removeEventListener('click', HTMLComboboxTagElement.onClearTag);
|
|
26
|
+
}
|
|
27
|
+
static onClearTag(event) {
|
|
28
|
+
const [target] = event.composedPath().filter(el => el instanceof HTMLComboboxTagElement);
|
|
29
|
+
target?.dispatchEvent(new Event('tagcleared', HTMLComboboxTagElement.EventInit));
|
|
30
|
+
}
|
|
31
|
+
static captureFocus(event) {
|
|
32
|
+
event.stopPropagation();
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
}
|
|
15
35
|
}
|
|
16
36
|
if (!window.customElements.get('box-tag')) {
|
|
17
37
|
window.customElements.define('box-tag', HTMLComboboxTagElement);
|
package/dist/esm/index.js
CHANGED
|
@@ -3,65 +3,3 @@
|
|
|
3
3
|
/// <reference types="react" />
|
|
4
4
|
/// <reference types="vue" />
|
|
5
5
|
export * from './combobox/index.js';
|
|
6
|
-
// declare global {
|
|
7
|
-
// namespace React {
|
|
8
|
-
// namespace JSX {
|
|
9
|
-
// interface IntrinsicElements {
|
|
10
|
-
// 'combo-box': React.DetailedHTMLProps<Omit<React.HTMLAttributes<HTMLComboboxElement>, 'defaultValue'>,
|
|
11
|
-
// HTMLComboboxElement
|
|
12
|
-
// > & ComboboxJsxAttributes;
|
|
13
|
-
//
|
|
14
|
-
// 'box-option': React.DetailedHTMLProps<
|
|
15
|
-
// React.HTMLAttributes<HTMLComboboxOptionElement>,
|
|
16
|
-
// HTMLComboboxOptionElement>
|
|
17
|
-
// & ComboboxOptionJsxAttributes;
|
|
18
|
-
//
|
|
19
|
-
// 'box-tag': React.DetailedHTMLProps<React.HTMLAttributes<HTMLComboboxTagElement>, HTMLComboboxTagElement>;
|
|
20
|
-
// }
|
|
21
|
-
// }
|
|
22
|
-
// }
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// namespace preact {
|
|
26
|
-
// namespace JSX {
|
|
27
|
-
// interface IntrinsicElements {
|
|
28
|
-
// 'combo-box': preact.HTMLAttributes<HTMLComboboxElement> & ComboboxJsxAttributes;
|
|
29
|
-
// 'box-option': preact.HTMLAttributes<HTMLComboboxOptionElement> & ComboboxOptionJsxAttributes;
|
|
30
|
-
// 'box-tag': preact.HTMLAttributes<HTMLComboboxTagElement>;
|
|
31
|
-
// }
|
|
32
|
-
// }
|
|
33
|
-
// }
|
|
34
|
-
//
|
|
35
|
-
// }
|
|
36
|
-
//
|
|
37
|
-
// declare module 'preact' {
|
|
38
|
-
// namespace JSX {
|
|
39
|
-
// interface IntrinsicElements {
|
|
40
|
-
// 'combo-box': preact.HTMLAttributes<HTMLComboboxElement> & ComboboxJsxAttributes;
|
|
41
|
-
// 'box-option': preact.HTMLAttributes<HTMLComboboxOptionElement> & ComboboxOptionJsxAttributes;
|
|
42
|
-
// 'box-tag': preact.HTMLAttributes<HTMLComboboxTagElement>;
|
|
43
|
-
// }
|
|
44
|
-
// }
|
|
45
|
-
// }
|
|
46
|
-
//
|
|
47
|
-
// // Solid
|
|
48
|
-
// declare module 'solid-js' {
|
|
49
|
-
// namespace JSX {
|
|
50
|
-
// interface IntrinsicElements {
|
|
51
|
-
// 'combo-box': HTMLAttributes<HTMLComboboxElement> & ComboboxJsxAttributes
|
|
52
|
-
// 'box-option': HTMLAttributes<HTMLComboboxOptionElement> & ComboboxOptionJsxAttributes
|
|
53
|
-
// 'box-tag': HTMLAttributes<HTMLElement>
|
|
54
|
-
// }
|
|
55
|
-
// }
|
|
56
|
-
// }
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// declare global {
|
|
60
|
-
// namespace JSX {
|
|
61
|
-
// interface IntrinsicElements {
|
|
62
|
-
// 'combo-box': ComboboxJsxAttributes;
|
|
63
|
-
// 'box-option': ComboboxOptionJsxAttributes;
|
|
64
|
-
// 'box-tag': HTMLElement;
|
|
65
|
-
// }
|
|
66
|
-
// }
|
|
67
|
-
// }
|