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.
- package/README.md +139 -24
- package/dist/cjs/combobox/Combobox.markup.d.ts +22 -1
- package/dist/cjs/combobox/Combobox.markup.js +156 -35
- package/dist/cjs/combobox/HTML.combobox.element.d.ts +32 -5
- package/dist/cjs/combobox/HTML.combobox.element.js +431 -98
- package/dist/cjs/combobox/HTML.combobox.option.element.d.ts +25 -4
- package/dist/cjs/combobox/HTML.combobox.option.element.js +104 -73
- package/dist/cjs/combobox/HTML.combobox.options.collection.d.ts +37 -0
- package/dist/cjs/combobox/HTML.combobox.options.collection.js +176 -0
- package/dist/cjs/combobox/HTML.combobox.tag.element.d.ts +7 -1
- package/dist/cjs/combobox/HTML.combobox.tag.element.js +36 -12
- package/dist/cjs/combobox/index.d.ts +1 -0
- package/dist/cjs/combobox/index.js +1 -0
- package/dist/cjs/playwright.d.ts +48 -0
- package/dist/cjs/playwright.js +127 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/combobox/Combobox.markup.js +156 -35
- package/dist/esm/combobox/HTML.combobox.element.js +431 -98
- package/dist/esm/combobox/HTML.combobox.option.element.js +104 -73
- package/dist/esm/combobox/HTML.combobox.options.collection.js +170 -0
- package/dist/esm/combobox/HTML.combobox.tag.element.js +36 -12
- package/dist/esm/combobox/index.js +1 -0
- package/dist/esm/playwright.js +123 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/combobox/Combobox.markup.d.ts +22 -1
- package/dist/types/combobox/HTML.combobox.element.d.ts +32 -5
- package/dist/types/combobox/HTML.combobox.option.element.d.ts +25 -4
- package/dist/types/combobox/HTML.combobox.options.collection.d.ts +37 -0
- package/dist/types/combobox/HTML.combobox.tag.element.d.ts +7 -1
- package/dist/types/combobox/index.d.ts +1 -0
- package/dist/types/playwright.d.ts +48 -0
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- 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
|
-
|
|
10
|
-
static onNavigate(event: KeyboardEvent): void;
|
|
27
|
+
constructor();
|
|
11
28
|
connectedCallback(): void;
|
|
12
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
|
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
|
|
12
|
-
if (!
|
|
13
|
-
|
|
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
|
|
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
|
|
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
|
|
31
|
-
|
|
47
|
+
const control = event.currentTarget;
|
|
48
|
+
control?.closest('box-tag')?.dispatchEvent(new Event('tagcleared', HTMLComboboxTagElement.EventInit));
|
|
32
49
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
event.
|
|
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;
|
|
@@ -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>;
|