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.
@@ -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
- if (name === 'selected')
122
- this.part.toggle(name, force);
123
- return super.toggleAttribute(name, force);
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
- if (name === 'selected')
118
- this.part.toggle(name, force);
119
- return super.toggleAttribute(name, force);
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')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "html-combobox-element",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "The combo-box web-component & customizable select polyfill",
5
5
  "type": "module",
6
6
  "scripts": {