html-combobox-element 0.0.5 → 0.0.6

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.
Files changed (33) hide show
  1. package/README.md +139 -24
  2. package/dist/cjs/combobox/Combobox.markup.d.ts +22 -1
  3. package/dist/cjs/combobox/Combobox.markup.js +156 -35
  4. package/dist/cjs/combobox/HTML.combobox.element.d.ts +32 -5
  5. package/dist/cjs/combobox/HTML.combobox.element.js +431 -98
  6. package/dist/cjs/combobox/HTML.combobox.option.element.d.ts +25 -4
  7. package/dist/cjs/combobox/HTML.combobox.option.element.js +104 -73
  8. package/dist/cjs/combobox/HTML.combobox.options.collection.d.ts +37 -0
  9. package/dist/cjs/combobox/HTML.combobox.options.collection.js +176 -0
  10. package/dist/cjs/combobox/HTML.combobox.tag.element.d.ts +7 -1
  11. package/dist/cjs/combobox/HTML.combobox.tag.element.js +36 -12
  12. package/dist/cjs/combobox/index.d.ts +1 -0
  13. package/dist/cjs/combobox/index.js +1 -0
  14. package/dist/cjs/playwright.d.ts +48 -0
  15. package/dist/cjs/playwright.js +127 -0
  16. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  17. package/dist/esm/combobox/Combobox.markup.js +156 -35
  18. package/dist/esm/combobox/HTML.combobox.element.js +431 -98
  19. package/dist/esm/combobox/HTML.combobox.option.element.js +104 -73
  20. package/dist/esm/combobox/HTML.combobox.options.collection.js +170 -0
  21. package/dist/esm/combobox/HTML.combobox.tag.element.js +36 -12
  22. package/dist/esm/combobox/index.js +1 -0
  23. package/dist/esm/playwright.js +123 -0
  24. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  25. package/dist/types/combobox/Combobox.markup.d.ts +22 -1
  26. package/dist/types/combobox/HTML.combobox.element.d.ts +32 -5
  27. package/dist/types/combobox/HTML.combobox.option.element.d.ts +25 -4
  28. package/dist/types/combobox/HTML.combobox.options.collection.d.ts +37 -0
  29. package/dist/types/combobox/HTML.combobox.tag.element.d.ts +7 -1
  30. package/dist/types/combobox/index.d.ts +1 -0
  31. package/dist/types/playwright.d.ts +48 -0
  32. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  33. package/package.json +18 -5
@@ -1,24 +1,45 @@
1
+ /**
2
+ * The `<option>` of `<combo-box>`.
3
+ *
4
+ * The browser only knows what `selected`/`disabled` mean on a native `<option>`, so the role and
5
+ * both states are set through ElementInternals, and mirrored as `role`, `aria-selected` and
6
+ * `aria-disabled` attributes. Internals are invisible from outside the element, so the attributes
7
+ * are what tools reading the DOM (Playwright's getByRole, Testing Library, axe) can see.
8
+ * Both say the same thing; for the browser an attribute takes precedence over internals.
9
+ *
10
+ * Attributes and properties:
11
+ * - Setters (and the setAttribute/removeAttribute overrides that route to them) only normalize
12
+ * the value and write the attribute.
13
+ * - attributeChangedCallback applies the side effects (state, ARIA, notifying the combobox).
14
+ * It's the one hook the browser calls for every way an attribute can change: the setters,
15
+ * the native Element.prototype methods, setAttributeNS/setAttributeNode, DevTools, and other
16
+ * JavaScript worlds (test runners, extensions) that can't see this class at all.
17
+ */
1
18
  export declare class HTMLComboboxOptionElement extends HTMLElement {
2
19
  #private;
3
20
  static OWN_IDL: Set<string>;
21
+ static observedAttributes: string[];
4
22
  static EventInit: {
5
23
  bubbles: boolean;
6
24
  cancelable: boolean;
7
25
  composed: boolean;
8
26
  };
9
- static captureFocus(event: FocusEvent): void;
10
- static onNavigate(event: KeyboardEvent): void;
27
+ constructor();
11
28
  connectedCallback(): void;
12
- disconnectedCallback(): void;
29
+ /** Like <option>: the value attribute, or the text when there is none */
13
30
  get value(): string;
14
31
  set value(value: string);
32
+ /** Like <option>: the label attribute when present, otherwise the text */
15
33
  get label(): string;
16
34
  set label(value: string);
35
+ /** Like <option>: the text content with whitespace stripped and collapsed */
36
+ get text(): string;
37
+ set text(value: string);
17
38
  get selected(): boolean;
18
39
  set selected(value: boolean);
19
40
  get disabled(): boolean;
20
41
  set disabled(value: boolean);
42
+ attributeChangedCallback(name: 'selected' | 'disabled', _oldValue: string | null, newValue: string | null): void;
21
43
  setAttribute(name: string, value: string): void;
22
44
  removeAttribute(name: string): void;
23
- toggleAttribute(name: string, force?: boolean): boolean;
24
45
  }
