@substrate-system/actor-typeahead 0.0.2 → 0.0.4
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 +45 -11
- package/dist/custom-elements.json +83 -6
- package/dist/index.cjs +150 -38
- package/dist/index.cjs.map +3 -3
- package/dist/index.css +23 -2
- package/dist/index.d.ts +16 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +150 -38
- package/dist/index.js.map +3 -3
- package/dist/index.min.css +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +3 -3
- package/dist/meta.json +4 -4
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ rewritten in TypeScript, and rendering in light DOM only. See
|
|
|
29
29
|
* [ESM](#esm)
|
|
30
30
|
* [Common JS](#common-js)
|
|
31
31
|
* [Attributes](#attributes)
|
|
32
|
+
* [Properties](#properties)
|
|
32
33
|
* [Events](#events)
|
|
33
34
|
- [CSS](#css)
|
|
34
35
|
* [Import CSS](#import-css)
|
|
@@ -56,7 +57,7 @@ Wrap an `<input>` with `<actor-typeahead>`, and import the JS and CSS.
|
|
|
56
57
|
|
|
57
58
|
```html
|
|
58
59
|
<actor-typeahead>
|
|
59
|
-
<input
|
|
60
|
+
<input />
|
|
60
61
|
</actor-typeahead>
|
|
61
62
|
```
|
|
62
63
|
|
|
@@ -69,6 +70,11 @@ Typing in the input shows a Suggestion list of matching accounts.
|
|
|
69
70
|
Choosing one, with the arrow keys and Enter or with the pointer, fills in
|
|
70
71
|
the input with the account's handle.
|
|
71
72
|
|
|
73
|
+
Enter and Escape are left alone when the component has nothing to do
|
|
74
|
+
with them. Enter with no highlighted Suggestion submits a surrounding
|
|
75
|
+
form, and Escape with the Suggestion list closed reaches a surrounding
|
|
76
|
+
`<dialog>`.
|
|
77
|
+
|
|
72
78
|
## API
|
|
73
79
|
|
|
74
80
|
This exposes ESM and common JS via
|
|
@@ -95,17 +101,39 @@ it more than once is safe.
|
|
|
95
101
|
|
|
96
102
|
```html
|
|
97
103
|
<actor-typeahead host="https://public.api.bsky.app" rows="3">
|
|
98
|
-
<input
|
|
104
|
+
<input />
|
|
99
105
|
</actor-typeahead>
|
|
100
106
|
```
|
|
101
107
|
|
|
102
108
|
Both are reflected as properties, so `el.rows = '3'` works too.
|
|
103
109
|
|
|
110
|
+
### Properties
|
|
111
|
+
|
|
112
|
+
* `announce` -- A function `(count:number) => string` that returns the
|
|
113
|
+
text screen readers hear when a Typeahead request settles. `count` is
|
|
114
|
+
the number of Suggestions, and is 0 when nothing matched the Query.
|
|
115
|
+
The default is English, "No Suggestions", "1 Suggestion available" or
|
|
116
|
+
"N Suggestions available". Replace it to translate; your function
|
|
117
|
+
handles plurals and 0. It is read each time a request settles, so you
|
|
118
|
+
can set it at any time. There is no matching attribute.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const el = document.querySelector('actor-typeahead')!
|
|
122
|
+
el.announce = count => {
|
|
123
|
+
if (count === 0) return 'Sin sugerencias'
|
|
124
|
+
return count === 1 ?
|
|
125
|
+
'1 sugerencia disponible' :
|
|
126
|
+
`${count} sugerencias disponibles`
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A request that fails is not announced.
|
|
131
|
+
|
|
104
132
|
### Events
|
|
105
133
|
|
|
106
134
|
Events are namespaced by the tag name.
|
|
107
135
|
|
|
108
|
-
* `actor-typeahead:select` -- A result was chosen. The event `detail` is
|
|
136
|
+
* `actor-typeahead:select` -- A result was chosen. The event's `detail` is
|
|
109
137
|
`{ handle, avatar? }`.
|
|
110
138
|
|
|
111
139
|
```ts
|
|
@@ -138,7 +166,7 @@ import '@substrate-system/actor-typeahead/min/css'
|
|
|
138
166
|
```css
|
|
139
167
|
actor-typeahead {
|
|
140
168
|
--color-background: #fff;
|
|
141
|
-
--color-border: #
|
|
169
|
+
--color-border: #0007;
|
|
142
170
|
--color-shadow: #000;
|
|
143
171
|
--color-hover: #0001;
|
|
144
172
|
--color-avatar-fallback: #0002;
|
|
@@ -158,21 +186,27 @@ of it. The Suggestion list markup is:
|
|
|
158
186
|
<input role="combobox" aria-autocomplete="list" aria-expanded="true"
|
|
159
187
|
aria-controls="actor-typeahead-1-list"
|
|
160
188
|
aria-activedescendant="actor-typeahead-1-option-0">
|
|
161
|
-
<
|
|
189
|
+
<div class="suggestion-list" id="actor-typeahead-1-list"
|
|
162
190
|
role="listbox">
|
|
163
|
-
<
|
|
164
|
-
|
|
191
|
+
<button type="button" class="suggestion"
|
|
192
|
+
id="actor-typeahead-1-option-0" role="option"
|
|
193
|
+
tabindex="-1" aria-selected="true">
|
|
165
194
|
<div class="avatar"><img class="img" alt=""></div>
|
|
166
195
|
<span class="handle"></span>
|
|
167
|
-
</
|
|
168
|
-
</
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
<!-- visually hidden, announces how many Suggestions arrived -->
|
|
199
|
+
<div class="suggestion-count" role="status"></div>
|
|
169
200
|
</actor-typeahead>
|
|
170
201
|
```
|
|
171
202
|
|
|
172
203
|
`aria-selected="true"` marks the Suggestion highlighted with the
|
|
173
204
|
keyboard. The input also gets `autocomplete="off"` unless you set
|
|
174
|
-
`autocomplete` yourself.
|
|
175
|
-
|
|
205
|
+
`autocomplete` yourself. The Suggestion list gets the same name as the
|
|
206
|
+
input, from the input's `aria-labelledby`, its `aria-label`, or its
|
|
207
|
+
`<label>`s, in that order. Your labels do not need an `id`: when any
|
|
208
|
+
label lacks one, the list gets the label text as its `aria-label`. The
|
|
209
|
+
component never writes attributes on your labels.
|
|
176
210
|
|
|
177
211
|
### Avoid a flash of undefined content
|
|
178
212
|
|
|
@@ -64,6 +64,15 @@
|
|
|
64
64
|
"text": "string|null"
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
|
+
{
|
|
68
|
+
"kind": "field",
|
|
69
|
+
"name": "announce",
|
|
70
|
+
"type": {
|
|
71
|
+
"text": "(count:number) => string"
|
|
72
|
+
},
|
|
73
|
+
"default": "defaultAnnounce",
|
|
74
|
+
"description": "Returns the text announced when a Typeahead request settles with\n`count` Suggestions, including 0 when nothing matched the Query.\nDefaults to English. Replace it to translate; the replacement\nhandles plurals and 0. Read at announce time, so it can be set\nafter connect."
|
|
75
|
+
},
|
|
67
76
|
{
|
|
68
77
|
"kind": "field",
|
|
69
78
|
"name": "#id",
|
|
@@ -90,7 +99,25 @@
|
|
|
90
99
|
},
|
|
91
100
|
{
|
|
92
101
|
"kind": "field",
|
|
93
|
-
"name": "#
|
|
102
|
+
"name": "#suggestionList",
|
|
103
|
+
"privacy": "private",
|
|
104
|
+
"type": {
|
|
105
|
+
"text": "HTMLDivElement|null"
|
|
106
|
+
},
|
|
107
|
+
"default": "null"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"kind": "field",
|
|
111
|
+
"name": "#suggestionCount",
|
|
112
|
+
"privacy": "private",
|
|
113
|
+
"type": {
|
|
114
|
+
"text": "HTMLDivElement|null"
|
|
115
|
+
},
|
|
116
|
+
"default": "null"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"kind": "field",
|
|
120
|
+
"name": "#nbsp",
|
|
94
121
|
"privacy": "private",
|
|
95
122
|
"type": {
|
|
96
123
|
"text": "boolean"
|
|
@@ -99,10 +126,18 @@
|
|
|
99
126
|
},
|
|
100
127
|
{
|
|
101
128
|
"kind": "field",
|
|
102
|
-
"name": "#
|
|
129
|
+
"name": "#timer",
|
|
103
130
|
"privacy": "private",
|
|
104
131
|
"type": {
|
|
105
|
-
"text": "
|
|
132
|
+
"text": "ReturnType<typeof setTimeout>|undefined"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"kind": "field",
|
|
137
|
+
"name": "#controller",
|
|
138
|
+
"privacy": "private",
|
|
139
|
+
"type": {
|
|
140
|
+
"text": "AbortController|null"
|
|
106
141
|
},
|
|
107
142
|
"default": "null"
|
|
108
143
|
},
|
|
@@ -153,14 +188,50 @@
|
|
|
153
188
|
}
|
|
154
189
|
]
|
|
155
190
|
},
|
|
191
|
+
{
|
|
192
|
+
"kind": "method",
|
|
193
|
+
"name": "#request",
|
|
194
|
+
"privacy": "private",
|
|
195
|
+
"parameters": [
|
|
196
|
+
{
|
|
197
|
+
"name": "query",
|
|
198
|
+
"type": {
|
|
199
|
+
"text": "string"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"kind": "method",
|
|
206
|
+
"name": "#announce",
|
|
207
|
+
"privacy": "private",
|
|
208
|
+
"parameters": [
|
|
209
|
+
{
|
|
210
|
+
"name": "count",
|
|
211
|
+
"type": {
|
|
212
|
+
"text": "number"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
"description": "Write the Suggestion count. A trailing no-break space alternates\nbetween writes, so screen readers re-read an identical count."
|
|
217
|
+
},
|
|
156
218
|
{
|
|
157
219
|
"kind": "method",
|
|
158
220
|
"name": "#onfocusout",
|
|
159
|
-
"privacy": "private"
|
|
221
|
+
"privacy": "private",
|
|
222
|
+
"parameters": [
|
|
223
|
+
{
|
|
224
|
+
"name": "evt",
|
|
225
|
+
"type": {
|
|
226
|
+
"text": "FocusEvent"
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
"description": "Close only when focus leaves the host. Some assistive technology\nfocuses a Suggestion before it sends the click that selects it."
|
|
160
231
|
},
|
|
161
232
|
{
|
|
162
233
|
"kind": "method",
|
|
163
|
-
"name": "#
|
|
234
|
+
"name": "#onclick",
|
|
164
235
|
"privacy": "private",
|
|
165
236
|
"parameters": [
|
|
166
237
|
{
|
|
@@ -184,6 +255,12 @@
|
|
|
184
255
|
}
|
|
185
256
|
]
|
|
186
257
|
},
|
|
258
|
+
{
|
|
259
|
+
"kind": "method",
|
|
260
|
+
"name": "#cancel",
|
|
261
|
+
"privacy": "private",
|
|
262
|
+
"description": "Stop any pending Typeahead request: the debounce timer, and the\nrequest in flight, whose response is then dropped."
|
|
263
|
+
},
|
|
187
264
|
{
|
|
188
265
|
"kind": "method",
|
|
189
266
|
"name": "#clear",
|
|
@@ -192,7 +269,7 @@
|
|
|
192
269
|
{
|
|
193
270
|
"kind": "method",
|
|
194
271
|
"name": "render",
|
|
195
|
-
"description": "Render into light DOM
|
|
272
|
+
"description": "Render into light DOM. Only the suggestion list is rebuilt,\nso the input element, and anything the consumer attached to it,\nis never replaced. On the input, only the\nARIA combobox attributes are written, plus `autocomplete=\"off\"`\nwhen the page did not set `autocomplete`."
|
|
196
273
|
},
|
|
197
274
|
{
|
|
198
275
|
"kind": "method",
|
package/dist/index.cjs
CHANGED
|
@@ -25,6 +25,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
25
25
|
var import_web_component = require("@substrate-system/web-component");
|
|
26
26
|
const DEFAULT_HOST = "https://public.api.bsky.app";
|
|
27
27
|
const DEFAULT_ROWS = 5;
|
|
28
|
+
const DEBOUNCE_MS = 150;
|
|
28
29
|
const ANCHORED = typeof CSS !== "undefined" && CSS.supports("anchor-scope: --a");
|
|
29
30
|
let instances = 0;
|
|
30
31
|
class ActorTypeahead extends import_web_component.WebComponent.create("actor-typeahead") {
|
|
@@ -32,17 +33,35 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
|
|
|
32
33
|
__name(this, "ActorTypeahead");
|
|
33
34
|
}
|
|
34
35
|
static reflectedStringAttributes = ["host", "rows"];
|
|
36
|
+
/**
|
|
37
|
+
* Returns the text announced when a Typeahead request settles with
|
|
38
|
+
* `count` Suggestions, including 0 when nothing matched the Query.
|
|
39
|
+
* Defaults to English. Replace it to translate; the replacement
|
|
40
|
+
* handles plurals and 0. Read at announce time, so it can be set
|
|
41
|
+
* after connect.
|
|
42
|
+
*/
|
|
43
|
+
announce = defaultAnnounce;
|
|
35
44
|
#id = `actor-typeahead-${++instances}`;
|
|
36
45
|
#actors = [];
|
|
37
46
|
#index = -1;
|
|
38
|
-
#pressed = false;
|
|
39
47
|
#suggestionList = null;
|
|
48
|
+
#suggestionCount = null;
|
|
49
|
+
#nbsp = false;
|
|
50
|
+
#timer;
|
|
51
|
+
#controller = null;
|
|
40
52
|
constructor() {
|
|
41
53
|
super();
|
|
42
54
|
this.addEventListener("input", this);
|
|
43
55
|
this.addEventListener("focusout", this);
|
|
44
56
|
this.addEventListener("keydown", this);
|
|
45
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Stop any pending Typeahead request. The Suggestion list is left as
|
|
60
|
+
* is; the next Query rebuilds it.
|
|
61
|
+
*/
|
|
62
|
+
disconnectedCallback() {
|
|
63
|
+
this.#cancel();
|
|
64
|
+
}
|
|
46
65
|
get #limit() {
|
|
47
66
|
const rows = Number.parseInt(this.rows ?? "");
|
|
48
67
|
return Number.isNaN(rows) ? DEFAULT_ROWS : rows;
|
|
@@ -56,18 +75,23 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
|
|
|
56
75
|
if (evt instanceof KeyboardEvent) this.#onkeydown(evt);
|
|
57
76
|
break;
|
|
58
77
|
case "focusout":
|
|
59
|
-
this.#onfocusout();
|
|
78
|
+
if (evt instanceof FocusEvent) this.#onfocusout(evt);
|
|
60
79
|
break;
|
|
61
|
-
case "
|
|
62
|
-
|
|
80
|
+
case "mousedown":
|
|
81
|
+
evt.preventDefault();
|
|
63
82
|
break;
|
|
64
|
-
case "
|
|
65
|
-
this.#
|
|
83
|
+
case "click":
|
|
84
|
+
this.#onclick(evt);
|
|
66
85
|
break;
|
|
67
86
|
}
|
|
68
87
|
}
|
|
69
88
|
#onkeydown(evt) {
|
|
70
89
|
const last = this.#actors.length - 1;
|
|
90
|
+
const moves = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"];
|
|
91
|
+
if (last < 0 && moves.includes(evt.key)) {
|
|
92
|
+
evt.preventDefault();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
71
95
|
switch (evt.key) {
|
|
72
96
|
case "ArrowDown":
|
|
73
97
|
evt.preventDefault();
|
|
@@ -81,7 +105,7 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
|
|
|
81
105
|
break;
|
|
82
106
|
case "ArrowUp":
|
|
83
107
|
evt.preventDefault();
|
|
84
|
-
this.#index =
|
|
108
|
+
this.#index = this.#index < 0 ? last : this.#index - 1;
|
|
85
109
|
this.render();
|
|
86
110
|
break;
|
|
87
111
|
case "PageUp":
|
|
@@ -90,45 +114,78 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
|
|
|
90
114
|
this.render();
|
|
91
115
|
break;
|
|
92
116
|
case "Escape":
|
|
93
|
-
evt.preventDefault();
|
|
117
|
+
if (this.#actors.length) evt.preventDefault();
|
|
94
118
|
this.#clear();
|
|
95
119
|
break;
|
|
96
120
|
case "Enter":
|
|
121
|
+
if (!this.#actors[this.#index]) {
|
|
122
|
+
this.#cancel();
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
97
125
|
evt.preventDefault();
|
|
98
126
|
this.#select(this.#index);
|
|
99
127
|
break;
|
|
100
128
|
}
|
|
101
129
|
}
|
|
102
|
-
|
|
130
|
+
#oninput(evt) {
|
|
103
131
|
if (!(evt.target instanceof HTMLInputElement)) return;
|
|
104
132
|
const query = evt.target.value;
|
|
105
133
|
if (!query) {
|
|
106
134
|
this.#clear();
|
|
107
135
|
return;
|
|
108
136
|
}
|
|
137
|
+
this.#cancel();
|
|
138
|
+
this.#timer = setTimeout(() => this.#request(query), DEBOUNCE_MS);
|
|
139
|
+
}
|
|
140
|
+
async #request(query) {
|
|
141
|
+
this.#timer = void 0;
|
|
109
142
|
const url = new URL(
|
|
110
143
|
"xrpc/app.bsky.actor.searchActorsTypeahead",
|
|
111
144
|
this.host ?? DEFAULT_HOST
|
|
112
145
|
);
|
|
113
146
|
url.searchParams.set("q", query);
|
|
114
147
|
url.searchParams.set("limit", String(this.#limit));
|
|
115
|
-
const
|
|
116
|
-
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
this.#controller = controller;
|
|
150
|
+
const { signal } = controller;
|
|
151
|
+
let json;
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetch(url, { signal });
|
|
154
|
+
if (signal.aborted) return;
|
|
155
|
+
if (!res.ok) throw new Error(`status ${res.status}`);
|
|
156
|
+
json = await res.json();
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (isAbortError(err)) return;
|
|
159
|
+
this.#clear();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (signal.aborted) return;
|
|
117
163
|
this.#actors = parseActors(json);
|
|
118
164
|
this.#index = -1;
|
|
119
165
|
this.render();
|
|
166
|
+
this.#announce(this.#actors.length);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Write the Suggestion count. A trailing no-break space alternates
|
|
170
|
+
* between writes, so screen readers re-read an identical count.
|
|
171
|
+
*/
|
|
172
|
+
#announce(count) {
|
|
173
|
+
if (!this.#suggestionCount) return;
|
|
174
|
+
this.#nbsp = !this.#nbsp;
|
|
175
|
+
this.#suggestionCount.textContent = this.announce(count) + (this.#nbsp ? "\xA0" : "");
|
|
120
176
|
}
|
|
121
|
-
|
|
122
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Close only when focus leaves the host. Some assistive technology
|
|
179
|
+
* focuses a Suggestion before it sends the click that selects it.
|
|
180
|
+
*/
|
|
181
|
+
#onfocusout(evt) {
|
|
182
|
+
const next = evt.relatedTarget;
|
|
183
|
+
if (next instanceof Node && this.contains(next)) return;
|
|
123
184
|
this.#clear();
|
|
124
185
|
}
|
|
125
|
-
#
|
|
126
|
-
this.#pressed = false;
|
|
186
|
+
#onclick(evt) {
|
|
127
187
|
const option = evt.target instanceof Element ? evt.target.closest('[role="option"]') : null;
|
|
128
|
-
if (!option)
|
|
129
|
-
this.qs("input")?.focus();
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
188
|
+
if (!option) return;
|
|
132
189
|
this.#select(Number(option.dataset.index));
|
|
133
190
|
}
|
|
134
191
|
#select(index) {
|
|
@@ -140,30 +197,49 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
|
|
|
140
197
|
this.#clear();
|
|
141
198
|
this.emit("select", { detail: actor });
|
|
142
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Stop any pending Typeahead request: the debounce timer, and the
|
|
202
|
+
* request in flight, whose response is then dropped.
|
|
203
|
+
*/
|
|
204
|
+
#cancel() {
|
|
205
|
+
clearTimeout(this.#timer);
|
|
206
|
+
this.#timer = void 0;
|
|
207
|
+
this.#controller?.abort();
|
|
208
|
+
this.#controller = null;
|
|
209
|
+
}
|
|
143
210
|
#clear() {
|
|
211
|
+
this.#cancel();
|
|
144
212
|
this.#actors = [];
|
|
145
213
|
this.#index = -1;
|
|
146
214
|
this.render();
|
|
147
215
|
}
|
|
148
216
|
/**
|
|
149
|
-
* Render into light DOM
|
|
150
|
-
*
|
|
151
|
-
*
|
|
217
|
+
* Render into light DOM. Only the suggestion list is rebuilt,
|
|
218
|
+
* so the input element, and anything the consumer attached to it,
|
|
219
|
+
* is never replaced. On the input, only the
|
|
152
220
|
* ARIA combobox attributes are written, plus `autocomplete="off"`
|
|
153
221
|
* when the page did not set `autocomplete`.
|
|
154
222
|
*/
|
|
155
223
|
render() {
|
|
156
224
|
if (!this.#suggestionList) {
|
|
157
|
-
this.#suggestionList = document.createElement("
|
|
225
|
+
this.#suggestionList = document.createElement("div");
|
|
158
226
|
this.#suggestionList.className = "suggestion-list";
|
|
159
227
|
this.#suggestionList.id = `${this.#id}-list`;
|
|
160
228
|
this.#suggestionList.setAttribute("role", "listbox");
|
|
161
229
|
if (ANCHORED) this.#suggestionList.popover = "manual";
|
|
162
|
-
this.#suggestionList.addEventListener("
|
|
163
|
-
this.#suggestionList.addEventListener("
|
|
230
|
+
this.#suggestionList.addEventListener("mousedown", this);
|
|
231
|
+
this.#suggestionList.addEventListener("click", this);
|
|
164
232
|
}
|
|
165
233
|
const list = this.#suggestionList;
|
|
166
234
|
if (list.parentNode !== this) this.append(list);
|
|
235
|
+
if (!this.#suggestionCount) {
|
|
236
|
+
this.#suggestionCount = document.createElement("div");
|
|
237
|
+
this.#suggestionCount.className = "suggestion-count";
|
|
238
|
+
this.#suggestionCount.setAttribute("role", "status");
|
|
239
|
+
}
|
|
240
|
+
const count = this.#suggestionCount;
|
|
241
|
+
if (count.parentNode !== this) this.append(count);
|
|
242
|
+
if (!this.#actors.length) count.textContent = "";
|
|
167
243
|
list.replaceChildren(...this.#actors.map((actor, i) => {
|
|
168
244
|
const option = suggestion(actor, i, i === this.#index);
|
|
169
245
|
option.id = this.#optionId(i);
|
|
@@ -179,11 +255,11 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
|
|
|
179
255
|
}
|
|
180
256
|
input.setAttribute("aria-controls", list.id);
|
|
181
257
|
input.setAttribute("aria-expanded", String(open));
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
list.removeAttribute(
|
|
258
|
+
const name = nameSource(input, this);
|
|
259
|
+
for (const attr of ["aria-labelledby", "aria-label"]) {
|
|
260
|
+
const value = name[attr];
|
|
261
|
+
if (value) list.setAttribute(attr, value);
|
|
262
|
+
else list.removeAttribute(attr);
|
|
187
263
|
}
|
|
188
264
|
if (this.#actors[this.#index]) {
|
|
189
265
|
input.setAttribute(
|
|
@@ -215,12 +291,14 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
|
|
|
215
291
|
}
|
|
216
292
|
ActorTypeahead.define();
|
|
217
293
|
function suggestion(actor, index, active) {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
294
|
+
const button = document.createElement("button");
|
|
295
|
+
button.type = "button";
|
|
296
|
+
button.tabIndex = -1;
|
|
297
|
+
button.className = "suggestion";
|
|
298
|
+
button.setAttribute("role", "option");
|
|
299
|
+
button.setAttribute("aria-selected", String(active));
|
|
300
|
+
button.dataset.index = String(index);
|
|
301
|
+
button.dataset.handle = actor.handle;
|
|
224
302
|
const avatar = document.createElement("div");
|
|
225
303
|
avatar.className = "avatar";
|
|
226
304
|
const img = document.createElement("img");
|
|
@@ -231,10 +309,40 @@ function suggestion(actor, index, active) {
|
|
|
231
309
|
handle.className = "handle";
|
|
232
310
|
handle.textContent = actor.handle;
|
|
233
311
|
avatar.append(img);
|
|
234
|
-
|
|
235
|
-
return
|
|
312
|
+
button.append(avatar, handle);
|
|
313
|
+
return button;
|
|
236
314
|
}
|
|
237
315
|
__name(suggestion, "suggestion");
|
|
316
|
+
function nameSource(input, host) {
|
|
317
|
+
const labelledby = input.getAttribute("aria-labelledby");
|
|
318
|
+
if (labelledby) return { "aria-labelledby": labelledby };
|
|
319
|
+
const label = input.getAttribute("aria-label");
|
|
320
|
+
if (label) return { "aria-label": label };
|
|
321
|
+
const labels = [...input.labels ?? []];
|
|
322
|
+
if (!labels.length) return {};
|
|
323
|
+
if (labels.every((label2) => label2.id)) {
|
|
324
|
+
return {
|
|
325
|
+
"aria-labelledby": labels.map((label2) => label2.id).join(" ")
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const text = labels.map((label2) => labelText(label2, host).trim()).filter(Boolean).join(" ");
|
|
329
|
+
return text ? { "aria-label": text } : {};
|
|
330
|
+
}
|
|
331
|
+
__name(nameSource, "nameSource");
|
|
332
|
+
function labelText(label, host) {
|
|
333
|
+
const walker = document.createTreeWalker(label, NodeFilter.SHOW_TEXT, {
|
|
334
|
+
acceptNode: /* @__PURE__ */ __name((node) => host.contains(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT, "acceptNode")
|
|
335
|
+
});
|
|
336
|
+
let text = "";
|
|
337
|
+
while (walker.nextNode()) text += walker.currentNode.nodeValue;
|
|
338
|
+
return text;
|
|
339
|
+
}
|
|
340
|
+
__name(labelText, "labelText");
|
|
341
|
+
function defaultAnnounce(count) {
|
|
342
|
+
if (count === 0) return "No Suggestions";
|
|
343
|
+
return count === 1 ? "1 Suggestion available" : `${count} Suggestions available`;
|
|
344
|
+
}
|
|
345
|
+
__name(defaultAnnounce, "defaultAnnounce");
|
|
238
346
|
function parseActors(json) {
|
|
239
347
|
if (!isRecord(json) || !Array.isArray(json.actors)) return [];
|
|
240
348
|
return json.actors.flatMap((actor) => {
|
|
@@ -243,6 +351,10 @@ function parseActors(json) {
|
|
|
243
351
|
});
|
|
244
352
|
}
|
|
245
353
|
__name(parseActors, "parseActors");
|
|
354
|
+
function isAbortError(err) {
|
|
355
|
+
return err instanceof DOMException && err.name === "AbortError";
|
|
356
|
+
}
|
|
357
|
+
__name(isAbortError, "isAbortError");
|
|
246
358
|
function isRecord(value) {
|
|
247
359
|
return typeof value === "object" && value !== null;
|
|
248
360
|
}
|