html-combobox-element 0.0.4 → 0.0.5
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/cjs/combobox/Combobox.markup.js +1 -1
- package/dist/cjs/combobox/HTML.combobox.element.js +25 -1
- package/dist/cjs/combobox/HTML.combobox.option.element.js +10 -3
- package/dist/esm/combobox/Combobox.markup.js +1 -1
- package/dist/esm/combobox/HTML.combobox.element.js +25 -1
- package/dist/esm/combobox/HTML.combobox.option.element.js +10 -3
- package/package.json +1 -1
|
@@ -425,7 +425,7 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
425
425
|
|
|
426
426
|
<div id="dropdown" popover="manual" part='dropdown' >
|
|
427
427
|
<input placeholder='Поиск' name="search-input" part="search-input" inputmode='search' autofocus />
|
|
428
|
-
<div part="options" id='options'></div>
|
|
428
|
+
<div part="options" id='options' role="listbox"></div>
|
|
429
429
|
</div>
|
|
430
430
|
|
|
431
431
|
<template id='tag-template'>
|
|
@@ -21,13 +21,13 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
21
21
|
super();
|
|
22
22
|
this.#internals = this.attachInternals();
|
|
23
23
|
this.#internals.role = "combobox";
|
|
24
|
-
this.#internals.ariaHasPopup = "dialog";
|
|
25
24
|
this.shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: true }); //delegatesFocus: true
|
|
26
25
|
this.#markup = new Combobox_markup_js_1.ComboboxMarkup(this.shadowRoot, this.#internals);
|
|
27
26
|
this.shadowRoot.appendChild(Combobox_markup_js_1.ComboboxMarkup.markup.content.cloneNode(true));
|
|
28
27
|
this.shadowRoot.adoptedStyleSheets = _a.styleSheet;
|
|
29
28
|
this.#observer = new MutationObserver(this.#onOptionsChanges);
|
|
30
29
|
this.#markup.connect();
|
|
30
|
+
this.#syncPopupSemantics();
|
|
31
31
|
}
|
|
32
32
|
// Lifecycle callbacks
|
|
33
33
|
connectedCallback() {
|
|
@@ -184,12 +184,14 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
184
184
|
}
|
|
185
185
|
set filterable(value) {
|
|
186
186
|
super.toggleAttribute('filterable', (0, Boolean_attribute_value_normalizer_js_1.toBoolean)(value));
|
|
187
|
+
this.#syncPopupSemantics();
|
|
187
188
|
}
|
|
188
189
|
get searchable() {
|
|
189
190
|
return this.hasAttribute('searchable');
|
|
190
191
|
}
|
|
191
192
|
set searchable(value) {
|
|
192
193
|
super.toggleAttribute('searchable', (0, Boolean_attribute_value_normalizer_js_1.toBoolean)(value));
|
|
194
|
+
this.#syncPopupSemantics();
|
|
193
195
|
}
|
|
194
196
|
// <select> specific properties (implements HTMLSelectElement)
|
|
195
197
|
get disabled() {
|
|
@@ -215,6 +217,7 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
215
217
|
}
|
|
216
218
|
set multiple(value) {
|
|
217
219
|
super.toggleAttribute('multiple', (0, Boolean_attribute_value_normalizer_js_1.toBoolean)(value));
|
|
220
|
+
this.#syncPopupSemantics();
|
|
218
221
|
}
|
|
219
222
|
// get options
|
|
220
223
|
// (there is no constructor of HTMLOptionsCollection, need to implement)
|
|
@@ -400,6 +403,7 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
400
403
|
if (state === oldState)
|
|
401
404
|
return;
|
|
402
405
|
if (state === 'open') {
|
|
406
|
+
this.#syncPopupSemantics();
|
|
403
407
|
this.#prevValue = this.value;
|
|
404
408
|
this.#changed = true;
|
|
405
409
|
}
|
|
@@ -454,6 +458,26 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
454
458
|
this.#markup.sort(this.#markup.searchInput?.value || '');
|
|
455
459
|
}
|
|
456
460
|
};
|
|
461
|
+
// Without a search field the popup is just the listbox. With one, it is a dialog holding the field and the listbox.
|
|
462
|
+
#syncPopupSemantics() {
|
|
463
|
+
const withSearch = this.searchable || this.filterable;
|
|
464
|
+
this.#internals.ariaHasPopup = withSearch ? 'dialog' : 'listbox';
|
|
465
|
+
if (withSearch) {
|
|
466
|
+
this.#markup?.dropdown?.setAttribute('role', 'dialog');
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
this.#markup?.dropdown?.removeAttribute('role');
|
|
470
|
+
}
|
|
471
|
+
this.#markup?.optionsContainer?.setAttribute('aria-multiselectable', String(this.multiple));
|
|
472
|
+
// Shadow content can't point at the outer <label> with aria-labelledby, so it borrows the label's text.
|
|
473
|
+
const name = Array.from(this.#internals.labels ?? [], (label) => label.textContent?.trim()).filter(Boolean).join(' ');
|
|
474
|
+
for (const popup of [this.#markup?.dropdown, this.#markup?.optionsContainer]) {
|
|
475
|
+
if (name && popup?.hasAttribute('role'))
|
|
476
|
+
popup.setAttribute('aria-label', name);
|
|
477
|
+
else
|
|
478
|
+
popup?.removeAttribute('aria-label');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
457
481
|
#setValidityAndFormValue() {
|
|
458
482
|
this.#markup.clearAllButton?.toggleAttribute('disabled', !Boolean(this.#values.size));
|
|
459
483
|
let empty = false;
|
|
@@ -6,6 +6,8 @@ const Boolean_attribute_value_normalizer_js_1 = require("./Boolean.attribute.val
|
|
|
6
6
|
class HTMLComboboxOptionElement extends HTMLElement {
|
|
7
7
|
static OWN_IDL = new Set(['value', 'label', 'selected', 'disabled']);
|
|
8
8
|
static EventInit = { bubbles: true, cancelable: true, composed: true };
|
|
9
|
+
// The browser only knows what `selected` means on a native <option>; here the state reaches assistive tech through internals.
|
|
10
|
+
#internals = this.attachInternals();
|
|
9
11
|
static captureFocus(event) {
|
|
10
12
|
if (event.relatedTarget && event.relatedTarget instanceof _a) {
|
|
11
13
|
event.stopPropagation();
|
|
@@ -40,6 +42,7 @@ class HTMLComboboxOptionElement extends HTMLElement {
|
|
|
40
42
|
this.part.add('option');
|
|
41
43
|
super.setAttribute('tabindex', "0");
|
|
42
44
|
super.setAttribute('role', "option");
|
|
45
|
+
this.#internals.ariaSelected = String(this.selected);
|
|
43
46
|
if (!this.value)
|
|
44
47
|
this.value = this.label;
|
|
45
48
|
if (!this.textContent)
|
|
@@ -82,6 +85,7 @@ class HTMLComboboxOptionElement extends HTMLElement {
|
|
|
82
85
|
}
|
|
83
86
|
super.toggleAttribute('selected', force);
|
|
84
87
|
this.part.toggle('selected', force);
|
|
88
|
+
this.#internals.ariaSelected = String(force);
|
|
85
89
|
// console.log('this.isConnected', this.isConnected)
|
|
86
90
|
if (this.isConnected) {
|
|
87
91
|
this.dispatchEvent(new Event('optionselectedstatechange', _a.EventInit));
|
|
@@ -118,9 +122,12 @@ class HTMLComboboxOptionElement extends HTMLElement {
|
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
toggleAttribute(name, force) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
const result = super.toggleAttribute(name, force);
|
|
126
|
+
if (name === 'selected') {
|
|
127
|
+
this.part.toggle(name, result);
|
|
128
|
+
this.#internals.ariaSelected = String(result);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
124
131
|
}
|
|
125
132
|
}
|
|
126
133
|
exports.HTMLComboboxOptionElement = HTMLComboboxOptionElement;
|
|
@@ -422,7 +422,7 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
422
422
|
|
|
423
423
|
<div id="dropdown" popover="manual" part='dropdown' >
|
|
424
424
|
<input placeholder='Поиск' name="search-input" part="search-input" inputmode='search' autofocus />
|
|
425
|
-
<div part="options" id='options'></div>
|
|
425
|
+
<div part="options" id='options' role="listbox"></div>
|
|
426
426
|
</div>
|
|
427
427
|
|
|
428
428
|
<template id='tag-template'>
|
|
@@ -17,13 +17,13 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
17
17
|
super();
|
|
18
18
|
this.#internals = this.attachInternals();
|
|
19
19
|
this.#internals.role = "combobox";
|
|
20
|
-
this.#internals.ariaHasPopup = "dialog";
|
|
21
20
|
this.shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: true }); //delegatesFocus: true
|
|
22
21
|
this.#markup = new ComboboxMarkup(this.shadowRoot, this.#internals);
|
|
23
22
|
this.shadowRoot.appendChild(ComboboxMarkup.markup.content.cloneNode(true));
|
|
24
23
|
this.shadowRoot.adoptedStyleSheets = HTMLComboboxElement.styleSheet;
|
|
25
24
|
this.#observer = new MutationObserver(this.#onOptionsChanges);
|
|
26
25
|
this.#markup.connect();
|
|
26
|
+
this.#syncPopupSemantics();
|
|
27
27
|
}
|
|
28
28
|
// Lifecycle callbacks
|
|
29
29
|
connectedCallback() {
|
|
@@ -180,12 +180,14 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
180
180
|
}
|
|
181
181
|
set filterable(value) {
|
|
182
182
|
super.toggleAttribute('filterable', toBoolean(value));
|
|
183
|
+
this.#syncPopupSemantics();
|
|
183
184
|
}
|
|
184
185
|
get searchable() {
|
|
185
186
|
return this.hasAttribute('searchable');
|
|
186
187
|
}
|
|
187
188
|
set searchable(value) {
|
|
188
189
|
super.toggleAttribute('searchable', toBoolean(value));
|
|
190
|
+
this.#syncPopupSemantics();
|
|
189
191
|
}
|
|
190
192
|
// <select> specific properties (implements HTMLSelectElement)
|
|
191
193
|
get disabled() {
|
|
@@ -211,6 +213,7 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
211
213
|
}
|
|
212
214
|
set multiple(value) {
|
|
213
215
|
super.toggleAttribute('multiple', toBoolean(value));
|
|
216
|
+
this.#syncPopupSemantics();
|
|
214
217
|
}
|
|
215
218
|
// get options
|
|
216
219
|
// (there is no constructor of HTMLOptionsCollection, need to implement)
|
|
@@ -396,6 +399,7 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
396
399
|
if (state === oldState)
|
|
397
400
|
return;
|
|
398
401
|
if (state === 'open') {
|
|
402
|
+
this.#syncPopupSemantics();
|
|
399
403
|
this.#prevValue = this.value;
|
|
400
404
|
this.#changed = true;
|
|
401
405
|
}
|
|
@@ -450,6 +454,26 @@ export class HTMLComboboxElement extends HTMLElement {
|
|
|
450
454
|
this.#markup.sort(this.#markup.searchInput?.value || '');
|
|
451
455
|
}
|
|
452
456
|
};
|
|
457
|
+
// Without a search field the popup is just the listbox. With one, it is a dialog holding the field and the listbox.
|
|
458
|
+
#syncPopupSemantics() {
|
|
459
|
+
const withSearch = this.searchable || this.filterable;
|
|
460
|
+
this.#internals.ariaHasPopup = withSearch ? 'dialog' : 'listbox';
|
|
461
|
+
if (withSearch) {
|
|
462
|
+
this.#markup?.dropdown?.setAttribute('role', 'dialog');
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
this.#markup?.dropdown?.removeAttribute('role');
|
|
466
|
+
}
|
|
467
|
+
this.#markup?.optionsContainer?.setAttribute('aria-multiselectable', String(this.multiple));
|
|
468
|
+
// Shadow content can't point at the outer <label> with aria-labelledby, so it borrows the label's text.
|
|
469
|
+
const name = Array.from(this.#internals.labels ?? [], (label) => label.textContent?.trim()).filter(Boolean).join(' ');
|
|
470
|
+
for (const popup of [this.#markup?.dropdown, this.#markup?.optionsContainer]) {
|
|
471
|
+
if (name && popup?.hasAttribute('role'))
|
|
472
|
+
popup.setAttribute('aria-label', name);
|
|
473
|
+
else
|
|
474
|
+
popup?.removeAttribute('aria-label');
|
|
475
|
+
}
|
|
476
|
+
}
|
|
453
477
|
#setValidityAndFormValue() {
|
|
454
478
|
this.#markup.clearAllButton?.toggleAttribute('disabled', !Boolean(this.#values.size));
|
|
455
479
|
let empty = false;
|
|
@@ -2,6 +2,8 @@ import { toBoolean } from './Boolean.attribute.value.normalizer.js';
|
|
|
2
2
|
export class HTMLComboboxOptionElement extends HTMLElement {
|
|
3
3
|
static OWN_IDL = new Set(['value', 'label', 'selected', 'disabled']);
|
|
4
4
|
static EventInit = { bubbles: true, cancelable: true, composed: true };
|
|
5
|
+
// The browser only knows what `selected` means on a native <option>; here the state reaches assistive tech through internals.
|
|
6
|
+
#internals = this.attachInternals();
|
|
5
7
|
static captureFocus(event) {
|
|
6
8
|
if (event.relatedTarget && event.relatedTarget instanceof HTMLComboboxOptionElement) {
|
|
7
9
|
event.stopPropagation();
|
|
@@ -36,6 +38,7 @@ export class HTMLComboboxOptionElement extends HTMLElement {
|
|
|
36
38
|
this.part.add('option');
|
|
37
39
|
super.setAttribute('tabindex', "0");
|
|
38
40
|
super.setAttribute('role', "option");
|
|
41
|
+
this.#internals.ariaSelected = String(this.selected);
|
|
39
42
|
if (!this.value)
|
|
40
43
|
this.value = this.label;
|
|
41
44
|
if (!this.textContent)
|
|
@@ -78,6 +81,7 @@ export class HTMLComboboxOptionElement extends HTMLElement {
|
|
|
78
81
|
}
|
|
79
82
|
super.toggleAttribute('selected', force);
|
|
80
83
|
this.part.toggle('selected', force);
|
|
84
|
+
this.#internals.ariaSelected = String(force);
|
|
81
85
|
// console.log('this.isConnected', this.isConnected)
|
|
82
86
|
if (this.isConnected) {
|
|
83
87
|
this.dispatchEvent(new Event('optionselectedstatechange', HTMLComboboxOptionElement.EventInit));
|
|
@@ -114,9 +118,12 @@ export class HTMLComboboxOptionElement extends HTMLElement {
|
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
toggleAttribute(name, force) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
const result = super.toggleAttribute(name, force);
|
|
122
|
+
if (name === 'selected') {
|
|
123
|
+
this.part.toggle(name, result);
|
|
124
|
+
this.#internals.ariaSelected = String(result);
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
120
127
|
}
|
|
121
128
|
}
|
|
122
129
|
if (!window.customElements.get('box-option')) {
|