@@ -3,102 +3,140 @@ var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.HTMLComboboxOptionElement = void 0;
5
5
  const Boolean_attribute_value_normalizer_js_1 = require("./Boolean.attribute.value.normalizer.js");
6
+ /**
7
+ * The `<option>` of `<combo-box>`.
8
+ *
9
+ * The browser only knows what `selected`/`disabled` mean on a native `<option>`, so the role and
10
+ * both states are set through ElementInternals, and mirrored as `role`, `aria-selected` and
11
+ * `aria-disabled` attributes. Internals are invisible from outside the element, so the attributes
12
+ * are what tools reading the DOM (Playwright's getByRole, Testing Library, axe) can see.
13
+ * Both say the same thing; for the browser an attribute takes precedence over internals.
14
+ *
15
+ * Attributes and properties:
16
+ * - Setters (and the setAttribute/removeAttribute overrides that route to them) only normalize
17
+ * the value and write the attribute.
18
+ * - attributeChangedCallback applies the side effects (state, ARIA, notifying the combobox).
19
+ * It's the one hook the browser calls for every way an attribute can change: the setters,
20
+ * the native Element.prototype methods, setAttributeNS/setAttributeNode, DevTools, and other
21
+ * JavaScript worlds (test runners, extensions) that can't see this class at all.
22
+ */
6
23
  class HTMLComboboxOptionElement extends HTMLElement {
7
24
  static OWN_IDL = new Set(['value', 'label', 'selected', 'disabled']);
25
+ static observedAttributes = ['selected', 'disabled'];
8
26
  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();
11
- static captureFocus(event) {
12
- if (event.relatedTarget && event.relatedTarget instanceof _a) {
13
- event.stopPropagation();
14
- event.preventDefault();
15
- }
16
- }
17
- static onNavigate(event) {
18
- if (!event.target ||
19
- !(event.target instanceof _a) ||
20
- event.key === 'Tab' ||
21
- event.key === 'Enter')
22
- return;
23
- event.preventDefault();
24
- if (event.key === 'ArrowDown') {
25
- const next = event.target.nextElementSibling;
26
- if (next && next instanceof _a) {
27
- next.focus();
28
- }
29
- }
30
- if (event.key === 'ArrowUp') {
31
- const prev = event.target.previousElementSibling;
32
- if (prev && prev instanceof _a) {
33
- prev.focus();
34
- }
35
- }
27
+ #internals;
28
+ /** The states the side effects were last applied for (compared in attributeChangedCallback) */
29
+ #applied = { selected: false, disabled: false };
30
+ constructor() {
31
+ super();
32
+ this.#internals = this.attachInternals();
33
+ this.#internals.role = 'option';
36
34
  }
37
35
  connectedCallback() {
38
- this.addEventListener('keydown', _a.onNavigate);
39
- this.addEventListener('focusout', _a.captureFocus);
40
- this.addEventListener('click', this.#toggleSelection);
41
36
  this.#initialAttributesSynchronization();
42
37
  this.part.add('option');
43
- super.setAttribute('tabindex', "0");
44
- super.setAttribute('role', "option");
45
- this.#internals.ariaSelected = String(this.selected);
46
- if (!this.value)
47
- this.value = this.label;
38
+ // Focusable on click and from script (arrow keys), but not a tab stop: like native options,
39
+ // the list is entered with the arrow keys from the combobox, not with Tab.
40
+ if (!this.hasAttribute('tabindex'))
41
+ super.setAttribute('tabindex', '-1');
42
+ if (!this.hasAttribute('role'))
43
+ super.setAttribute('role', 'option');
44
+ this.#syncSelected(this.selected);
45
+ this.#syncDisabled(this.disabled);
46
+ // No label → value copy: like <option>, the value is the value attribute or the text.
47
+ // (<box-option label="Plum"></box-option> displays its label through CSS, see the markup styles.)
48
48
  if (!this.textContent)
49
- this.textContent = this.value;
49
+ this.textContent = this.getAttribute('value') ?? '';
50
50
  this.dispatchEvent(new Event('optionselectedstatechange', _a.EventInit));
51
51
  }
52
- disconnectedCallback() {
53
- this.removeEventListener('click', this.#toggleSelection);
54
- this.removeEventListener('keydown', _a.onNavigate);
55
- this.removeEventListener('focusout', _a.captureFocus);
56
- }
57
- #toggleSelection = (event) => {
58
- event.stopPropagation();
59
- this.selected = !this.selected;
60
- };
52
+ /** Like <option>: the value attribute, or the text when there is none */
61
53
  get value() {
62
- return this.getAttribute('value');
54
+ return this.getAttribute('value') ?? this.text;
63
55
  }
64
56
  set value(value) {
65
57
  if (value == null)
66
58
  return;
67
59
  super.setAttribute('value', String(value));
68
60
  }
61
+ /** Like <option>: the label attribute when present, otherwise the text */
69
62
  get label() {
70
- return this.getAttribute('label');
63
+ return this.getAttribute('label') ?? this.text;
71
64
  }
72
65
  set label(value) {
73
66
  if (value == null)
74
67
  return;
75
68
  super.setAttribute('label', String(value));
76
69
  }
70
+ /** Like <option>: the text content with whitespace stripped and collapsed */
71
+ get text() {
72
+ return (this.textContent ?? '').replace(/[\t\n\f\r ]+/g, ' ').trim();
73
+ }
74
+ set text(value) {
75
+ this.textContent = value;
76
+ }
77
77
  get selected() {
78
78
  return this.hasAttribute('selected');
79
79
  }
80
+ // Normalization only: the side effects run in attributeChangedCallback
80
81
  set selected(value) {
81
- const prev = this.selected;
82
- const force = (0, Boolean_attribute_value_normalizer_js_1.toBoolean)(value);
83
- if (prev === force) {
84
- return;
85
- }
86
- super.toggleAttribute('selected', force);
87
- this.part.toggle('selected', force);
88
- this.#internals.ariaSelected = String(force);
89
- // console.log('this.isConnected', this.isConnected)
90
- if (this.isConnected) {
91
- this.dispatchEvent(new Event('optionselectedstatechange', _a.EventInit));
92
- }
82
+ super.toggleAttribute('selected', (0, Boolean_attribute_value_normalizer_js_1.toBoolean)(value));
93
83
  }
94
84
  get disabled() {
95
85
  return this.hasAttribute('disabled');
96
86
  }
97
87
  set disabled(value) {
98
- const force = (0, Boolean_attribute_value_normalizer_js_1.toBoolean)(value);
99
- super.toggleAttribute('disabled', force);
100
- this.part.toggle('disabled', force);
101
- super.toggleAttribute('inert', force);
88
+ super.toggleAttribute('disabled', (0, Boolean_attribute_value_normalizer_js_1.toBoolean)(value));
89
+ }
90
+ attributeChangedCallback(name, _oldValue, newValue) {
91
+ // A path that skipped the setters can leave a value they'd never write, e.g. selected="false".
92
+ // Fix it once: the removal calls back again with a valid value (which applies the change);
93
+ // that removal is valid, so it isn't fixed again: no loop.
94
+ if (newValue !== null && !(0, Boolean_attribute_value_normalizer_js_1.toBoolean)(newValue)) {
95
+ super.removeAttribute(name);
96
+ return;
97
+ }
98
+ const now = newValue !== null;
99
+ // Compared with the state last applied, not with the old attribute value: after a fix-up
100
+ // the old value is "false", which says nothing about what was applied before it.
101
+ if (this.#applied[name] === now)
102
+ return;
103
+ if (name === 'selected') {
104
+ this.#syncSelected(now);
105
+ if (this.isConnected) {
106
+ this.dispatchEvent(new Event('optionselectedstatechange', _a.EventInit));
107
+ }
108
+ }
109
+ else {
110
+ this.#syncDisabled(now);
111
+ }
112
+ }
113
+ #syncSelected(selected) {
114
+ this.#applied.selected = selected;
115
+ this.part.toggle('selected', selected);
116
+ this.#syncAriaSelected();
117
+ }
118
+ // A disabled option stays in the accessibility tree (like a native disabled <option>), it is
119
+ // only marked as disabled. `inert` would remove it from the tree altogether.
120
+ #syncDisabled(disabled) {
121
+ this.#applied.disabled = disabled;
122
+ this.part.toggle('disabled', disabled);
123
+ this.#internals.ariaDisabled = disabled ? 'true' : null;
124
+ if (disabled)
125
+ super.setAttribute('aria-disabled', 'true');
126
+ else
127
+ super.removeAttribute('aria-disabled');
128
+ this.#syncAriaSelected();
129
+ }
130
+ // Like a native <option>: a disabled option that isn't selected isn't selectable, so it reports
131
+ // no selected state at all (rather than selected=false).
132
+ #syncAriaSelected() {
133
+ const selected = this.selected;
134
+ const ariaSelected = this.disabled && !selected ? null : String(selected);
135
+ this.#internals.ariaSelected = ariaSelected;
136
+ if (ariaSelected === null)
137
+ super.removeAttribute('aria-selected');
138
+ else
139
+ super.setAttribute('aria-selected', ariaSelected);
102
140
  }
103
141
  #initialAttributesSynchronization() {
104
142
  for (const key of _a.OWN_IDL) {
@@ -114,21 +152,14 @@ class HTMLComboboxOptionElement extends HTMLElement {
114
152
  }
115
153
  }
116
154
  removeAttribute(name) {
117
- if (_a.OWN_IDL.has(name)) {
118
- this[name] = null;
155
+ // value/label setters ignore null (like option.value = null they'd need a string), so remove directly
156
+ if (name === 'selected' || name === 'disabled') {
157
+ this[name] = false;
119
158
  }
120
159
  else {
121
160
  super.removeAttribute(name);
122
161
  }
123
162
  }
124
- 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;
131
- }
132
163
  }
133
164
  exports.HTMLComboboxOptionElement = HTMLComboboxOptionElement;
134
165
  _a = HTMLComboboxOptionElement;
@@ -0,0 +1,37 @@
1
+ import { HTMLComboboxOptionElement } from './HTML.combobox.option.element.js';
2
+ interface CollectionSource {
3
+ /** The listbox the options live in (inside the combobox shadow root) */
4
+ container: () => HTMLElement;
5
+ /** Live list of the options, in tree order (optgroup children included) */
6
+ options: () => NodeListOf<HTMLComboboxOptionElement>;
7
+ }
8
+ /**
9
+ * The `options` of a `<combo-box>`: an implementation of `HTMLOptionsCollection`, whose constructor
10
+ * is not exposed (it can't be constructed or subclassed). Same behaviour as `select.options`:
11
+ * it is live, supports `options[i]` (also as an assignment target), `length` (also settable),
12
+ * `item()`, `namedItem()`, `add()`, `remove()`, `selectedIndex` and iteration.
13
+ */
14
+ export declare class HTMLComboboxOptionsCollection {
15
+ #private;
16
+ [index: number]: HTMLComboboxOptionElement;
17
+ constructor(source: CollectionSource);
18
+ get [Symbol.toStringTag](): string;
19
+ [Symbol.iterator](): IterableIterator<HTMLComboboxOptionElement>;
20
+ get length(): number;
21
+ /** Like `select.options.length = n`: appends blank options, or removes the last ones. */
22
+ set length(value: number);
23
+ item(index: number): HTMLComboboxOptionElement | null;
24
+ /** The first option whose `id` or `name` attribute is `name` */
25
+ namedItem(name: string): HTMLComboboxOptionElement | null;
26
+ /**
27
+ * Inserts an option (or optgroup) before `before` (an option/optgroup of this list, or an index),
28
+ * or at the end. Throws like `select.options.add()`.
29
+ */
30
+ add(element: HTMLComboboxOptionElement | HTMLOptGroupElement, before?: HTMLElement | number | null): void;
31
+ /** Removes the option at `index` (does nothing when out of range) */
32
+ remove(index: number): void;
33
+ /** Index of the first selected option, or -1. Setting it selects only that option. */
34
+ get selectedIndex(): number;
35
+ set selectedIndex(value: number);
36
+ }
37
+ export {};
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ var _a;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.HTMLComboboxOptionsCollection = void 0;
5
+ const HTML_combobox_option_element_js_1 = require("./HTML.combobox.option.element.js");
6
+ // Kept outside the instance: the collection is used through a Proxy, and private (#) fields
7
+ // can't be read through a Proxy receiver.
8
+ const sources = new WeakMap();
9
+ const source = (collection) => sources.get(collection);
10
+ const isIndex = (prop) => typeof prop === 'string' && /^(0|[1-9]\d*)$/.test(prop);
11
+ /**
12
+ * The `options` of a `<combo-box>`: an implementation of `HTMLOptionsCollection`, whose constructor
13
+ * is not exposed (it can't be constructed or subclassed). Same behaviour as `select.options`:
14
+ * it is live, supports `options[i]` (also as an assignment target), `length` (also settable),
15
+ * `item()`, `namedItem()`, `add()`, `remove()`, `selectedIndex` and iteration.
16
+ */
17
+ class HTMLComboboxOptionsCollection {
18
+ constructor(source) {
19
+ sources.set(this, source);
20
+ const proxy = new Proxy(this, {
21
+ get(target, prop, receiver) {
22
+ if (isIndex(prop))
23
+ return target.item(Number(prop)) ?? undefined;
24
+ if (typeof prop === 'symbol' || prop in target)
25
+ return Reflect.get(target, prop, receiver);
26
+ return target.namedItem(prop) ?? undefined;
27
+ },
28
+ set(target, prop, value, receiver) {
29
+ if (isIndex(prop)) {
30
+ target.#setIndexed(Number(prop), value);
31
+ return true;
32
+ }
33
+ return Reflect.set(target, prop, value, receiver);
34
+ },
35
+ has(target, prop) {
36
+ if (isIndex(prop))
37
+ return Number(prop) < target.length;
38
+ if (typeof prop === 'symbol' || prop in target)
39
+ return Reflect.has(target, prop);
40
+ return target.namedItem(prop) !== null;
41
+ },
42
+ ownKeys(target) {
43
+ const names = new Set();
44
+ for (const option of target) {
45
+ if (option.id)
46
+ names.add(option.id);
47
+ const name = option.getAttribute('name');
48
+ if (name)
49
+ names.add(name);
50
+ }
51
+ const indices = Array.from({ length: target.length }, (_, i) => String(i));
52
+ return [...indices, ...Array.from(names).filter((name) => !isIndex(name)), ...Reflect.ownKeys(target)];
53
+ },
54
+ getOwnPropertyDescriptor(target, prop) {
55
+ if (isIndex(prop)) {
56
+ const value = target.item(Number(prop));
57
+ return value ? { value, enumerable: true, configurable: true, writable: true } : undefined;
58
+ }
59
+ if (typeof prop === 'string' && !(prop in target)) {
60
+ const value = target.namedItem(prop);
61
+ return value ? { value, enumerable: true, configurable: true, writable: false } : undefined;
62
+ }
63
+ return Reflect.getOwnPropertyDescriptor(target, prop);
64
+ },
65
+ deleteProperty(target, prop) {
66
+ // Like the native collection, indexed and named properties can't be deleted
67
+ if (isIndex(prop) || (typeof prop === 'string' && !(prop in target)))
68
+ return false;
69
+ return Reflect.deleteProperty(target, prop);
70
+ },
71
+ });
72
+ sources.set(proxy, source);
73
+ return proxy;
74
+ }
75
+ get [Symbol.toStringTag]() {
76
+ return 'HTMLOptionsCollection';
77
+ }
78
+ *[Symbol.iterator]() {
79
+ yield* Array.from(source(this).options());
80
+ }
81
+ get length() {
82
+ return source(this).options().length;
83
+ }
84
+ /** Like `select.options.length = n`: appends blank options, or removes the last ones. */
85
+ set length(value) {
86
+ const length = Number(value) >>> 0;
87
+ const options = Array.from(source(this).options());
88
+ if (length > options.length) {
89
+ source(this).container().append(..._a.#blankOptions(length - options.length));
90
+ }
91
+ else {
92
+ options.slice(length).forEach((option) => option.remove());
93
+ }
94
+ }
95
+ item(index) {
96
+ return source(this).options().item(Number(index) >>> 0) ?? null;
97
+ }
98
+ /** The first option whose `id` or `name` attribute is `name` */
99
+ namedItem(name) {
100
+ if (name === '')
101
+ return null;
102
+ for (const option of this) {
103
+ if (option.id === name || option.getAttribute('name') === name)
104
+ return option;
105
+ }
106
+ return null;
107
+ }
108
+ /**
109
+ * Inserts an option (or optgroup) before `before` (an option/optgroup of this list, or an index),
110
+ * or at the end. Throws like `select.options.add()`.
111
+ */
112
+ add(element, before) {
113
+ const isOption = element instanceof HTML_combobox_option_element_js_1.HTMLComboboxOptionElement || (element instanceof Element && element.localName === 'box-option');
114
+ if (!isOption && !(element instanceof HTMLOptGroupElement)) {
115
+ throw new TypeError(`Failed to execute 'add' on 'HTMLOptionsCollection': The provided value is not a <box-option> or an <optgroup>.`);
116
+ }
117
+ const container = source(this).container();
118
+ let reference = null;
119
+ if (typeof before === 'number') {
120
+ reference = this.item(before);
121
+ }
122
+ else if (before != null) {
123
+ if (!container.contains(before)) {
124
+ throw new DOMException(`Failed to execute 'add' on 'HTMLOptionsCollection': The node before which the new node is to be inserted is not a child of this node.`, 'NotFoundError');
125
+ }
126
+ reference = before;
127
+ }
128
+ if (element === reference)
129
+ return;
130
+ const parent = reference?.parentNode ?? container;
131
+ parent.insertBefore(element, reference);
132
+ }
133
+ /** Removes the option at `index` (does nothing when out of range) */
134
+ remove(index) {
135
+ this.item(index)?.remove();
136
+ }
137
+ /** Index of the first selected option, or -1. Setting it selects only that option. */
138
+ get selectedIndex() {
139
+ return Array.from(this).findIndex((option) => option.selected);
140
+ }
141
+ set selectedIndex(value) {
142
+ const index = Number(value);
143
+ const options = Array.from(this);
144
+ // Deselect first, so a single-select combobox doesn't see two selections at once
145
+ options.forEach((option, i) => { if (i !== index)
146
+ option.selected = false; });
147
+ if (options[index])
148
+ options[index].selected = true;
149
+ }
150
+ // `options[i] = option`, as specified for HTMLOptionsCollection: null removes the option at i;
151
+ // past the end, pads with blank options; otherwise replaces the option at i in place (inside its
152
+ // optgroup, if any). Chrome's native implementation diverges there: it moves the new option out
153
+ // of the optgroup, or throws when the next option is in one.
154
+ #setIndexed(index, value) {
155
+ if (value == null) {
156
+ this.remove(index);
157
+ return;
158
+ }
159
+ if (!(value instanceof HTML_combobox_option_element_js_1.HTMLComboboxOptionElement) && !(value instanceof Element && value.localName === 'box-option')) {
160
+ throw new TypeError(`Failed to set an indexed property on 'HTMLOptionsCollection': The provided value is not a <box-option>.`);
161
+ }
162
+ const length = this.length;
163
+ if (index >= length) {
164
+ source(this).container().append(..._a.#blankOptions(index - length), value);
165
+ return;
166
+ }
167
+ const current = this.item(index);
168
+ if (current !== value)
169
+ current.parentNode.replaceChild(value, current);
170
+ }
171
+ static #blankOptions(count) {
172
+ return Array.from({ length: count }, () => document.createElement('box-option'));
173
+ }
174
+ }
175
+ exports.HTMLComboboxOptionsCollection = HTMLComboboxOptionsCollection;
176
+ _a = HTMLComboboxOptionsCollection;
@@ -1,12 +1,18 @@
1
+ /**
2
+ * A selected value shown inside `<combo-box>`. The tags are exposed as a list of items,
3
+ * each with a "Remove …" button when the combobox is `clearable` or `multiple`.
4
+ */
1
5
  export declare class HTMLComboboxTagElement extends HTMLElement {
6
+ #private;
2
7
  static EventInit: {
3
8
  bubbles: boolean;
4
9
  cancelable: boolean;
5
10
  composed: boolean;
6
11
  };
12
+ constructor();
7
13
  get value(): string;
8
14
  connectedCallback(): void;
9
15
  disconnectedCallback(): void;
10
16
  static onClearTag(event: Event): void;
11
- static captureFocus(event: Event): void;
17
+ static onControlKeyDown(event: KeyboardEvent): void;
12
18
  }
@@ -1,38 +1,62 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HTMLComboboxTagElement = void 0;
4
+ /**
5
+ * A selected value shown inside `<combo-box>`. The tags are exposed as a list of items,
6
+ * each with a "Remove …" button when the combobox is `clearable` or `multiple`.
7
+ */
4
8
  class HTMLComboboxTagElement extends HTMLElement {
5
9
  static EventInit = { bubbles: true, cancelable: true, composed: true };
10
+ #internals;
11
+ constructor() {
12
+ super();
13
+ this.#internals = this.attachInternals();
14
+ this.#internals.role = 'listitem';
15
+ }
6
16
  get value() {
7
17
  return this.getAttribute('value') || '';
8
18
  }
19
+ get #control() {
20
+ return this.querySelector('[part*="clear-tag"]');
21
+ }
9
22
  connectedCallback() {
10
23
  this.part.add('tag');
11
- const control = this.querySelector('[part*="clear-tag"]');
12
- if (!this.hasAttribute('visible')) {
13
- super.setAttribute('tabindex', '0');
24
+ const control = this.#control;
25
+ if (control && !(control instanceof HTMLButtonElement)) {
26
+ // A non-native button (e.g. <div part="clear-tag">) needs the button semantics and focusability added
27
+ if (!control.hasAttribute('role'))
28
+ control.setAttribute('role', 'button');
29
+ if (!control.hasAttribute('tabindex'))
30
+ control.setAttribute('tabindex', '0');
14
31
  }
15
- control?.addEventListener('focusout', HTMLComboboxTagElement.captureFocus);
16
32
  control?.addEventListener('click', HTMLComboboxTagElement.onClearTag);
33
+ control?.addEventListener('keydown', HTMLComboboxTagElement.onControlKeyDown);
17
34
  const root = this.getRootNode();
18
- if (root?.host.hasAttribute('multiple')) {
35
+ if (root?.host?.hasAttribute('multiple')) {
19
36
  if (!control) {
20
37
  throw new Error(`A <button> with part="clear-tag" is required for <combo-box> with multiple attribute`);
21
38
  }
22
39
  }
23
40
  }
24
41
  disconnectedCallback() {
25
- const control = this.querySelector('[part*="clear-tag"]');
26
- control?.removeEventListener('focusout', HTMLComboboxTagElement.captureFocus);
42
+ const control = this.#control;
27
43
  control?.removeEventListener('click', HTMLComboboxTagElement.onClearTag);
44
+ control?.removeEventListener('keydown', HTMLComboboxTagElement.onControlKeyDown);
28
45
  }
29
46
  static onClearTag(event) {
30
- const target = event.target;
31
- target?.parentElement?.dispatchEvent(new Event('tagcleared', HTMLComboboxTagElement.EventInit));
47
+ const control = event.currentTarget;
48
+ control?.closest('box-tag')?.dispatchEvent(new Event('tagcleared', HTMLComboboxTagElement.EventInit));
32
49
  }
33
- static captureFocus(event) {
34
- event.stopPropagation();
35
- event.preventDefault();
50
+ // role="button" on a non-<button> doesn't give it keyboard activation, add it.
51
+ static onControlKeyDown(event) {
52
+ const control = event.currentTarget;
53
+ if (control instanceof HTMLButtonElement)
54
+ return;
55
+ if (event.key === 'Enter' || event.key === ' ') {
56
+ event.preventDefault();
57
+ event.stopPropagation();
58
+ control.click();
59
+ }
36
60
  }
37
61
  }
38
62
  exports.HTMLComboboxTagElement = HTMLComboboxTagElement;
@@ -1 +1,2 @@
1
1
  export * from './HTML.combobox.element.js';
2
+ export * from './HTML.combobox.options.collection.js';
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./HTML.combobox.element.js"), exports);
18
+ __exportStar(require("./HTML.combobox.options.collection.js"), exports);
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Makes Playwright's `selectOption()`, `toHaveValue()` and `toHaveValues()` work on `<combo-box>`
3
+ * as they do on `<select>`.
4
+ *
5
+ * Why it's needed: Playwright runs its page-side code in an isolated JavaScript world. That world
6
+ * shares the DOM with the page, but not the page's JavaScript: the `<combo-box>` class (its
7
+ * `options`, `value`, `multiple`, …) doesn't exist there, so Playwright sees a plain element and
8
+ * refuses it ("Element is not a <select> element").
9
+ *
10
+ * What it does: Playwright's custom selector engines registered with `contentScript: true` are
11
+ * evaluated in that isolated world. This "engine" selects nothing; evaluating it teaches the
12
+ * isolated world the `<select>` API of `<combo-box>`/`<box-option>`, implemented through the DOM
13
+ * only (attributes, shadow root). `<combo-box>` reacts to `selected` attributes changed from any
14
+ * world, so this drives the real component.
15
+ *
16
+ * It never touches the page's own world, so the application under test sees no difference.
17
+ *
18
+ * @example
19
+ * // fixtures.ts
20
+ * import { test as base } from '@playwright/test';
21
+ * import { registerCombobox } from 'html-combobox-element/playwright';
22
+ *
23
+ * export const test = base.extend<{}, { combobox: void }>({
24
+ * combobox: [async ({ playwright }, use) => {
25
+ * await registerCombobox(playwright.selectors);
26
+ * await use();
27
+ * }, { scope: 'worker', auto: true }],
28
+ * });
29
+ *
30
+ * // in a test
31
+ * await page.getByLabel('Fruit').selectOption('banana');
32
+ * await expect(page.getByLabel('Fruit')).toHaveValue('banana');
33
+ */
34
+ /** The part of Playwright's `Selectors` this module uses (no dependency on Playwright) */
35
+ export interface SelectorsLike {
36
+ register(name: string, script: string, options?: {
37
+ contentScript?: boolean;
38
+ }): Promise<void>;
39
+ }
40
+ /** Name under which the engine is registered by default */
41
+ export declare const COMBOBOX_ENGINE_NAME = "combobox-select-api";
42
+ /** Source of the selector engine, for `selectors.register(name, source, { contentScript: true })` */
43
+ export declare const comboboxSelectorEngine: string;
44
+ /**
45
+ * Registers the engine. Call it once per worker, before any page is created
46
+ * (Playwright requires selector engines to be registered before pages exist).
47
+ */
48
+ export declare function registerCombobox(selectors: SelectorsLike, name?: string): Promise<void>;