@reticular/speakable 1.5.0 → 1.7.0
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 +86 -2
- package/dist/browser/index.d.ts +58 -0
- package/dist/browser/index.js +3978 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/chunk-WYFXGXYU.js +87 -0
- package/dist/chunk-WYFXGXYU.js.map +1 -0
- package/dist/cli.js +40 -19
- package/dist/cli.js.map +1 -1
- package/dist/harness/index.d.ts +57 -0
- package/dist/harness/index.js +176 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/index.js +44 -22
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +40 -19
- package/dist/mcp.js.map +1 -1
- package/dist/speakable-browser.global.js +10 -0
- package/dist/speakable-browser.global.js.map +1 -0
- package/dist/storybook/dist/chunk-3SVI6IQE.js +86 -0
- package/dist/storybook/dist/chunk-JSBRDJBE.js +30 -0
- package/dist/storybook/dist/chunk-QVJRQZT5.js +16 -0
- package/dist/storybook/dist/index.js +17 -0
- package/dist/storybook/dist/manager.js +701 -0
- package/dist/storybook/dist/preset.js +38 -0
- package/dist/storybook/dist/preview.js +4199 -0
- package/dist/storybook/dist/upgrade-5GL42JH4.js +7 -0
- package/dist/types-Blh0EnZF.d.ts +213 -0
- package/dist/upgrade-53M5VYGH.js +7 -0
- package/dist/upgrade-53M5VYGH.js.map +1 -0
- package/package.json +28 -3
|
@@ -0,0 +1,3978 @@
|
|
|
1
|
+
import {
|
|
2
|
+
awaitCustomElementsReady
|
|
3
|
+
} from "../chunk-WYFXGXYU.js";
|
|
4
|
+
|
|
5
|
+
// src/model/types.ts
|
|
6
|
+
var SUPPORTED_ROLES = [
|
|
7
|
+
// Original roles (22)
|
|
8
|
+
"button",
|
|
9
|
+
"link",
|
|
10
|
+
"heading",
|
|
11
|
+
"textbox",
|
|
12
|
+
"checkbox",
|
|
13
|
+
"radio",
|
|
14
|
+
"combobox",
|
|
15
|
+
"listbox",
|
|
16
|
+
"option",
|
|
17
|
+
"list",
|
|
18
|
+
"listitem",
|
|
19
|
+
"navigation",
|
|
20
|
+
"main",
|
|
21
|
+
"banner",
|
|
22
|
+
"contentinfo",
|
|
23
|
+
"region",
|
|
24
|
+
"img",
|
|
25
|
+
"article",
|
|
26
|
+
"complementary",
|
|
27
|
+
"form",
|
|
28
|
+
"search",
|
|
29
|
+
"generic",
|
|
30
|
+
// New roles (21)
|
|
31
|
+
"paragraph",
|
|
32
|
+
"blockquote",
|
|
33
|
+
"code",
|
|
34
|
+
"staticText",
|
|
35
|
+
"table",
|
|
36
|
+
"row",
|
|
37
|
+
"cell",
|
|
38
|
+
"columnheader",
|
|
39
|
+
"rowheader",
|
|
40
|
+
"term",
|
|
41
|
+
"definition",
|
|
42
|
+
"figure",
|
|
43
|
+
"caption",
|
|
44
|
+
"group",
|
|
45
|
+
"dialog",
|
|
46
|
+
"meter",
|
|
47
|
+
"progressbar",
|
|
48
|
+
"status",
|
|
49
|
+
"document",
|
|
50
|
+
"application",
|
|
51
|
+
"separator"
|
|
52
|
+
];
|
|
53
|
+
var CURRENT_MODEL_VERSION = {
|
|
54
|
+
major: 1,
|
|
55
|
+
minor: 0
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/extractor/aria-name.ts
|
|
59
|
+
function computeAccessibleName(element, visited = /* @__PURE__ */ new Set()) {
|
|
60
|
+
const warnings = [];
|
|
61
|
+
if (visited.has(element)) {
|
|
62
|
+
warnings.push({
|
|
63
|
+
message: "Circular reference detected in aria-labelledby chain",
|
|
64
|
+
element
|
|
65
|
+
});
|
|
66
|
+
return { name: "", warnings };
|
|
67
|
+
}
|
|
68
|
+
visited.add(element);
|
|
69
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
70
|
+
if (labelledBy) {
|
|
71
|
+
const name = computeNameFromLabelledBy(element, labelledBy, visited, warnings);
|
|
72
|
+
return { name, warnings };
|
|
73
|
+
}
|
|
74
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
75
|
+
if (ariaLabel && ariaLabel.trim()) {
|
|
76
|
+
return { name: ariaLabel.trim(), warnings };
|
|
77
|
+
}
|
|
78
|
+
if (isFormControl(element)) {
|
|
79
|
+
const labelName = computeNameFromLabel(element);
|
|
80
|
+
if (labelName) {
|
|
81
|
+
return { name: labelName, warnings };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (element.tagName.toLowerCase() === "img") {
|
|
85
|
+
const alt = element.getAttribute("alt");
|
|
86
|
+
if (alt !== null) {
|
|
87
|
+
return { name: alt.trim(), warnings };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (supportsNameFromContent(element)) {
|
|
91
|
+
const textName = computeNameFromContent(element, visited);
|
|
92
|
+
if (textName) {
|
|
93
|
+
return { name: textName, warnings };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const title = element.getAttribute("title");
|
|
97
|
+
if (title && title.trim()) {
|
|
98
|
+
return { name: title.trim(), warnings };
|
|
99
|
+
}
|
|
100
|
+
return { name: "", warnings };
|
|
101
|
+
}
|
|
102
|
+
function computeNameFromLabelledBy(element, labelledBy, visited, warnings) {
|
|
103
|
+
const document = element.ownerDocument;
|
|
104
|
+
if (!document) {
|
|
105
|
+
return "";
|
|
106
|
+
}
|
|
107
|
+
const ids = labelledBy.trim().split(/\s+/);
|
|
108
|
+
const names = [];
|
|
109
|
+
for (const id of ids) {
|
|
110
|
+
if (!id) continue;
|
|
111
|
+
const referencedElement = document.getElementById(id);
|
|
112
|
+
if (!referencedElement) {
|
|
113
|
+
warnings.push({
|
|
114
|
+
message: `aria-labelledby references non-existent ID: ${id}`,
|
|
115
|
+
element
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const result = computeAccessibleName(referencedElement, new Set(visited));
|
|
120
|
+
warnings.push(...result.warnings);
|
|
121
|
+
const name = result.name || getTextContent(referencedElement);
|
|
122
|
+
if (name) {
|
|
123
|
+
names.push(name);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return names.join(" ").trim();
|
|
127
|
+
}
|
|
128
|
+
function computeNameFromLabel(element) {
|
|
129
|
+
const document = element.ownerDocument;
|
|
130
|
+
if (!document) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
const id = element.getAttribute("id");
|
|
134
|
+
if (id) {
|
|
135
|
+
const label = document.querySelector(`label[for="${id}"]`);
|
|
136
|
+
if (label) {
|
|
137
|
+
return getTextContent(label);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const parentLabel = element.closest("label");
|
|
141
|
+
if (parentLabel) {
|
|
142
|
+
return getTextContent(parentLabel);
|
|
143
|
+
}
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
function computeNameFromContent(element, _visited) {
|
|
147
|
+
return getTextContent(element);
|
|
148
|
+
}
|
|
149
|
+
function getTextContent(element) {
|
|
150
|
+
if (element.getAttribute("aria-hidden") === "true") {
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
let text = "";
|
|
154
|
+
for (const node of Array.from(element.childNodes)) {
|
|
155
|
+
if (node.nodeType === 3) {
|
|
156
|
+
text += node.textContent || "";
|
|
157
|
+
} else if (node.nodeType === 1) {
|
|
158
|
+
const childElement = node;
|
|
159
|
+
if (childElement.getAttribute("aria-hidden") === "true") {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
text += getTextContent(childElement);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return text.trim();
|
|
166
|
+
}
|
|
167
|
+
function isFormControl(element) {
|
|
168
|
+
const tagName = element.tagName.toLowerCase();
|
|
169
|
+
return tagName === "input" || tagName === "textarea" || tagName === "select" || tagName === "button";
|
|
170
|
+
}
|
|
171
|
+
function supportsNameFromContent(element) {
|
|
172
|
+
const tagName = element.tagName.toLowerCase();
|
|
173
|
+
const role = element.getAttribute("role");
|
|
174
|
+
const contentElements = [
|
|
175
|
+
"button",
|
|
176
|
+
"a",
|
|
177
|
+
"h1",
|
|
178
|
+
"h2",
|
|
179
|
+
"h3",
|
|
180
|
+
"h4",
|
|
181
|
+
"h5",
|
|
182
|
+
"h6",
|
|
183
|
+
"summary",
|
|
184
|
+
"figcaption",
|
|
185
|
+
"legend",
|
|
186
|
+
"caption",
|
|
187
|
+
"th",
|
|
188
|
+
"td",
|
|
189
|
+
"li",
|
|
190
|
+
"dt",
|
|
191
|
+
"dd"
|
|
192
|
+
];
|
|
193
|
+
if (contentElements.includes(tagName)) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
const contentRoles = [
|
|
197
|
+
"button",
|
|
198
|
+
"link",
|
|
199
|
+
"heading",
|
|
200
|
+
"tab",
|
|
201
|
+
"treeitem",
|
|
202
|
+
"option",
|
|
203
|
+
"row",
|
|
204
|
+
"cell",
|
|
205
|
+
"columnheader",
|
|
206
|
+
"rowheader",
|
|
207
|
+
"tooltip",
|
|
208
|
+
"menuitem",
|
|
209
|
+
"menuitemcheckbox",
|
|
210
|
+
"menuitemradio"
|
|
211
|
+
];
|
|
212
|
+
if (role && contentRoles.includes(role)) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
function computeAccessibleDescription(element) {
|
|
218
|
+
const warnings = [];
|
|
219
|
+
const describedBy = element.getAttribute("aria-describedby");
|
|
220
|
+
if (describedBy) {
|
|
221
|
+
const description = computeDescriptionFromDescribedBy(
|
|
222
|
+
element,
|
|
223
|
+
describedBy,
|
|
224
|
+
warnings
|
|
225
|
+
);
|
|
226
|
+
if (description) {
|
|
227
|
+
return { description, warnings };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const title = element.getAttribute("title");
|
|
231
|
+
if (title && title.trim()) {
|
|
232
|
+
return { description: title.trim(), warnings };
|
|
233
|
+
}
|
|
234
|
+
return { description: "", warnings };
|
|
235
|
+
}
|
|
236
|
+
function computeDescriptionFromDescribedBy(element, describedBy, warnings) {
|
|
237
|
+
const document = element.ownerDocument;
|
|
238
|
+
if (!document) {
|
|
239
|
+
return "";
|
|
240
|
+
}
|
|
241
|
+
const ids = describedBy.trim().split(/\s+/);
|
|
242
|
+
const descriptions = [];
|
|
243
|
+
for (const id of ids) {
|
|
244
|
+
if (!id) continue;
|
|
245
|
+
const referencedElement = document.getElementById(id);
|
|
246
|
+
if (!referencedElement) {
|
|
247
|
+
warnings.push({
|
|
248
|
+
message: `aria-describedby references non-existent ID: ${id}`,
|
|
249
|
+
element
|
|
250
|
+
});
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const text = getTextContent(referencedElement);
|
|
254
|
+
if (text) {
|
|
255
|
+
descriptions.push(text);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return descriptions.join(" ").trim();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/extractor/role-mapper.ts
|
|
262
|
+
var IMPLICIT_ROLE_MAP = {
|
|
263
|
+
// Interactive elements
|
|
264
|
+
"button": "button",
|
|
265
|
+
"a": "link",
|
|
266
|
+
// Only if href attribute present
|
|
267
|
+
// Form controls
|
|
268
|
+
"input": "textbox",
|
|
269
|
+
// Default, varies by type
|
|
270
|
+
"textarea": "textbox",
|
|
271
|
+
"select": "listbox",
|
|
272
|
+
// Headings
|
|
273
|
+
"h1": "heading",
|
|
274
|
+
"h2": "heading",
|
|
275
|
+
"h3": "heading",
|
|
276
|
+
"h4": "heading",
|
|
277
|
+
"h5": "heading",
|
|
278
|
+
"h6": "heading",
|
|
279
|
+
// Landmarks
|
|
280
|
+
"nav": "navigation",
|
|
281
|
+
"main": "main",
|
|
282
|
+
"header": "banner",
|
|
283
|
+
// Only if not nested in article/section
|
|
284
|
+
"footer": "contentinfo",
|
|
285
|
+
// Only if not nested in article/section
|
|
286
|
+
"aside": "complementary",
|
|
287
|
+
"form": "form",
|
|
288
|
+
"section": "region",
|
|
289
|
+
// Only if has accessible name
|
|
290
|
+
// Lists
|
|
291
|
+
"ul": "list",
|
|
292
|
+
"ol": "list",
|
|
293
|
+
"li": "listitem",
|
|
294
|
+
// Images
|
|
295
|
+
"img": "img",
|
|
296
|
+
// Articles
|
|
297
|
+
"article": "article",
|
|
298
|
+
// Static content (new)
|
|
299
|
+
"p": "paragraph",
|
|
300
|
+
"blockquote": "blockquote",
|
|
301
|
+
"code": "code",
|
|
302
|
+
"pre": "code",
|
|
303
|
+
// Tables (new)
|
|
304
|
+
"table": "table",
|
|
305
|
+
"tr": "row",
|
|
306
|
+
"td": "cell",
|
|
307
|
+
"th": "columnheader",
|
|
308
|
+
// Default; scope="row" handled in computeImplicitRole
|
|
309
|
+
// Definition lists (new)
|
|
310
|
+
"dl": "list",
|
|
311
|
+
"dt": "term",
|
|
312
|
+
"dd": "definition",
|
|
313
|
+
// Figures (new)
|
|
314
|
+
"figure": "figure",
|
|
315
|
+
"figcaption": "caption",
|
|
316
|
+
// Disclosure (new)
|
|
317
|
+
"details": "group",
|
|
318
|
+
"summary": "button",
|
|
319
|
+
// Dialogs and widgets (new)
|
|
320
|
+
"dialog": "dialog",
|
|
321
|
+
"meter": "meter",
|
|
322
|
+
"progress": "progressbar",
|
|
323
|
+
"output": "status",
|
|
324
|
+
// Forms (new)
|
|
325
|
+
"fieldset": "group",
|
|
326
|
+
"legend": "caption",
|
|
327
|
+
// Embedded content (new)
|
|
328
|
+
"iframe": "document",
|
|
329
|
+
"video": "application",
|
|
330
|
+
"audio": "application",
|
|
331
|
+
// Separators (new)
|
|
332
|
+
"hr": "separator",
|
|
333
|
+
// Table caption (new)
|
|
334
|
+
"caption": "caption"
|
|
335
|
+
};
|
|
336
|
+
var INPUT_TYPE_ROLE_MAP = {
|
|
337
|
+
"button": "button",
|
|
338
|
+
"submit": "button",
|
|
339
|
+
"reset": "button",
|
|
340
|
+
"checkbox": "checkbox",
|
|
341
|
+
"radio": "radio",
|
|
342
|
+
"text": "textbox",
|
|
343
|
+
"email": "textbox",
|
|
344
|
+
"password": "textbox",
|
|
345
|
+
"search": "textbox",
|
|
346
|
+
"tel": "textbox",
|
|
347
|
+
"url": "textbox",
|
|
348
|
+
"number": "textbox"
|
|
349
|
+
};
|
|
350
|
+
function computeRole(element) {
|
|
351
|
+
const warnings = [];
|
|
352
|
+
const explicitRole = element.getAttribute("role");
|
|
353
|
+
if (explicitRole) {
|
|
354
|
+
const role = validateAndNormalizeRole(explicitRole, element, warnings);
|
|
355
|
+
if (role) {
|
|
356
|
+
return { role, warnings };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const implicitRole = computeImplicitRole(element, warnings);
|
|
360
|
+
return { role: implicitRole, warnings };
|
|
361
|
+
}
|
|
362
|
+
function validateAndNormalizeRole(roleAttr, element, warnings) {
|
|
363
|
+
const role = roleAttr.trim().toLowerCase();
|
|
364
|
+
if (role === "presentation" || role === "none") {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
if (SUPPORTED_ROLES.includes(role)) {
|
|
368
|
+
return role;
|
|
369
|
+
}
|
|
370
|
+
warnings.push({
|
|
371
|
+
message: `Invalid or unsupported role: "${roleAttr}". Falling back to implicit role.`,
|
|
372
|
+
element
|
|
373
|
+
});
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
function computeImplicitRole(element, _warnings) {
|
|
377
|
+
const tagName = element.tagName.toLowerCase();
|
|
378
|
+
if (tagName === "input") {
|
|
379
|
+
return computeInputRole(element);
|
|
380
|
+
}
|
|
381
|
+
if (tagName === "a") {
|
|
382
|
+
return element.hasAttribute("href") ? "link" : null;
|
|
383
|
+
}
|
|
384
|
+
if (tagName === "header") {
|
|
385
|
+
return isTopLevelLandmark(element) ? "banner" : null;
|
|
386
|
+
}
|
|
387
|
+
if (tagName === "footer") {
|
|
388
|
+
return isTopLevelLandmark(element) ? "contentinfo" : null;
|
|
389
|
+
}
|
|
390
|
+
if (tagName === "th") {
|
|
391
|
+
const scope = element.getAttribute("scope");
|
|
392
|
+
return scope === "row" ? "rowheader" : "columnheader";
|
|
393
|
+
}
|
|
394
|
+
if (tagName === "section") {
|
|
395
|
+
return element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby") ? "region" : null;
|
|
396
|
+
}
|
|
397
|
+
const implicitRole = IMPLICIT_ROLE_MAP[tagName];
|
|
398
|
+
return implicitRole || null;
|
|
399
|
+
}
|
|
400
|
+
function computeInputRole(input) {
|
|
401
|
+
const type = (input.getAttribute("type") || "text").toLowerCase();
|
|
402
|
+
return INPUT_TYPE_ROLE_MAP[type] || "textbox";
|
|
403
|
+
}
|
|
404
|
+
function isTopLevelLandmark(element) {
|
|
405
|
+
let parent = element.parentElement;
|
|
406
|
+
while (parent) {
|
|
407
|
+
const tagName = parent.tagName.toLowerCase();
|
|
408
|
+
if (tagName === "article" || tagName === "section") {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
parent = parent.parentElement;
|
|
412
|
+
}
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
function isAccessible(element) {
|
|
416
|
+
if (element.getAttribute("aria-hidden") === "true") {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
const role = element.getAttribute("role");
|
|
420
|
+
if (role === "presentation" || role === "none") {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
const roleResult = computeRole(element);
|
|
424
|
+
return roleResult.role !== null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/extractor/state-extractor.ts
|
|
428
|
+
function extractState(element) {
|
|
429
|
+
const warnings = [];
|
|
430
|
+
const state = {};
|
|
431
|
+
const expanded = extractBooleanAttribute(element, "aria-expanded", warnings);
|
|
432
|
+
if (expanded !== void 0) {
|
|
433
|
+
state.expanded = expanded;
|
|
434
|
+
}
|
|
435
|
+
const checked = extractTriStateAttribute(element, "aria-checked", warnings);
|
|
436
|
+
if (checked !== void 0) {
|
|
437
|
+
state.checked = checked;
|
|
438
|
+
}
|
|
439
|
+
const pressed = extractTriStateAttribute(element, "aria-pressed", warnings);
|
|
440
|
+
if (pressed !== void 0) {
|
|
441
|
+
state.pressed = pressed;
|
|
442
|
+
}
|
|
443
|
+
const selected = extractBooleanAttribute(element, "aria-selected", warnings);
|
|
444
|
+
if (selected !== void 0) {
|
|
445
|
+
state.selected = selected;
|
|
446
|
+
}
|
|
447
|
+
const disabled = extractBooleanAttribute(element, "aria-disabled", warnings);
|
|
448
|
+
if (disabled !== void 0) {
|
|
449
|
+
state.disabled = disabled;
|
|
450
|
+
}
|
|
451
|
+
const invalid = extractBooleanAttribute(element, "aria-invalid", warnings);
|
|
452
|
+
if (invalid !== void 0) {
|
|
453
|
+
state.invalid = invalid;
|
|
454
|
+
}
|
|
455
|
+
const required = extractBooleanAttribute(element, "aria-required", warnings);
|
|
456
|
+
if (required !== void 0) {
|
|
457
|
+
state.required = required;
|
|
458
|
+
}
|
|
459
|
+
const readonly = extractBooleanAttribute(element, "aria-readonly", warnings);
|
|
460
|
+
if (readonly !== void 0) {
|
|
461
|
+
state.readonly = readonly;
|
|
462
|
+
}
|
|
463
|
+
const busy = extractBooleanAttribute(element, "aria-busy", warnings);
|
|
464
|
+
if (busy !== void 0) {
|
|
465
|
+
state.busy = busy;
|
|
466
|
+
}
|
|
467
|
+
const current = extractCurrentAttribute(element, warnings);
|
|
468
|
+
if (current !== void 0) {
|
|
469
|
+
state.current = current;
|
|
470
|
+
}
|
|
471
|
+
const grabbed = extractBooleanAttribute(element, "aria-grabbed", warnings);
|
|
472
|
+
if (grabbed !== void 0) {
|
|
473
|
+
state.grabbed = grabbed;
|
|
474
|
+
}
|
|
475
|
+
const hidden = extractBooleanAttribute(element, "aria-hidden", warnings);
|
|
476
|
+
if (hidden !== void 0) {
|
|
477
|
+
state.hidden = hidden;
|
|
478
|
+
}
|
|
479
|
+
const level = extractLevelAttribute(element, warnings);
|
|
480
|
+
if (level !== void 0) {
|
|
481
|
+
state.level = level;
|
|
482
|
+
}
|
|
483
|
+
const posinset = extractNumberAttribute(element, "aria-posinset", warnings);
|
|
484
|
+
if (posinset !== void 0) {
|
|
485
|
+
state.posinset = posinset;
|
|
486
|
+
}
|
|
487
|
+
const setsize = extractNumberAttribute(element, "aria-setsize", warnings);
|
|
488
|
+
if (setsize !== void 0) {
|
|
489
|
+
state.setsize = setsize;
|
|
490
|
+
}
|
|
491
|
+
extractNativeStates(element, state, warnings);
|
|
492
|
+
return { state, warnings };
|
|
493
|
+
}
|
|
494
|
+
function extractValue(element) {
|
|
495
|
+
const warnings = [];
|
|
496
|
+
const tagName = element.tagName.toLowerCase();
|
|
497
|
+
if (tagName === "input") {
|
|
498
|
+
return extractInputValue(element, warnings);
|
|
499
|
+
}
|
|
500
|
+
if (tagName === "textarea") {
|
|
501
|
+
return extractTextareaValue(element, warnings);
|
|
502
|
+
}
|
|
503
|
+
if (tagName === "select") {
|
|
504
|
+
return extractSelectValue(element, warnings);
|
|
505
|
+
}
|
|
506
|
+
const valueNow = element.getAttribute("aria-valuenow");
|
|
507
|
+
if (valueNow !== null) {
|
|
508
|
+
const current = parseFloat(valueNow);
|
|
509
|
+
if (!isNaN(current)) {
|
|
510
|
+
const valueMin = element.getAttribute("aria-valuemin");
|
|
511
|
+
const valueMax = element.getAttribute("aria-valuemax");
|
|
512
|
+
const valueText = element.getAttribute("aria-valuetext");
|
|
513
|
+
return {
|
|
514
|
+
value: {
|
|
515
|
+
current,
|
|
516
|
+
min: valueMin !== null ? parseFloat(valueMin) : void 0,
|
|
517
|
+
max: valueMax !== null ? parseFloat(valueMax) : void 0,
|
|
518
|
+
text: valueText || void 0
|
|
519
|
+
},
|
|
520
|
+
warnings
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return { value: void 0, warnings };
|
|
525
|
+
}
|
|
526
|
+
function extractBooleanAttribute(element, attrName, warnings) {
|
|
527
|
+
const value = element.getAttribute(attrName);
|
|
528
|
+
if (value === null) {
|
|
529
|
+
return void 0;
|
|
530
|
+
}
|
|
531
|
+
const normalized = value.toLowerCase().trim();
|
|
532
|
+
if (normalized === "true") {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
if (normalized === "false") {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
warnings.push({
|
|
539
|
+
message: `Invalid ${attrName} value: "${value}". Expected "true" or "false".`,
|
|
540
|
+
element
|
|
541
|
+
});
|
|
542
|
+
return void 0;
|
|
543
|
+
}
|
|
544
|
+
function extractTriStateAttribute(element, attrName, warnings) {
|
|
545
|
+
const value = element.getAttribute(attrName);
|
|
546
|
+
if (value === null) {
|
|
547
|
+
return void 0;
|
|
548
|
+
}
|
|
549
|
+
const normalized = value.toLowerCase().trim();
|
|
550
|
+
if (normalized === "true") {
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
if (normalized === "false") {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
if (normalized === "mixed") {
|
|
557
|
+
return "mixed";
|
|
558
|
+
}
|
|
559
|
+
warnings.push({
|
|
560
|
+
message: `Invalid ${attrName} value: "${value}". Expected "true", "false", or "mixed".`,
|
|
561
|
+
element
|
|
562
|
+
});
|
|
563
|
+
return void 0;
|
|
564
|
+
}
|
|
565
|
+
function extractCurrentAttribute(element, warnings) {
|
|
566
|
+
const value = element.getAttribute("aria-current");
|
|
567
|
+
if (value === null) {
|
|
568
|
+
return void 0;
|
|
569
|
+
}
|
|
570
|
+
const normalized = value.toLowerCase().trim();
|
|
571
|
+
const validValues = ["page", "step", "location", "date", "time", "true", "false"];
|
|
572
|
+
if (validValues.includes(normalized)) {
|
|
573
|
+
return normalized === "false" ? false : normalized;
|
|
574
|
+
}
|
|
575
|
+
warnings.push({
|
|
576
|
+
message: `Invalid aria-current value: "${value}". Expected one of: ${validValues.join(", ")}.`,
|
|
577
|
+
element
|
|
578
|
+
});
|
|
579
|
+
return void 0;
|
|
580
|
+
}
|
|
581
|
+
function extractLevelAttribute(element, warnings) {
|
|
582
|
+
const ariaLevel = element.getAttribute("aria-level");
|
|
583
|
+
if (ariaLevel !== null) {
|
|
584
|
+
const level = parseInt(ariaLevel, 10);
|
|
585
|
+
if (!isNaN(level) && level >= 1 && level <= 6) {
|
|
586
|
+
return level;
|
|
587
|
+
}
|
|
588
|
+
warnings.push({
|
|
589
|
+
message: `Invalid aria-level value: "${ariaLevel}". Expected integer 1-6.`,
|
|
590
|
+
element
|
|
591
|
+
});
|
|
592
|
+
return void 0;
|
|
593
|
+
}
|
|
594
|
+
const tagName = element.tagName.toLowerCase();
|
|
595
|
+
const match = tagName.match(/^h([1-6])$/);
|
|
596
|
+
if (match) {
|
|
597
|
+
return parseInt(match[1], 10);
|
|
598
|
+
}
|
|
599
|
+
return void 0;
|
|
600
|
+
}
|
|
601
|
+
function extractNumberAttribute(element, attrName, warnings) {
|
|
602
|
+
const value = element.getAttribute(attrName);
|
|
603
|
+
if (value === null) {
|
|
604
|
+
return void 0;
|
|
605
|
+
}
|
|
606
|
+
const num = parseInt(value, 10);
|
|
607
|
+
if (isNaN(num) || num < 1) {
|
|
608
|
+
warnings.push({
|
|
609
|
+
message: `Invalid ${attrName} value: "${value}". Expected positive integer.`,
|
|
610
|
+
element
|
|
611
|
+
});
|
|
612
|
+
return void 0;
|
|
613
|
+
}
|
|
614
|
+
return num;
|
|
615
|
+
}
|
|
616
|
+
function extractNativeStates(element, state, _warnings) {
|
|
617
|
+
const tagName = element.tagName.toLowerCase();
|
|
618
|
+
if (tagName === "input") {
|
|
619
|
+
const input = element;
|
|
620
|
+
const type = input.type.toLowerCase();
|
|
621
|
+
if (type === "checkbox" || type === "radio") {
|
|
622
|
+
if (state.checked === void 0) {
|
|
623
|
+
state.checked = input.checked;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if ("disabled" in element) {
|
|
628
|
+
const htmlElement = element;
|
|
629
|
+
if (state.disabled === void 0 && htmlElement.disabled) {
|
|
630
|
+
state.disabled = true;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if ("required" in element) {
|
|
634
|
+
const htmlElement = element;
|
|
635
|
+
if (state.required === void 0 && htmlElement.required) {
|
|
636
|
+
state.required = true;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if ("readOnly" in element) {
|
|
640
|
+
const htmlElement = element;
|
|
641
|
+
if (state.readonly === void 0 && htmlElement.readOnly) {
|
|
642
|
+
state.readonly = true;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function extractInputValue(input, warnings) {
|
|
647
|
+
const type = input.type.toLowerCase();
|
|
648
|
+
if (type === "checkbox" || type === "radio") {
|
|
649
|
+
return { value: void 0, warnings };
|
|
650
|
+
}
|
|
651
|
+
if (type === "button" || type === "submit" || type === "reset") {
|
|
652
|
+
return { value: void 0, warnings };
|
|
653
|
+
}
|
|
654
|
+
const value = input.value;
|
|
655
|
+
if (value) {
|
|
656
|
+
return {
|
|
657
|
+
value: {
|
|
658
|
+
current: value,
|
|
659
|
+
text: value
|
|
660
|
+
},
|
|
661
|
+
warnings
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
return { value: void 0, warnings };
|
|
665
|
+
}
|
|
666
|
+
function extractTextareaValue(textarea, warnings) {
|
|
667
|
+
const value = textarea.value;
|
|
668
|
+
if (value) {
|
|
669
|
+
return {
|
|
670
|
+
value: {
|
|
671
|
+
current: value,
|
|
672
|
+
text: value
|
|
673
|
+
},
|
|
674
|
+
warnings
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
return { value: void 0, warnings };
|
|
678
|
+
}
|
|
679
|
+
function extractSelectValue(select, warnings) {
|
|
680
|
+
const value = select.value;
|
|
681
|
+
const selectedOption = select.options[select.selectedIndex];
|
|
682
|
+
const text = selectedOption?.textContent || value;
|
|
683
|
+
if (value) {
|
|
684
|
+
return {
|
|
685
|
+
value: {
|
|
686
|
+
current: value,
|
|
687
|
+
text
|
|
688
|
+
},
|
|
689
|
+
warnings
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
return { value: void 0, warnings };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/extractor/focus-extractor.ts
|
|
696
|
+
var NATIVELY_FOCUSABLE_ELEMENTS = [
|
|
697
|
+
"a",
|
|
698
|
+
"button",
|
|
699
|
+
"input",
|
|
700
|
+
"select",
|
|
701
|
+
"textarea",
|
|
702
|
+
"area",
|
|
703
|
+
"iframe",
|
|
704
|
+
"object",
|
|
705
|
+
"embed",
|
|
706
|
+
"audio",
|
|
707
|
+
"video"
|
|
708
|
+
];
|
|
709
|
+
function extractFocusInfo(element) {
|
|
710
|
+
const tabindexAttr = element.getAttribute("tabindex");
|
|
711
|
+
const tabindex = tabindexAttr !== null ? parseInt(tabindexAttr, 10) : void 0;
|
|
712
|
+
const focusable = isFocusable(element, tabindex);
|
|
713
|
+
return {
|
|
714
|
+
focusable,
|
|
715
|
+
...tabindexAttr !== null && !isNaN(tabindex) && { tabindex }
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function isFocusable(element, tabindex) {
|
|
719
|
+
const tagName = element.tagName.toLowerCase();
|
|
720
|
+
if (isDisabled(element)) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
if (tabindex !== void 0 && !isNaN(tabindex)) {
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
if (isNativelyFocusable(element, tagName)) {
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
function isNativelyFocusable(element, tagName) {
|
|
732
|
+
if (tagName === "a" || tagName === "area") {
|
|
733
|
+
return element.hasAttribute("href");
|
|
734
|
+
}
|
|
735
|
+
if (NATIVELY_FOCUSABLE_ELEMENTS.includes(tagName)) {
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
function isDisabled(element) {
|
|
741
|
+
if ("disabled" in element) {
|
|
742
|
+
const htmlElement = element;
|
|
743
|
+
if (htmlElement.disabled) {
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const ariaDisabled = element.getAttribute("aria-disabled");
|
|
748
|
+
if (ariaDisabled === "true") {
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/extractor/tree-builder.ts
|
|
755
|
+
function isHidden(element) {
|
|
756
|
+
if (element.getAttribute("aria-hidden") === "true") return true;
|
|
757
|
+
if (typeof getComputedStyle === "function") {
|
|
758
|
+
try {
|
|
759
|
+
const style = getComputedStyle(element);
|
|
760
|
+
if (style.display === "none") return true;
|
|
761
|
+
if (style.visibility === "hidden") return true;
|
|
762
|
+
} catch {
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
function processChildNode(node, children, warnings) {
|
|
768
|
+
if (node.nodeType === 3) {
|
|
769
|
+
const text = (node.textContent || "").trim();
|
|
770
|
+
if (text.length > 0) {
|
|
771
|
+
children.push({
|
|
772
|
+
role: "staticText",
|
|
773
|
+
name: text,
|
|
774
|
+
state: {},
|
|
775
|
+
focus: { focusable: false },
|
|
776
|
+
children: []
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (node.nodeType === 1) {
|
|
782
|
+
const element = node;
|
|
783
|
+
if (isHidden(element)) return;
|
|
784
|
+
if (!isAccessible(element)) {
|
|
785
|
+
const grandchildren = collectChildrenFromRolelessElement(element, warnings);
|
|
786
|
+
children.push(...grandchildren);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const childNode = buildNodeRecursive(element, warnings);
|
|
790
|
+
if (childNode) children.push(childNode);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function collectChildrenFromRolelessElement(element, warnings) {
|
|
794
|
+
return collectAccessibleChildren(element, warnings);
|
|
795
|
+
}
|
|
796
|
+
function collectAccessibleChildren(element, warnings) {
|
|
797
|
+
const children = [];
|
|
798
|
+
if (element.shadowRoot) {
|
|
799
|
+
for (const child of Array.from(element.shadowRoot.childNodes)) {
|
|
800
|
+
processShadowNode(child, children, warnings);
|
|
801
|
+
}
|
|
802
|
+
return children;
|
|
803
|
+
}
|
|
804
|
+
if (element.localName && element.localName.includes("-") && element.childNodes.length === 0) {
|
|
805
|
+
warnings.push({
|
|
806
|
+
message: `Custom element <${element.localName}> has no accessible children and may have a closed shadow root; its content could not be analyzed.`,
|
|
807
|
+
element
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
for (const child of Array.from(element.childNodes)) {
|
|
811
|
+
processChildNode(child, children, warnings);
|
|
812
|
+
}
|
|
813
|
+
return children;
|
|
814
|
+
}
|
|
815
|
+
function processShadowNode(node, children, warnings) {
|
|
816
|
+
if (node.nodeType === 1) {
|
|
817
|
+
const element = node;
|
|
818
|
+
if (element.localName === "slot") {
|
|
819
|
+
const slot = element;
|
|
820
|
+
let assigned = [];
|
|
821
|
+
if (typeof slot.assignedNodes === "function") {
|
|
822
|
+
try {
|
|
823
|
+
assigned = Array.from(slot.assignedNodes({ flatten: true }));
|
|
824
|
+
} catch {
|
|
825
|
+
assigned = [];
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (assigned.length > 0) {
|
|
829
|
+
for (const assignedNode of assigned) {
|
|
830
|
+
processChildNode(assignedNode, children, warnings);
|
|
831
|
+
}
|
|
832
|
+
} else {
|
|
833
|
+
for (const child of Array.from(slot.childNodes)) {
|
|
834
|
+
processShadowNode(child, children, warnings);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
processChildNode(node, children, warnings);
|
|
841
|
+
}
|
|
842
|
+
function buildAccessibilityTree(rootElement, sourceHash) {
|
|
843
|
+
const warnings = [];
|
|
844
|
+
if (isHidden(rootElement)) {
|
|
845
|
+
const model2 = {
|
|
846
|
+
version: CURRENT_MODEL_VERSION,
|
|
847
|
+
root: {
|
|
848
|
+
role: "generic",
|
|
849
|
+
name: "",
|
|
850
|
+
state: {},
|
|
851
|
+
focus: { focusable: false },
|
|
852
|
+
children: []
|
|
853
|
+
},
|
|
854
|
+
metadata: {
|
|
855
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
856
|
+
...sourceHash && { sourceHash }
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
return { model: model2, warnings };
|
|
860
|
+
}
|
|
861
|
+
const rootNode = buildNodeRecursive(rootElement, warnings);
|
|
862
|
+
const accessibleRoot = rootNode || createGenericContainer(rootElement, warnings);
|
|
863
|
+
const model = {
|
|
864
|
+
version: CURRENT_MODEL_VERSION,
|
|
865
|
+
root: accessibleRoot,
|
|
866
|
+
metadata: {
|
|
867
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
868
|
+
...sourceHash && { sourceHash }
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
return { model, warnings };
|
|
872
|
+
}
|
|
873
|
+
function buildNodeRecursive(element, warnings) {
|
|
874
|
+
if (isHidden(element)) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
if (!isAccessible(element)) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
const roleResult = computeRole(element);
|
|
881
|
+
warnings.push(...roleResult.warnings.map((w) => ({
|
|
882
|
+
message: w.message,
|
|
883
|
+
element: w.element
|
|
884
|
+
})));
|
|
885
|
+
if (!roleResult.role) {
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
const nameResult = computeAccessibleName(element);
|
|
889
|
+
warnings.push(...nameResult.warnings.map((w) => ({
|
|
890
|
+
message: w.message,
|
|
891
|
+
element: w.element
|
|
892
|
+
})));
|
|
893
|
+
const descResult = computeAccessibleDescription(element);
|
|
894
|
+
warnings.push(...descResult.warnings.map((w) => ({
|
|
895
|
+
message: w.message,
|
|
896
|
+
element: w.element
|
|
897
|
+
})));
|
|
898
|
+
const stateResult = extractState(element);
|
|
899
|
+
warnings.push(...stateResult.warnings.map((w) => ({
|
|
900
|
+
message: w.message,
|
|
901
|
+
element: w.element
|
|
902
|
+
})));
|
|
903
|
+
const valueResult = extractValue(element);
|
|
904
|
+
warnings.push(...valueResult.warnings.map((w) => ({
|
|
905
|
+
message: w.message,
|
|
906
|
+
element: w.element
|
|
907
|
+
})));
|
|
908
|
+
const focusInfo = extractFocusInfo(element);
|
|
909
|
+
const children = collectAccessibleChildren(element, warnings);
|
|
910
|
+
const node = {
|
|
911
|
+
role: roleResult.role,
|
|
912
|
+
name: nameResult.name,
|
|
913
|
+
...descResult.description && { description: descResult.description },
|
|
914
|
+
...valueResult.value && { value: valueResult.value },
|
|
915
|
+
state: stateResult.state,
|
|
916
|
+
focus: focusInfo,
|
|
917
|
+
children
|
|
918
|
+
};
|
|
919
|
+
return node;
|
|
920
|
+
}
|
|
921
|
+
function createGenericContainer(rootElement, warnings) {
|
|
922
|
+
const children = collectAccessibleChildren(rootElement, warnings);
|
|
923
|
+
return {
|
|
924
|
+
role: "generic",
|
|
925
|
+
name: "",
|
|
926
|
+
state: {},
|
|
927
|
+
focus: {
|
|
928
|
+
focusable: false
|
|
929
|
+
},
|
|
930
|
+
children
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/cli/colors.ts
|
|
935
|
+
import pc from "picocolors";
|
|
936
|
+
var identity = (s) => s;
|
|
937
|
+
function createColors(enabled) {
|
|
938
|
+
if (!enabled) {
|
|
939
|
+
return {
|
|
940
|
+
error: identity,
|
|
941
|
+
warning: identity,
|
|
942
|
+
info: identity,
|
|
943
|
+
success: identity,
|
|
944
|
+
heading: identity,
|
|
945
|
+
title: identity,
|
|
946
|
+
dim: identity,
|
|
947
|
+
roleName: identity,
|
|
948
|
+
stateName: identity,
|
|
949
|
+
elementName: identity,
|
|
950
|
+
sectionHeader: identity,
|
|
951
|
+
description: identity,
|
|
952
|
+
bold: identity,
|
|
953
|
+
enabled: false
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
error: pc.red,
|
|
958
|
+
warning: pc.yellow,
|
|
959
|
+
info: pc.blue,
|
|
960
|
+
success: pc.green,
|
|
961
|
+
heading: (s) => pc.bold(pc.cyan(s)),
|
|
962
|
+
title: (s) => pc.bold(pc.cyan(s)),
|
|
963
|
+
dim: pc.dim,
|
|
964
|
+
roleName: pc.cyan,
|
|
965
|
+
stateName: pc.yellow,
|
|
966
|
+
elementName: (s) => pc.bold(pc.white(s)),
|
|
967
|
+
sectionHeader: (s) => pc.bold(pc.white(s)),
|
|
968
|
+
description: pc.dim,
|
|
969
|
+
bold: pc.bold,
|
|
970
|
+
enabled: true
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/renderer/nvda-renderer.ts
|
|
975
|
+
function renderNVDA(model, colorize) {
|
|
976
|
+
const c = createColors(colorize ?? false);
|
|
977
|
+
const announcements = [];
|
|
978
|
+
renderNodeNVDA(model.root, announcements, c);
|
|
979
|
+
return announcements.join("\n");
|
|
980
|
+
}
|
|
981
|
+
function renderNodeNVDA(node, announcements, c) {
|
|
982
|
+
const announcement = formatNodeNVDA(node, c);
|
|
983
|
+
if (announcement) {
|
|
984
|
+
announcements.push(announcement);
|
|
985
|
+
}
|
|
986
|
+
for (const child of node.children) {
|
|
987
|
+
if (child.name && child.name === node.name && node.children.length === 1) {
|
|
988
|
+
for (const grandchild of child.children) {
|
|
989
|
+
renderNodeNVDA(grandchild, announcements, c);
|
|
990
|
+
}
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
renderNodeNVDA(child, announcements, c);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
function formatNodeNVDA(node, c) {
|
|
997
|
+
const parts = [];
|
|
998
|
+
if (node.name) {
|
|
999
|
+
parts.push(c.elementName(node.name));
|
|
1000
|
+
}
|
|
1001
|
+
const roleText = formatRoleNVDA(node);
|
|
1002
|
+
if (roleText) {
|
|
1003
|
+
parts.push(c.roleName(roleText));
|
|
1004
|
+
}
|
|
1005
|
+
const stateText = formatStatesNVDA(node);
|
|
1006
|
+
if (stateText) {
|
|
1007
|
+
parts.push(c.stateName(stateText));
|
|
1008
|
+
}
|
|
1009
|
+
if (node.value) {
|
|
1010
|
+
const valueText = node.value.text || String(node.value.current);
|
|
1011
|
+
if (valueText) {
|
|
1012
|
+
parts.push(valueText);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if (node.description) {
|
|
1016
|
+
parts.push(c.description(node.description));
|
|
1017
|
+
}
|
|
1018
|
+
return parts.join(", ");
|
|
1019
|
+
}
|
|
1020
|
+
function formatRoleNVDA(node) {
|
|
1021
|
+
const role = node.role;
|
|
1022
|
+
switch (role) {
|
|
1023
|
+
case "button":
|
|
1024
|
+
return "button";
|
|
1025
|
+
case "link":
|
|
1026
|
+
return "link";
|
|
1027
|
+
case "heading":
|
|
1028
|
+
if (node.state.level) {
|
|
1029
|
+
return `heading level ${node.state.level}`;
|
|
1030
|
+
}
|
|
1031
|
+
return "heading";
|
|
1032
|
+
case "textbox":
|
|
1033
|
+
return "edit";
|
|
1034
|
+
case "checkbox":
|
|
1035
|
+
return "checkbox";
|
|
1036
|
+
case "radio":
|
|
1037
|
+
return "radio button";
|
|
1038
|
+
case "combobox":
|
|
1039
|
+
return "combo box";
|
|
1040
|
+
case "listbox":
|
|
1041
|
+
return "list box";
|
|
1042
|
+
case "option":
|
|
1043
|
+
return "option";
|
|
1044
|
+
case "list":
|
|
1045
|
+
return "list";
|
|
1046
|
+
case "listitem":
|
|
1047
|
+
return "list item";
|
|
1048
|
+
case "navigation":
|
|
1049
|
+
return "navigation landmark";
|
|
1050
|
+
case "main":
|
|
1051
|
+
return "main landmark";
|
|
1052
|
+
case "banner":
|
|
1053
|
+
return "banner landmark";
|
|
1054
|
+
case "contentinfo":
|
|
1055
|
+
return "content information landmark";
|
|
1056
|
+
case "region":
|
|
1057
|
+
return "region landmark";
|
|
1058
|
+
case "complementary":
|
|
1059
|
+
return "complementary landmark";
|
|
1060
|
+
case "form":
|
|
1061
|
+
return "form landmark";
|
|
1062
|
+
case "search":
|
|
1063
|
+
return "search landmark";
|
|
1064
|
+
case "img":
|
|
1065
|
+
return "graphic";
|
|
1066
|
+
case "article":
|
|
1067
|
+
return "article";
|
|
1068
|
+
case "generic":
|
|
1069
|
+
return "";
|
|
1070
|
+
case "staticText":
|
|
1071
|
+
case "paragraph":
|
|
1072
|
+
case "cell":
|
|
1073
|
+
case "term":
|
|
1074
|
+
case "definition":
|
|
1075
|
+
case "caption":
|
|
1076
|
+
return "";
|
|
1077
|
+
case "blockquote":
|
|
1078
|
+
return "block quote";
|
|
1079
|
+
case "code":
|
|
1080
|
+
return "code";
|
|
1081
|
+
case "table":
|
|
1082
|
+
return "table";
|
|
1083
|
+
case "row":
|
|
1084
|
+
return "row";
|
|
1085
|
+
case "columnheader":
|
|
1086
|
+
return "column header";
|
|
1087
|
+
case "rowheader":
|
|
1088
|
+
return "row header";
|
|
1089
|
+
case "figure":
|
|
1090
|
+
return "figure";
|
|
1091
|
+
case "dialog":
|
|
1092
|
+
return "dialog";
|
|
1093
|
+
case "meter":
|
|
1094
|
+
return "meter";
|
|
1095
|
+
case "progressbar":
|
|
1096
|
+
return "progress bar";
|
|
1097
|
+
case "status":
|
|
1098
|
+
return "status";
|
|
1099
|
+
case "group":
|
|
1100
|
+
return node.name ? "grouping" : "";
|
|
1101
|
+
case "document":
|
|
1102
|
+
return "document";
|
|
1103
|
+
case "application":
|
|
1104
|
+
return "embedded object";
|
|
1105
|
+
case "separator":
|
|
1106
|
+
return "separator";
|
|
1107
|
+
default:
|
|
1108
|
+
return role;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function formatStatesNVDA(node) {
|
|
1112
|
+
const states = [];
|
|
1113
|
+
if (node.state.expanded !== void 0) {
|
|
1114
|
+
states.push(node.state.expanded ? "expanded" : "collapsed");
|
|
1115
|
+
}
|
|
1116
|
+
if (node.state.checked !== void 0) {
|
|
1117
|
+
if (node.state.checked === "mixed") {
|
|
1118
|
+
states.push("half checked");
|
|
1119
|
+
} else {
|
|
1120
|
+
states.push(node.state.checked ? "checked" : "not checked");
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (node.state.pressed !== void 0) {
|
|
1124
|
+
if (node.state.pressed === "mixed") {
|
|
1125
|
+
states.push("half pressed");
|
|
1126
|
+
} else {
|
|
1127
|
+
states.push(node.state.pressed ? "pressed" : "not pressed");
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (node.state.selected !== void 0) {
|
|
1131
|
+
states.push(node.state.selected ? "selected" : "not selected");
|
|
1132
|
+
}
|
|
1133
|
+
if (node.state.disabled) {
|
|
1134
|
+
states.push("unavailable");
|
|
1135
|
+
}
|
|
1136
|
+
if (node.state.invalid) {
|
|
1137
|
+
states.push("invalid entry");
|
|
1138
|
+
}
|
|
1139
|
+
if (node.state.required) {
|
|
1140
|
+
states.push("required");
|
|
1141
|
+
}
|
|
1142
|
+
if (node.state.readonly) {
|
|
1143
|
+
states.push("read only");
|
|
1144
|
+
}
|
|
1145
|
+
if (node.state.busy) {
|
|
1146
|
+
states.push("busy");
|
|
1147
|
+
}
|
|
1148
|
+
if (node.state.current) {
|
|
1149
|
+
if (node.state.current === "page") {
|
|
1150
|
+
states.push("current page");
|
|
1151
|
+
} else if (node.state.current === "step") {
|
|
1152
|
+
states.push("current step");
|
|
1153
|
+
} else if (node.state.current === "location") {
|
|
1154
|
+
states.push("current location");
|
|
1155
|
+
} else if (node.state.current === "date") {
|
|
1156
|
+
states.push("current date");
|
|
1157
|
+
} else if (node.state.current === "time") {
|
|
1158
|
+
states.push("current time");
|
|
1159
|
+
} else if (node.state.current === "true") {
|
|
1160
|
+
states.push("current");
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (node.state.grabbed !== void 0) {
|
|
1164
|
+
states.push(node.state.grabbed ? "grabbed" : "not grabbed");
|
|
1165
|
+
}
|
|
1166
|
+
return states.join(", ");
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/renderer/jaws-renderer.ts
|
|
1170
|
+
function renderJAWS(model, colorize) {
|
|
1171
|
+
const c = createColors(colorize ?? false);
|
|
1172
|
+
const announcements = [];
|
|
1173
|
+
renderNodeJAWS(model.root, announcements, c);
|
|
1174
|
+
return announcements.join("\n");
|
|
1175
|
+
}
|
|
1176
|
+
function renderNodeJAWS(node, announcements, c) {
|
|
1177
|
+
const announcement = formatNodeJAWS(node, c);
|
|
1178
|
+
if (announcement) {
|
|
1179
|
+
announcements.push(announcement);
|
|
1180
|
+
}
|
|
1181
|
+
for (const child of node.children) {
|
|
1182
|
+
if (child.name && child.name === node.name && node.children.length === 1) {
|
|
1183
|
+
for (const grandchild of child.children) {
|
|
1184
|
+
renderNodeJAWS(grandchild, announcements, c);
|
|
1185
|
+
}
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
renderNodeJAWS(child, announcements, c);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function formatNodeJAWS(node, c) {
|
|
1192
|
+
const parts = [];
|
|
1193
|
+
if (node.name) {
|
|
1194
|
+
parts.push(c.elementName(node.name));
|
|
1195
|
+
}
|
|
1196
|
+
const roleText = formatRoleJAWS(node);
|
|
1197
|
+
if (roleText) {
|
|
1198
|
+
parts.push(c.roleName(roleText));
|
|
1199
|
+
}
|
|
1200
|
+
const stateText = formatStatesJAWS(node);
|
|
1201
|
+
if (stateText) {
|
|
1202
|
+
parts.push(c.stateName(stateText));
|
|
1203
|
+
}
|
|
1204
|
+
if (node.value) {
|
|
1205
|
+
const valueText = node.value.text || String(node.value.current);
|
|
1206
|
+
if (valueText) {
|
|
1207
|
+
parts.push(valueText);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (node.description) {
|
|
1211
|
+
parts.push(c.description(node.description));
|
|
1212
|
+
}
|
|
1213
|
+
return parts.join(", ");
|
|
1214
|
+
}
|
|
1215
|
+
function formatRoleJAWS(node) {
|
|
1216
|
+
const role = node.role;
|
|
1217
|
+
switch (role) {
|
|
1218
|
+
case "button":
|
|
1219
|
+
return "button";
|
|
1220
|
+
case "link":
|
|
1221
|
+
return "clickable";
|
|
1222
|
+
case "heading":
|
|
1223
|
+
if (node.state.level) {
|
|
1224
|
+
return `heading ${node.state.level}`;
|
|
1225
|
+
}
|
|
1226
|
+
return "heading";
|
|
1227
|
+
case "textbox":
|
|
1228
|
+
return "edit";
|
|
1229
|
+
case "checkbox":
|
|
1230
|
+
return "check box";
|
|
1231
|
+
case "radio":
|
|
1232
|
+
return "radio button";
|
|
1233
|
+
case "combobox":
|
|
1234
|
+
return "combo box";
|
|
1235
|
+
case "listbox":
|
|
1236
|
+
return "list box";
|
|
1237
|
+
case "option":
|
|
1238
|
+
return "option";
|
|
1239
|
+
case "list":
|
|
1240
|
+
return "list";
|
|
1241
|
+
case "listitem":
|
|
1242
|
+
return "list item";
|
|
1243
|
+
case "navigation":
|
|
1244
|
+
return "navigation region";
|
|
1245
|
+
case "main":
|
|
1246
|
+
return "main region";
|
|
1247
|
+
case "banner":
|
|
1248
|
+
return "banner region";
|
|
1249
|
+
case "contentinfo":
|
|
1250
|
+
return "content information region";
|
|
1251
|
+
case "region":
|
|
1252
|
+
return "region";
|
|
1253
|
+
case "img":
|
|
1254
|
+
return "graphic";
|
|
1255
|
+
case "article":
|
|
1256
|
+
return "article";
|
|
1257
|
+
case "complementary":
|
|
1258
|
+
return "complementary region";
|
|
1259
|
+
case "form":
|
|
1260
|
+
return "form";
|
|
1261
|
+
case "search":
|
|
1262
|
+
return "search region";
|
|
1263
|
+
case "generic":
|
|
1264
|
+
return "";
|
|
1265
|
+
case "staticText":
|
|
1266
|
+
case "paragraph":
|
|
1267
|
+
case "term":
|
|
1268
|
+
case "definition":
|
|
1269
|
+
case "caption":
|
|
1270
|
+
return "";
|
|
1271
|
+
case "blockquote":
|
|
1272
|
+
return "block quote";
|
|
1273
|
+
case "code":
|
|
1274
|
+
return "code";
|
|
1275
|
+
case "table": {
|
|
1276
|
+
const rows = node.children.filter((c) => c.role === "row");
|
|
1277
|
+
const rowCount = rows.length;
|
|
1278
|
+
const colCount = rows.length > 0 ? rows[0].children.length : 0;
|
|
1279
|
+
return `table with ${rowCount} rows and ${colCount} columns`;
|
|
1280
|
+
}
|
|
1281
|
+
case "row": {
|
|
1282
|
+
const pos = node.state.posinset ?? 0;
|
|
1283
|
+
return `row ${pos}`;
|
|
1284
|
+
}
|
|
1285
|
+
case "cell": {
|
|
1286
|
+
const pos = node.state.posinset ?? 0;
|
|
1287
|
+
return `column ${pos}`;
|
|
1288
|
+
}
|
|
1289
|
+
case "columnheader":
|
|
1290
|
+
return "column header";
|
|
1291
|
+
case "rowheader":
|
|
1292
|
+
return "row header";
|
|
1293
|
+
case "figure":
|
|
1294
|
+
return "figure";
|
|
1295
|
+
case "dialog":
|
|
1296
|
+
return "dialog";
|
|
1297
|
+
case "meter":
|
|
1298
|
+
return "meter";
|
|
1299
|
+
case "progressbar":
|
|
1300
|
+
return "progress bar";
|
|
1301
|
+
case "status":
|
|
1302
|
+
return "status";
|
|
1303
|
+
case "group":
|
|
1304
|
+
return node.name ? "group" : "";
|
|
1305
|
+
case "document":
|
|
1306
|
+
return "frame";
|
|
1307
|
+
case "application":
|
|
1308
|
+
return "embedded object";
|
|
1309
|
+
case "separator":
|
|
1310
|
+
return "separator";
|
|
1311
|
+
default:
|
|
1312
|
+
return role;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
function formatStatesJAWS(node) {
|
|
1316
|
+
const states = [];
|
|
1317
|
+
if (node.state.expanded !== void 0) {
|
|
1318
|
+
states.push(node.state.expanded ? "expanded" : "collapsed");
|
|
1319
|
+
}
|
|
1320
|
+
if (node.state.checked !== void 0) {
|
|
1321
|
+
if (node.state.checked === "mixed") {
|
|
1322
|
+
states.push("partially checked");
|
|
1323
|
+
} else if (node.state.checked) {
|
|
1324
|
+
states.push("checked");
|
|
1325
|
+
} else {
|
|
1326
|
+
states.push("not checked");
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (node.state.pressed !== void 0) {
|
|
1330
|
+
if (node.state.pressed === "mixed") {
|
|
1331
|
+
states.push("partially pressed");
|
|
1332
|
+
} else if (node.state.pressed) {
|
|
1333
|
+
states.push("pressed");
|
|
1334
|
+
} else {
|
|
1335
|
+
states.push("not pressed");
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (node.state.selected !== void 0) {
|
|
1339
|
+
states.push(node.state.selected ? "selected" : "not selected");
|
|
1340
|
+
}
|
|
1341
|
+
if (node.state.disabled) {
|
|
1342
|
+
states.push("unavailable");
|
|
1343
|
+
}
|
|
1344
|
+
if (node.state.invalid) {
|
|
1345
|
+
states.push("invalid entry");
|
|
1346
|
+
}
|
|
1347
|
+
if (node.state.required) {
|
|
1348
|
+
states.push("required");
|
|
1349
|
+
}
|
|
1350
|
+
if (node.state.readonly) {
|
|
1351
|
+
states.push("read only");
|
|
1352
|
+
}
|
|
1353
|
+
if (node.state.busy) {
|
|
1354
|
+
states.push("busy");
|
|
1355
|
+
}
|
|
1356
|
+
if (node.state.current) {
|
|
1357
|
+
switch (node.state.current) {
|
|
1358
|
+
case "page":
|
|
1359
|
+
states.push("current page");
|
|
1360
|
+
break;
|
|
1361
|
+
case "step":
|
|
1362
|
+
states.push("current step");
|
|
1363
|
+
break;
|
|
1364
|
+
case "location":
|
|
1365
|
+
states.push("current location");
|
|
1366
|
+
break;
|
|
1367
|
+
case "date":
|
|
1368
|
+
states.push("current date");
|
|
1369
|
+
break;
|
|
1370
|
+
case "time":
|
|
1371
|
+
states.push("current time");
|
|
1372
|
+
break;
|
|
1373
|
+
case "true":
|
|
1374
|
+
states.push("current");
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
if (node.state.grabbed !== void 0) {
|
|
1379
|
+
states.push(node.state.grabbed ? "grabbed" : "not grabbed");
|
|
1380
|
+
}
|
|
1381
|
+
return states.join(", ");
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// src/renderer/voiceover-renderer.ts
|
|
1385
|
+
function renderVoiceOver(model, colorize) {
|
|
1386
|
+
const c = createColors(colorize ?? false);
|
|
1387
|
+
const announcements = [];
|
|
1388
|
+
renderNodeVoiceOver(model.root, announcements, c);
|
|
1389
|
+
return announcements.join("\n");
|
|
1390
|
+
}
|
|
1391
|
+
function renderNodeVoiceOver(node, announcements, c) {
|
|
1392
|
+
const announcement = formatNodeVoiceOver(node, c);
|
|
1393
|
+
if (announcement) {
|
|
1394
|
+
announcements.push(announcement);
|
|
1395
|
+
}
|
|
1396
|
+
for (const child of node.children) {
|
|
1397
|
+
if (child.name && child.name === node.name && node.children.length === 1) {
|
|
1398
|
+
for (const grandchild of child.children) {
|
|
1399
|
+
renderNodeVoiceOver(grandchild, announcements, c);
|
|
1400
|
+
}
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
renderNodeVoiceOver(child, announcements, c);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
function formatNodeVoiceOver(node, c) {
|
|
1407
|
+
const parts = [];
|
|
1408
|
+
const roleFirst = shouldAnnounceRoleFirst(node.role);
|
|
1409
|
+
if (roleFirst) {
|
|
1410
|
+
const roleText = formatRoleVoiceOver(node);
|
|
1411
|
+
if (roleText) {
|
|
1412
|
+
parts.push(c.roleName(roleText));
|
|
1413
|
+
}
|
|
1414
|
+
if (node.name) {
|
|
1415
|
+
parts.push(c.elementName(node.name));
|
|
1416
|
+
}
|
|
1417
|
+
} else {
|
|
1418
|
+
if (node.name) {
|
|
1419
|
+
parts.push(c.elementName(node.name));
|
|
1420
|
+
}
|
|
1421
|
+
const roleText = formatRoleVoiceOver(node);
|
|
1422
|
+
if (roleText) {
|
|
1423
|
+
parts.push(c.roleName(roleText));
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const stateText = formatStatesVoiceOver(node);
|
|
1427
|
+
if (stateText) {
|
|
1428
|
+
parts.push(c.stateName(stateText));
|
|
1429
|
+
}
|
|
1430
|
+
if (node.value) {
|
|
1431
|
+
const valueText = node.value.text || String(node.value.current);
|
|
1432
|
+
if (valueText) {
|
|
1433
|
+
parts.push(valueText);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (node.description) {
|
|
1437
|
+
parts.push(c.description(node.description));
|
|
1438
|
+
}
|
|
1439
|
+
return parts.join(", ");
|
|
1440
|
+
}
|
|
1441
|
+
function shouldAnnounceRoleFirst(role) {
|
|
1442
|
+
return role === "heading" || role === "navigation" || role === "main" || role === "banner" || role === "contentinfo" || role === "complementary" || role === "region" || role === "form" || role === "search" || role === "blockquote" || role === "figure" || role === "dialog" || role === "group" || role === "document";
|
|
1443
|
+
}
|
|
1444
|
+
function formatRoleVoiceOver(node) {
|
|
1445
|
+
const role = node.role;
|
|
1446
|
+
switch (role) {
|
|
1447
|
+
case "button":
|
|
1448
|
+
return "button";
|
|
1449
|
+
case "link":
|
|
1450
|
+
return "link";
|
|
1451
|
+
case "heading":
|
|
1452
|
+
if (node.state.level) {
|
|
1453
|
+
return `heading level ${node.state.level}`;
|
|
1454
|
+
}
|
|
1455
|
+
return "heading";
|
|
1456
|
+
case "textbox":
|
|
1457
|
+
return "edit text";
|
|
1458
|
+
case "checkbox":
|
|
1459
|
+
return "checkbox";
|
|
1460
|
+
case "radio":
|
|
1461
|
+
return "radio button";
|
|
1462
|
+
case "combobox":
|
|
1463
|
+
return "combo box";
|
|
1464
|
+
case "listbox":
|
|
1465
|
+
return "list box";
|
|
1466
|
+
case "option":
|
|
1467
|
+
return "option";
|
|
1468
|
+
case "list":
|
|
1469
|
+
return "list";
|
|
1470
|
+
case "listitem":
|
|
1471
|
+
return "item";
|
|
1472
|
+
case "navigation":
|
|
1473
|
+
return "navigation";
|
|
1474
|
+
case "main":
|
|
1475
|
+
return "main";
|
|
1476
|
+
case "banner":
|
|
1477
|
+
return "banner";
|
|
1478
|
+
case "contentinfo":
|
|
1479
|
+
return "content information";
|
|
1480
|
+
case "region":
|
|
1481
|
+
return "region";
|
|
1482
|
+
case "complementary":
|
|
1483
|
+
return "complementary";
|
|
1484
|
+
case "form":
|
|
1485
|
+
return "form";
|
|
1486
|
+
case "search":
|
|
1487
|
+
return "search";
|
|
1488
|
+
case "img":
|
|
1489
|
+
return "image";
|
|
1490
|
+
case "article":
|
|
1491
|
+
return "article";
|
|
1492
|
+
case "generic":
|
|
1493
|
+
return "";
|
|
1494
|
+
case "staticText":
|
|
1495
|
+
case "paragraph":
|
|
1496
|
+
case "cell":
|
|
1497
|
+
case "term":
|
|
1498
|
+
case "definition":
|
|
1499
|
+
case "caption":
|
|
1500
|
+
return "";
|
|
1501
|
+
case "blockquote":
|
|
1502
|
+
return "blockquote";
|
|
1503
|
+
case "code":
|
|
1504
|
+
return "code";
|
|
1505
|
+
case "table": {
|
|
1506
|
+
const rows = node.children.filter((c) => c.role === "row");
|
|
1507
|
+
const rowCount = rows.length;
|
|
1508
|
+
const colCount = rows.length > 0 ? rows[0].children.length : 0;
|
|
1509
|
+
return `table, ${rowCount} rows, ${colCount} columns`;
|
|
1510
|
+
}
|
|
1511
|
+
case "row":
|
|
1512
|
+
return "row";
|
|
1513
|
+
case "columnheader":
|
|
1514
|
+
return "column header";
|
|
1515
|
+
case "rowheader":
|
|
1516
|
+
return "row header";
|
|
1517
|
+
case "figure":
|
|
1518
|
+
return "figure";
|
|
1519
|
+
case "dialog":
|
|
1520
|
+
return "web dialog";
|
|
1521
|
+
case "meter":
|
|
1522
|
+
return "level indicator";
|
|
1523
|
+
case "progressbar":
|
|
1524
|
+
return "progress indicator";
|
|
1525
|
+
case "status":
|
|
1526
|
+
return "status";
|
|
1527
|
+
case "group":
|
|
1528
|
+
return node.name ? "group" : "";
|
|
1529
|
+
case "document":
|
|
1530
|
+
return "frame";
|
|
1531
|
+
case "application":
|
|
1532
|
+
return "embedded object";
|
|
1533
|
+
case "separator":
|
|
1534
|
+
return "separator";
|
|
1535
|
+
default:
|
|
1536
|
+
return role;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
function formatStatesVoiceOver(node) {
|
|
1540
|
+
const states = [];
|
|
1541
|
+
if (node.state.expanded !== void 0) {
|
|
1542
|
+
states.push(node.state.expanded ? "expanded" : "collapsed");
|
|
1543
|
+
}
|
|
1544
|
+
if (node.state.checked !== void 0) {
|
|
1545
|
+
if (node.state.checked === "mixed") {
|
|
1546
|
+
states.push("mixed");
|
|
1547
|
+
} else {
|
|
1548
|
+
states.push(node.state.checked ? "checked" : "unchecked");
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (node.state.pressed !== void 0) {
|
|
1552
|
+
if (node.state.pressed === "mixed") {
|
|
1553
|
+
states.push("mixed");
|
|
1554
|
+
} else {
|
|
1555
|
+
states.push(node.state.pressed ? "pressed" : "not pressed");
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (node.state.selected !== void 0) {
|
|
1559
|
+
states.push(node.state.selected ? "selected" : "unselected");
|
|
1560
|
+
}
|
|
1561
|
+
if (node.state.disabled) {
|
|
1562
|
+
states.push("dimmed");
|
|
1563
|
+
}
|
|
1564
|
+
if (node.state.invalid) {
|
|
1565
|
+
states.push("invalid data");
|
|
1566
|
+
}
|
|
1567
|
+
if (node.state.required) {
|
|
1568
|
+
states.push("required");
|
|
1569
|
+
}
|
|
1570
|
+
if (node.state.readonly) {
|
|
1571
|
+
states.push("read only");
|
|
1572
|
+
}
|
|
1573
|
+
if (node.state.busy) {
|
|
1574
|
+
states.push("busy");
|
|
1575
|
+
}
|
|
1576
|
+
if (node.state.current) {
|
|
1577
|
+
if (node.state.current === "page") {
|
|
1578
|
+
states.push("current page");
|
|
1579
|
+
} else if (node.state.current === "step") {
|
|
1580
|
+
states.push("current step");
|
|
1581
|
+
} else if (node.state.current === "location") {
|
|
1582
|
+
states.push("current location");
|
|
1583
|
+
} else if (node.state.current === "date") {
|
|
1584
|
+
states.push("current date");
|
|
1585
|
+
} else if (node.state.current === "time") {
|
|
1586
|
+
states.push("current time");
|
|
1587
|
+
} else if (node.state.current === "true") {
|
|
1588
|
+
states.push("current");
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
if (node.state.grabbed !== void 0) {
|
|
1592
|
+
states.push(node.state.grabbed ? "grabbed" : "not grabbed");
|
|
1593
|
+
}
|
|
1594
|
+
return states.join(", ");
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// src/renderer/narrator-renderer.ts
|
|
1598
|
+
function renderNarrator(model, colorize) {
|
|
1599
|
+
const c = createColors(colorize ?? false);
|
|
1600
|
+
const announcements = [];
|
|
1601
|
+
renderNodeNarrator(model.root, announcements, c);
|
|
1602
|
+
return announcements.join("\n");
|
|
1603
|
+
}
|
|
1604
|
+
function renderNodeNarrator(node, announcements, c) {
|
|
1605
|
+
const announcement = formatNodeNarrator(node, c);
|
|
1606
|
+
if (announcement) {
|
|
1607
|
+
announcements.push(announcement);
|
|
1608
|
+
}
|
|
1609
|
+
for (const child of node.children) {
|
|
1610
|
+
if (child.name && child.name === node.name && node.children.length === 1) {
|
|
1611
|
+
for (const grandchild of child.children) {
|
|
1612
|
+
renderNodeNarrator(grandchild, announcements, c);
|
|
1613
|
+
}
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
renderNodeNarrator(child, announcements, c);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
function formatNodeNarrator(node, c) {
|
|
1620
|
+
const parts = [];
|
|
1621
|
+
const roleFirst = shouldAnnounceRoleFirst2(node.role);
|
|
1622
|
+
if (roleFirst) {
|
|
1623
|
+
const roleText = formatRoleNarrator(node);
|
|
1624
|
+
if (roleText) {
|
|
1625
|
+
parts.push(c.roleName(roleText));
|
|
1626
|
+
}
|
|
1627
|
+
if (node.name) {
|
|
1628
|
+
parts.push(c.elementName(node.name));
|
|
1629
|
+
}
|
|
1630
|
+
} else {
|
|
1631
|
+
if (node.name) {
|
|
1632
|
+
parts.push(c.elementName(node.name));
|
|
1633
|
+
}
|
|
1634
|
+
const roleText = formatRoleNarrator(node);
|
|
1635
|
+
if (roleText) {
|
|
1636
|
+
parts.push(c.roleName(roleText));
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
const stateText = formatStatesNarrator(node);
|
|
1640
|
+
if (stateText) {
|
|
1641
|
+
parts.push(c.stateName(stateText));
|
|
1642
|
+
}
|
|
1643
|
+
if (node.value) {
|
|
1644
|
+
const valueText = node.value.text || String(node.value.current);
|
|
1645
|
+
if (valueText) {
|
|
1646
|
+
parts.push(valueText);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (node.description) {
|
|
1650
|
+
parts.push(c.description(node.description));
|
|
1651
|
+
}
|
|
1652
|
+
return parts.join(", ");
|
|
1653
|
+
}
|
|
1654
|
+
function shouldAnnounceRoleFirst2(role) {
|
|
1655
|
+
const roleFirstRoles = [
|
|
1656
|
+
"link",
|
|
1657
|
+
"heading",
|
|
1658
|
+
"navigation",
|
|
1659
|
+
"main",
|
|
1660
|
+
"banner",
|
|
1661
|
+
"contentinfo",
|
|
1662
|
+
"complementary",
|
|
1663
|
+
"region",
|
|
1664
|
+
"form",
|
|
1665
|
+
"search",
|
|
1666
|
+
"blockquote",
|
|
1667
|
+
"figure",
|
|
1668
|
+
"dialog",
|
|
1669
|
+
"group",
|
|
1670
|
+
"document"
|
|
1671
|
+
];
|
|
1672
|
+
return roleFirstRoles.includes(role);
|
|
1673
|
+
}
|
|
1674
|
+
function formatRoleNarrator(node) {
|
|
1675
|
+
const role = node.role;
|
|
1676
|
+
switch (role) {
|
|
1677
|
+
case "button":
|
|
1678
|
+
return "button";
|
|
1679
|
+
case "link":
|
|
1680
|
+
return "link";
|
|
1681
|
+
case "heading":
|
|
1682
|
+
if (node.state.level) {
|
|
1683
|
+
return `Heading level ${node.state.level}`;
|
|
1684
|
+
}
|
|
1685
|
+
return "Heading";
|
|
1686
|
+
case "textbox":
|
|
1687
|
+
return "edit";
|
|
1688
|
+
case "checkbox":
|
|
1689
|
+
return "check box";
|
|
1690
|
+
case "radio":
|
|
1691
|
+
return "radio button";
|
|
1692
|
+
case "combobox":
|
|
1693
|
+
return "combo box";
|
|
1694
|
+
case "listbox":
|
|
1695
|
+
return "list box";
|
|
1696
|
+
case "option":
|
|
1697
|
+
return "option";
|
|
1698
|
+
case "list":
|
|
1699
|
+
return "list";
|
|
1700
|
+
case "listitem":
|
|
1701
|
+
return "list item";
|
|
1702
|
+
case "navigation":
|
|
1703
|
+
return "navigation";
|
|
1704
|
+
case "main":
|
|
1705
|
+
return "main";
|
|
1706
|
+
case "banner":
|
|
1707
|
+
return "banner";
|
|
1708
|
+
case "contentinfo":
|
|
1709
|
+
return "content info";
|
|
1710
|
+
case "complementary":
|
|
1711
|
+
return "complementary";
|
|
1712
|
+
case "form":
|
|
1713
|
+
return "form";
|
|
1714
|
+
case "search":
|
|
1715
|
+
return "search";
|
|
1716
|
+
case "region":
|
|
1717
|
+
return "region";
|
|
1718
|
+
case "img":
|
|
1719
|
+
return "image";
|
|
1720
|
+
case "article":
|
|
1721
|
+
return "article";
|
|
1722
|
+
case "generic":
|
|
1723
|
+
return "";
|
|
1724
|
+
case "staticText":
|
|
1725
|
+
case "paragraph":
|
|
1726
|
+
case "cell":
|
|
1727
|
+
case "term":
|
|
1728
|
+
case "definition":
|
|
1729
|
+
case "caption":
|
|
1730
|
+
return "";
|
|
1731
|
+
case "blockquote":
|
|
1732
|
+
return "block quote";
|
|
1733
|
+
case "code":
|
|
1734
|
+
return "code";
|
|
1735
|
+
case "table": {
|
|
1736
|
+
const rows = node.children.filter((c) => c.role === "row");
|
|
1737
|
+
const rowCount = rows.length;
|
|
1738
|
+
const colCount = rows.length > 0 ? rows[0].children.length : 0;
|
|
1739
|
+
return `table, ${rowCount} rows, ${colCount} columns`;
|
|
1740
|
+
}
|
|
1741
|
+
case "row":
|
|
1742
|
+
return "row";
|
|
1743
|
+
case "columnheader":
|
|
1744
|
+
return "column header";
|
|
1745
|
+
case "rowheader":
|
|
1746
|
+
return "row header";
|
|
1747
|
+
case "figure":
|
|
1748
|
+
return "figure";
|
|
1749
|
+
case "dialog":
|
|
1750
|
+
return "dialog";
|
|
1751
|
+
case "meter":
|
|
1752
|
+
return "meter";
|
|
1753
|
+
case "progressbar":
|
|
1754
|
+
return "progress bar";
|
|
1755
|
+
case "status":
|
|
1756
|
+
return "status";
|
|
1757
|
+
case "group":
|
|
1758
|
+
return node.name ? "group" : "";
|
|
1759
|
+
case "document":
|
|
1760
|
+
return "document";
|
|
1761
|
+
case "application":
|
|
1762
|
+
return "application";
|
|
1763
|
+
case "separator":
|
|
1764
|
+
return "separator";
|
|
1765
|
+
default:
|
|
1766
|
+
return role;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
function formatStatesNarrator(node) {
|
|
1770
|
+
const states = [];
|
|
1771
|
+
if (node.state.expanded !== void 0) {
|
|
1772
|
+
states.push(node.state.expanded ? "expanded" : "collapsed");
|
|
1773
|
+
}
|
|
1774
|
+
if (node.state.checked !== void 0) {
|
|
1775
|
+
if (node.state.checked === "mixed") {
|
|
1776
|
+
states.push("partially selected");
|
|
1777
|
+
} else {
|
|
1778
|
+
states.push(node.state.checked ? "checked" : "unchecked");
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
if (node.state.pressed !== void 0) {
|
|
1782
|
+
if (node.state.pressed === "mixed") {
|
|
1783
|
+
states.push("partially pressed");
|
|
1784
|
+
} else {
|
|
1785
|
+
states.push(node.state.pressed ? "pressed" : "not pressed");
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
if (node.state.selected !== void 0) {
|
|
1789
|
+
states.push(node.state.selected ? "selected" : "not selected");
|
|
1790
|
+
}
|
|
1791
|
+
if (node.state.disabled) {
|
|
1792
|
+
states.push("disabled");
|
|
1793
|
+
}
|
|
1794
|
+
if (node.state.invalid) {
|
|
1795
|
+
states.push("invalid");
|
|
1796
|
+
}
|
|
1797
|
+
if (node.state.required) {
|
|
1798
|
+
states.push("required");
|
|
1799
|
+
}
|
|
1800
|
+
if (node.state.readonly) {
|
|
1801
|
+
states.push("read only");
|
|
1802
|
+
}
|
|
1803
|
+
if (node.state.busy) {
|
|
1804
|
+
states.push("busy");
|
|
1805
|
+
}
|
|
1806
|
+
if (node.state.current) {
|
|
1807
|
+
if (node.state.current === "page") {
|
|
1808
|
+
states.push("current page");
|
|
1809
|
+
} else if (node.state.current === "step") {
|
|
1810
|
+
states.push("current step");
|
|
1811
|
+
} else if (node.state.current === "location") {
|
|
1812
|
+
states.push("current location");
|
|
1813
|
+
} else if (node.state.current === "date") {
|
|
1814
|
+
states.push("current date");
|
|
1815
|
+
} else if (node.state.current === "time") {
|
|
1816
|
+
states.push("current time");
|
|
1817
|
+
} else if (node.state.current === "true") {
|
|
1818
|
+
states.push("current");
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
if (node.state.grabbed !== void 0) {
|
|
1822
|
+
states.push(node.state.grabbed ? "grabbed" : "not grabbed");
|
|
1823
|
+
}
|
|
1824
|
+
return states.join(", ");
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// src/renderer/audit-renderer.ts
|
|
1828
|
+
function generateAuditReport(model) {
|
|
1829
|
+
const statistics = {
|
|
1830
|
+
totalElements: 0,
|
|
1831
|
+
roleDistribution: {},
|
|
1832
|
+
landmarkCount: 0,
|
|
1833
|
+
headingCount: 0,
|
|
1834
|
+
interactiveCount: 0,
|
|
1835
|
+
focusableCount: 0,
|
|
1836
|
+
statesUsed: /* @__PURE__ */ new Set()
|
|
1837
|
+
};
|
|
1838
|
+
const landmarks = [];
|
|
1839
|
+
const headings = [];
|
|
1840
|
+
const interactiveElements = [];
|
|
1841
|
+
const issues = [];
|
|
1842
|
+
collectAuditData(model.root, statistics, landmarks, headings, interactiveElements);
|
|
1843
|
+
detectLandmarkIssues(landmarks, issues);
|
|
1844
|
+
detectHeadingIssues(headings, issues);
|
|
1845
|
+
detectInteractiveIssues(interactiveElements, issues);
|
|
1846
|
+
return {
|
|
1847
|
+
statistics,
|
|
1848
|
+
landmarks,
|
|
1849
|
+
headings,
|
|
1850
|
+
interactiveElements,
|
|
1851
|
+
issues
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
function collectAuditData(node, statistics, landmarks, headings, interactiveElements) {
|
|
1855
|
+
if (node.role !== "generic") {
|
|
1856
|
+
statistics.totalElements++;
|
|
1857
|
+
statistics.roleDistribution[node.role] = (statistics.roleDistribution[node.role] || 0) + 1;
|
|
1858
|
+
if (isLandmark(node.role)) {
|
|
1859
|
+
statistics.landmarkCount++;
|
|
1860
|
+
landmarks.push(node);
|
|
1861
|
+
}
|
|
1862
|
+
if (node.role === "heading") {
|
|
1863
|
+
statistics.headingCount++;
|
|
1864
|
+
headings.push(node);
|
|
1865
|
+
}
|
|
1866
|
+
if (isInteractive(node.role)) {
|
|
1867
|
+
statistics.interactiveCount++;
|
|
1868
|
+
interactiveElements.push(node);
|
|
1869
|
+
}
|
|
1870
|
+
if (node.focus.focusable) {
|
|
1871
|
+
statistics.focusableCount++;
|
|
1872
|
+
}
|
|
1873
|
+
Object.keys(node.state).forEach((state) => {
|
|
1874
|
+
if (node.state[state] !== void 0) {
|
|
1875
|
+
statistics.statesUsed.add(state);
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
for (const child of node.children) {
|
|
1880
|
+
collectAuditData(child, statistics, landmarks, headings, interactiveElements);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
function isLandmark(role) {
|
|
1884
|
+
return [
|
|
1885
|
+
"navigation",
|
|
1886
|
+
"main",
|
|
1887
|
+
"banner",
|
|
1888
|
+
"contentinfo",
|
|
1889
|
+
"region",
|
|
1890
|
+
"complementary",
|
|
1891
|
+
"form",
|
|
1892
|
+
"search"
|
|
1893
|
+
].includes(role);
|
|
1894
|
+
}
|
|
1895
|
+
function isInteractive(role) {
|
|
1896
|
+
return [
|
|
1897
|
+
"button",
|
|
1898
|
+
"link",
|
|
1899
|
+
"textbox",
|
|
1900
|
+
"checkbox",
|
|
1901
|
+
"radio",
|
|
1902
|
+
"combobox",
|
|
1903
|
+
"listbox",
|
|
1904
|
+
"option"
|
|
1905
|
+
].includes(role);
|
|
1906
|
+
}
|
|
1907
|
+
function detectLandmarkIssues(landmarks, issues) {
|
|
1908
|
+
if (landmarks.length === 0) {
|
|
1909
|
+
issues.push({
|
|
1910
|
+
severity: "info",
|
|
1911
|
+
message: "No landmarks found",
|
|
1912
|
+
suggestion: "Consider adding semantic landmarks (main, nav, aside, etc.)"
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
landmarks.forEach((landmark) => {
|
|
1916
|
+
if (!landmark.name && ["navigation", "region", "complementary", "form"].includes(landmark.role)) {
|
|
1917
|
+
issues.push({
|
|
1918
|
+
severity: "warning",
|
|
1919
|
+
message: `${landmark.role} landmark has no accessible name`,
|
|
1920
|
+
suggestion: `Add aria-label="${landmark.role === "navigation" ? "Main navigation" : "Descriptive name"}" to the ${landmark.role} element`,
|
|
1921
|
+
element: {
|
|
1922
|
+
role: landmark.role,
|
|
1923
|
+
name: landmark.name
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
const landmarksByRole = /* @__PURE__ */ new Map();
|
|
1929
|
+
landmarks.forEach((landmark) => {
|
|
1930
|
+
if (!landmarksByRole.has(landmark.role)) {
|
|
1931
|
+
landmarksByRole.set(landmark.role, []);
|
|
1932
|
+
}
|
|
1933
|
+
landmarksByRole.get(landmark.role).push(landmark);
|
|
1934
|
+
});
|
|
1935
|
+
landmarksByRole.forEach((nodes, role) => {
|
|
1936
|
+
if (nodes.length > 1) {
|
|
1937
|
+
const allUnnamed = nodes.every((n) => !n.name);
|
|
1938
|
+
const allSameName = nodes.every((n) => n.name === nodes[0].name);
|
|
1939
|
+
if (allUnnamed || allSameName) {
|
|
1940
|
+
issues.push({
|
|
1941
|
+
severity: "warning",
|
|
1942
|
+
message: `Multiple ${role} landmarks without distinguishing names`,
|
|
1943
|
+
suggestion: `Add unique aria-label attributes to distinguish between ${role} landmarks`
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
function detectHeadingIssues(headings, issues) {
|
|
1950
|
+
if (headings.length === 0) {
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
const firstHeading = headings[0];
|
|
1954
|
+
if (firstHeading.state.level && firstHeading.state.level !== 1) {
|
|
1955
|
+
issues.push({
|
|
1956
|
+
severity: "error",
|
|
1957
|
+
message: `First heading is h${firstHeading.state.level} (should be h1)`,
|
|
1958
|
+
suggestion: `Change <h${firstHeading.state.level}> to <h1> or add h1 before it`,
|
|
1959
|
+
element: {
|
|
1960
|
+
role: firstHeading.role,
|
|
1961
|
+
name: firstHeading.name
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
for (let i = 1; i < headings.length; i++) {
|
|
1966
|
+
const prevLevel = headings[i - 1].state.level || 1;
|
|
1967
|
+
const currLevel = headings[i].state.level || 1;
|
|
1968
|
+
if (currLevel > prevLevel + 1) {
|
|
1969
|
+
issues.push({
|
|
1970
|
+
severity: "error",
|
|
1971
|
+
message: `Heading hierarchy violation: h${prevLevel} followed by h${currLevel} (skipped h${prevLevel + 1})`,
|
|
1972
|
+
suggestion: `Change h${currLevel} to h${prevLevel + 1} or add intermediate heading levels`,
|
|
1973
|
+
element: {
|
|
1974
|
+
role: headings[i].role,
|
|
1975
|
+
name: headings[i].name
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
function detectInteractiveIssues(elements, issues) {
|
|
1982
|
+
elements.forEach((element) => {
|
|
1983
|
+
if (!element.name) {
|
|
1984
|
+
issues.push({
|
|
1985
|
+
severity: "error",
|
|
1986
|
+
message: `${element.role} has no accessible name`,
|
|
1987
|
+
suggestion: element.role === "button" ? "Add text content or aria-label to button" : element.role === "link" ? "Add text content or aria-label to link" : `Add aria-label to ${element.role}`,
|
|
1988
|
+
element: {
|
|
1989
|
+
role: element.role,
|
|
1990
|
+
name: element.name
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
if (element.role === "textbox" && !element.name) {
|
|
1995
|
+
issues.push({
|
|
1996
|
+
severity: "error",
|
|
1997
|
+
message: "Input has no associated label",
|
|
1998
|
+
suggestion: "Add <label> element or aria-label attribute",
|
|
1999
|
+
element: {
|
|
2000
|
+
role: element.role,
|
|
2001
|
+
name: element.name
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// src/browser/analyze.ts
|
|
2009
|
+
var LANDMARK_ROLES = /* @__PURE__ */ new Set([
|
|
2010
|
+
"navigation",
|
|
2011
|
+
"main",
|
|
2012
|
+
"banner",
|
|
2013
|
+
"contentinfo",
|
|
2014
|
+
"complementary",
|
|
2015
|
+
"region",
|
|
2016
|
+
"form",
|
|
2017
|
+
"search"
|
|
2018
|
+
]);
|
|
2019
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
2020
|
+
"button",
|
|
2021
|
+
"link",
|
|
2022
|
+
"textbox",
|
|
2023
|
+
"checkbox",
|
|
2024
|
+
"radio",
|
|
2025
|
+
"combobox",
|
|
2026
|
+
"listbox",
|
|
2027
|
+
"option"
|
|
2028
|
+
]);
|
|
2029
|
+
function countStats(node) {
|
|
2030
|
+
const stats = { totalElements: 0, interactiveElements: 0, landmarks: 0, headings: 0 };
|
|
2031
|
+
function walk(n) {
|
|
2032
|
+
if (n.role !== "generic" && n.role !== "staticText") {
|
|
2033
|
+
stats.totalElements++;
|
|
2034
|
+
}
|
|
2035
|
+
if (INTERACTIVE_ROLES.has(n.role)) stats.interactiveElements++;
|
|
2036
|
+
if (LANDMARK_ROLES.has(n.role)) stats.landmarks++;
|
|
2037
|
+
if (n.role === "heading") stats.headings++;
|
|
2038
|
+
for (const child of n.children) walk(child);
|
|
2039
|
+
}
|
|
2040
|
+
walk(node);
|
|
2041
|
+
return stats;
|
|
2042
|
+
}
|
|
2043
|
+
function convertAuditFindings(model) {
|
|
2044
|
+
const report = generateAuditReport(model);
|
|
2045
|
+
return report.issues.map((issue) => ({
|
|
2046
|
+
severity: issue.severity,
|
|
2047
|
+
message: issue.message,
|
|
2048
|
+
selector: issue.element ? `${issue.element.role}${issue.element.name ? ` "${issue.element.name}"` : ""}` : issue.suggestion || ""
|
|
2049
|
+
}));
|
|
2050
|
+
}
|
|
2051
|
+
function emptyResult(extraWarnings = []) {
|
|
2052
|
+
return {
|
|
2053
|
+
nvda: [],
|
|
2054
|
+
jaws: [],
|
|
2055
|
+
voiceover: [],
|
|
2056
|
+
narrator: [],
|
|
2057
|
+
audit: [],
|
|
2058
|
+
stats: { totalElements: 0, interactiveElements: 0, landmarks: 0, headings: 0 },
|
|
2059
|
+
warnings: [...extraWarnings]
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
function analyzeElement(root, extraWarnings = []) {
|
|
2063
|
+
if (!root) {
|
|
2064
|
+
return emptyResult(extraWarnings);
|
|
2065
|
+
}
|
|
2066
|
+
let built;
|
|
2067
|
+
try {
|
|
2068
|
+
built = buildAccessibilityTree(root);
|
|
2069
|
+
} catch (err) {
|
|
2070
|
+
return emptyResult([
|
|
2071
|
+
...extraWarnings,
|
|
2072
|
+
`Analysis failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2073
|
+
]);
|
|
2074
|
+
}
|
|
2075
|
+
const { model, warnings: treeWarnings } = built;
|
|
2076
|
+
const nvda = renderNVDA(model, false).split("\n").filter(Boolean);
|
|
2077
|
+
const jaws = renderJAWS(model, false).split("\n").filter(Boolean);
|
|
2078
|
+
const voiceover = renderVoiceOver(model, false).split("\n").filter(Boolean);
|
|
2079
|
+
const narrator = renderNarrator(model, false).split("\n").filter(Boolean);
|
|
2080
|
+
const audit = convertAuditFindings(model);
|
|
2081
|
+
const stats = countStats(model.root);
|
|
2082
|
+
const warnings = [
|
|
2083
|
+
...extraWarnings,
|
|
2084
|
+
...treeWarnings.map((w) => w.message)
|
|
2085
|
+
];
|
|
2086
|
+
return { nvda, jaws, voiceover, narrator, audit, stats, warnings };
|
|
2087
|
+
}
|
|
2088
|
+
async function analyzeElementWithUpgrade(root, upgradeTimeoutMs) {
|
|
2089
|
+
const { awaitCustomElementsReady: awaitCustomElementsReady2 } = await import("../upgrade-53M5VYGH.js");
|
|
2090
|
+
const target = root ?? void 0;
|
|
2091
|
+
const scope = target && target.ownerDocument ? target.ownerDocument : target;
|
|
2092
|
+
const upgradeWarnings = scope ? await awaitCustomElementsReady2(scope, upgradeTimeoutMs) : [];
|
|
2093
|
+
return analyzeElement(root, upgradeWarnings.map((w) => w.message));
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/runtime/selector.ts
|
|
2097
|
+
function generateSelector(element) {
|
|
2098
|
+
if (element.id) {
|
|
2099
|
+
return `#${escapeCSSIdentifier(element.id)}`;
|
|
2100
|
+
}
|
|
2101
|
+
const document = element.ownerDocument;
|
|
2102
|
+
if (!document) {
|
|
2103
|
+
return getTagName(element);
|
|
2104
|
+
}
|
|
2105
|
+
const segments = [];
|
|
2106
|
+
let current = element;
|
|
2107
|
+
while (current && current !== document.documentElement) {
|
|
2108
|
+
const segment = buildSegment(current, document);
|
|
2109
|
+
segments.unshift(segment);
|
|
2110
|
+
const partialSelector = segments.join(" > ");
|
|
2111
|
+
try {
|
|
2112
|
+
const matches = document.querySelectorAll(partialSelector);
|
|
2113
|
+
if (matches.length === 1 && matches[0] === element) {
|
|
2114
|
+
return partialSelector;
|
|
2115
|
+
}
|
|
2116
|
+
} catch {
|
|
2117
|
+
}
|
|
2118
|
+
current = current.parentElement;
|
|
2119
|
+
}
|
|
2120
|
+
if (segments.length === 0) {
|
|
2121
|
+
return getTagName(element);
|
|
2122
|
+
}
|
|
2123
|
+
const fullSelector = segments.join(" > ");
|
|
2124
|
+
return fullSelector;
|
|
2125
|
+
}
|
|
2126
|
+
function buildSegment(element, _document) {
|
|
2127
|
+
const tag = getTagName(element);
|
|
2128
|
+
const parent = element.parentElement;
|
|
2129
|
+
if (parent) {
|
|
2130
|
+
const siblingsWithSameTag = Array.from(parent.children).filter(
|
|
2131
|
+
(child) => getTagName(child) === tag
|
|
2132
|
+
);
|
|
2133
|
+
if (siblingsWithSameTag.length === 1) {
|
|
2134
|
+
return tag;
|
|
2135
|
+
}
|
|
2136
|
+
} else {
|
|
2137
|
+
return tag;
|
|
2138
|
+
}
|
|
2139
|
+
const classes = getStableClasses(element);
|
|
2140
|
+
if (classes.length > 0) {
|
|
2141
|
+
const tagWithClasses = `${tag}.${classes.join(".")}`;
|
|
2142
|
+
if (parent) {
|
|
2143
|
+
const siblingsMatchingClasses = Array.from(parent.children).filter(
|
|
2144
|
+
(child) => {
|
|
2145
|
+
if (getTagName(child) !== tag) return false;
|
|
2146
|
+
return classes.every((cls) => child.classList.contains(cls));
|
|
2147
|
+
}
|
|
2148
|
+
);
|
|
2149
|
+
if (siblingsMatchingClasses.length === 1) {
|
|
2150
|
+
return tagWithClasses;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
const index = getNthChildIndex(element);
|
|
2155
|
+
return `${tag}:nth-child(${index})`;
|
|
2156
|
+
}
|
|
2157
|
+
function getTagName(element) {
|
|
2158
|
+
return element.tagName.toLowerCase();
|
|
2159
|
+
}
|
|
2160
|
+
function getStableClasses(element) {
|
|
2161
|
+
const classes = Array.from(element.classList);
|
|
2162
|
+
if (classes.length === 0) return [];
|
|
2163
|
+
const stableClasses = classes.filter((cls) => !looksLikeDynamicClass(cls));
|
|
2164
|
+
return stableClasses.sort().map(escapeCSSIdentifier);
|
|
2165
|
+
}
|
|
2166
|
+
function looksLikeDynamicClass(className) {
|
|
2167
|
+
if (/^[a-f0-9]{5,}$/i.test(className)) return true;
|
|
2168
|
+
if (/[_-][a-f0-9]{5,}$/i.test(className)) return true;
|
|
2169
|
+
if (/_{2,}[A-Za-z0-9]{4,}$/.test(className)) return true;
|
|
2170
|
+
return false;
|
|
2171
|
+
}
|
|
2172
|
+
function getNthChildIndex(element) {
|
|
2173
|
+
const parent = element.parentElement;
|
|
2174
|
+
if (!parent) return 1;
|
|
2175
|
+
const children = Array.from(parent.children);
|
|
2176
|
+
const index = children.indexOf(element);
|
|
2177
|
+
return index + 1;
|
|
2178
|
+
}
|
|
2179
|
+
function escapeCSSIdentifier(value) {
|
|
2180
|
+
if (!value) return value;
|
|
2181
|
+
let result = "";
|
|
2182
|
+
for (let i = 0; i < value.length; i++) {
|
|
2183
|
+
const char = value[i];
|
|
2184
|
+
const code = char.charCodeAt(0);
|
|
2185
|
+
if (i === 0 && code >= 48 && code <= 57) {
|
|
2186
|
+
result += `\\3${char} `;
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
if (char === "!" || char === '"' || char === "#" || char === "$" || char === "%" || char === "&" || char === "'" || char === "(" || char === ")" || char === "*" || char === "+" || char === "," || char === "." || char === "/" || char === ":" || char === ";" || char === "<" || char === "=" || char === ">" || char === "?" || char === "@" || char === "[" || char === "\\" || char === "]" || char === "^" || char === "`" || char === "{" || char === "|" || char === "}" || char === "~" || char === " ") {
|
|
2190
|
+
result += `\\${char}`;
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
result += char;
|
|
2194
|
+
}
|
|
2195
|
+
return result;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
// src/runtime/observers/focus-observer.ts
|
|
2199
|
+
function computeAccessibleName2(element, document) {
|
|
2200
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
2201
|
+
if (ariaLabel && ariaLabel.trim()) {
|
|
2202
|
+
return ariaLabel.trim();
|
|
2203
|
+
}
|
|
2204
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
2205
|
+
if (labelledBy && labelledBy.trim()) {
|
|
2206
|
+
const ids = labelledBy.trim().split(/\s+/);
|
|
2207
|
+
const parts = [];
|
|
2208
|
+
for (const id of ids) {
|
|
2209
|
+
const referenced = document.getElementById(id);
|
|
2210
|
+
if (referenced && referenced.textContent) {
|
|
2211
|
+
parts.push(referenced.textContent.trim());
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
const combined = parts.join(" ");
|
|
2215
|
+
if (combined) {
|
|
2216
|
+
return combined;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
const textContent = element.textContent;
|
|
2220
|
+
if (textContent && textContent.trim()) {
|
|
2221
|
+
return textContent.trim();
|
|
2222
|
+
}
|
|
2223
|
+
return "";
|
|
2224
|
+
}
|
|
2225
|
+
function getElementRole(element) {
|
|
2226
|
+
const explicitRole = element.getAttribute("role");
|
|
2227
|
+
if (explicitRole && explicitRole.trim()) {
|
|
2228
|
+
return explicitRole.trim();
|
|
2229
|
+
}
|
|
2230
|
+
const tagName = element.tagName.toLowerCase();
|
|
2231
|
+
switch (tagName) {
|
|
2232
|
+
case "button":
|
|
2233
|
+
return "button";
|
|
2234
|
+
case "a":
|
|
2235
|
+
return element.hasAttribute("href") ? "link" : "generic";
|
|
2236
|
+
case "input": {
|
|
2237
|
+
const type = (element.getAttribute("type") || "text").toLowerCase();
|
|
2238
|
+
switch (type) {
|
|
2239
|
+
case "button":
|
|
2240
|
+
case "submit":
|
|
2241
|
+
case "reset":
|
|
2242
|
+
case "image":
|
|
2243
|
+
return "button";
|
|
2244
|
+
case "checkbox":
|
|
2245
|
+
return "checkbox";
|
|
2246
|
+
case "radio":
|
|
2247
|
+
return "radio";
|
|
2248
|
+
case "range":
|
|
2249
|
+
return "slider";
|
|
2250
|
+
case "search":
|
|
2251
|
+
return "searchbox";
|
|
2252
|
+
default:
|
|
2253
|
+
return "textbox";
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
case "textarea":
|
|
2257
|
+
return "textbox";
|
|
2258
|
+
case "select":
|
|
2259
|
+
return element.hasAttribute("multiple") ? "listbox" : "combobox";
|
|
2260
|
+
case "nav":
|
|
2261
|
+
return "navigation";
|
|
2262
|
+
case "main":
|
|
2263
|
+
return "main";
|
|
2264
|
+
case "header":
|
|
2265
|
+
return "banner";
|
|
2266
|
+
case "footer":
|
|
2267
|
+
return "contentinfo";
|
|
2268
|
+
case "aside":
|
|
2269
|
+
return "complementary";
|
|
2270
|
+
case "form":
|
|
2271
|
+
return "form";
|
|
2272
|
+
case "section":
|
|
2273
|
+
return element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby") ? "region" : "generic";
|
|
2274
|
+
case "article":
|
|
2275
|
+
return "article";
|
|
2276
|
+
case "dialog":
|
|
2277
|
+
return "dialog";
|
|
2278
|
+
case "table":
|
|
2279
|
+
return "table";
|
|
2280
|
+
case "ul":
|
|
2281
|
+
case "ol":
|
|
2282
|
+
return "list";
|
|
2283
|
+
case "li":
|
|
2284
|
+
return "listitem";
|
|
2285
|
+
case "img":
|
|
2286
|
+
return element.getAttribute("alt") === "" ? "presentation" : "img";
|
|
2287
|
+
case "h1":
|
|
2288
|
+
case "h2":
|
|
2289
|
+
case "h3":
|
|
2290
|
+
case "h4":
|
|
2291
|
+
case "h5":
|
|
2292
|
+
case "h6":
|
|
2293
|
+
return "heading";
|
|
2294
|
+
case "option":
|
|
2295
|
+
return "option";
|
|
2296
|
+
case "progress":
|
|
2297
|
+
return "progressbar";
|
|
2298
|
+
case "output":
|
|
2299
|
+
return "status";
|
|
2300
|
+
case "menu":
|
|
2301
|
+
return "menu";
|
|
2302
|
+
default:
|
|
2303
|
+
return "generic";
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
function createFocusObserver(options) {
|
|
2307
|
+
const { document: doc, onEvent, getTimestamp } = options;
|
|
2308
|
+
let active = false;
|
|
2309
|
+
let previousTarget = null;
|
|
2310
|
+
function buildEventTarget2(element) {
|
|
2311
|
+
return {
|
|
2312
|
+
role: getElementRole(element),
|
|
2313
|
+
accessibleName: computeAccessibleName2(element, doc),
|
|
2314
|
+
selector: generateSelector(element)
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
function handleFocusIn(event) {
|
|
2318
|
+
if (!active) return;
|
|
2319
|
+
const target = event.target;
|
|
2320
|
+
if (!target || !(target instanceof doc.defaultView.Element)) {
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
const currentTarget = buildEventTarget2(target);
|
|
2324
|
+
const a11yEvent = {
|
|
2325
|
+
type: "FOCUS_CHANGED",
|
|
2326
|
+
timestamp: getTimestamp(),
|
|
2327
|
+
target: currentTarget,
|
|
2328
|
+
payload: {
|
|
2329
|
+
kind: "focus_changed",
|
|
2330
|
+
previousTarget
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2333
|
+
onEvent(a11yEvent);
|
|
2334
|
+
previousTarget = currentTarget;
|
|
2335
|
+
}
|
|
2336
|
+
return {
|
|
2337
|
+
start() {
|
|
2338
|
+
if (active) return;
|
|
2339
|
+
active = true;
|
|
2340
|
+
previousTarget = null;
|
|
2341
|
+
doc.addEventListener("focusin", handleFocusIn);
|
|
2342
|
+
},
|
|
2343
|
+
stop() {
|
|
2344
|
+
if (!active) return;
|
|
2345
|
+
active = false;
|
|
2346
|
+
doc.removeEventListener("focusin", handleFocusIn);
|
|
2347
|
+
},
|
|
2348
|
+
get isActive() {
|
|
2349
|
+
return active;
|
|
2350
|
+
}
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
// src/runtime/observers/mutation-observer.ts
|
|
2355
|
+
var ROLE_ATTRIBUTE = "role";
|
|
2356
|
+
var NAME_ATTRIBUTES = /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]);
|
|
2357
|
+
var STATE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
2358
|
+
"aria-expanded",
|
|
2359
|
+
"aria-selected",
|
|
2360
|
+
"aria-checked",
|
|
2361
|
+
"aria-disabled",
|
|
2362
|
+
"aria-hidden"
|
|
2363
|
+
]);
|
|
2364
|
+
var OBSERVED_ATTRIBUTES = [
|
|
2365
|
+
ROLE_ATTRIBUTE,
|
|
2366
|
+
"aria-label",
|
|
2367
|
+
"aria-labelledby",
|
|
2368
|
+
"aria-expanded",
|
|
2369
|
+
"aria-selected",
|
|
2370
|
+
"aria-checked",
|
|
2371
|
+
"aria-disabled",
|
|
2372
|
+
"aria-hidden"
|
|
2373
|
+
];
|
|
2374
|
+
function computeAccessibleName3(element, document) {
|
|
2375
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
2376
|
+
if (ariaLabel && ariaLabel.trim()) {
|
|
2377
|
+
return ariaLabel.trim();
|
|
2378
|
+
}
|
|
2379
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
2380
|
+
if (labelledBy && labelledBy.trim()) {
|
|
2381
|
+
const ids = labelledBy.trim().split(/\s+/);
|
|
2382
|
+
const parts = [];
|
|
2383
|
+
for (const id of ids) {
|
|
2384
|
+
const referenced = document.getElementById(id);
|
|
2385
|
+
if (referenced && referenced.textContent) {
|
|
2386
|
+
parts.push(referenced.textContent.trim());
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
const combined = parts.join(" ");
|
|
2390
|
+
if (combined) {
|
|
2391
|
+
return combined;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
const textContent = element.textContent;
|
|
2395
|
+
if (textContent && textContent.trim()) {
|
|
2396
|
+
return textContent.trim();
|
|
2397
|
+
}
|
|
2398
|
+
return "";
|
|
2399
|
+
}
|
|
2400
|
+
function getElementRole2(element) {
|
|
2401
|
+
const explicitRole = element.getAttribute("role");
|
|
2402
|
+
if (explicitRole && explicitRole.trim()) {
|
|
2403
|
+
return explicitRole.trim();
|
|
2404
|
+
}
|
|
2405
|
+
const tagName = element.tagName.toLowerCase();
|
|
2406
|
+
switch (tagName) {
|
|
2407
|
+
case "button":
|
|
2408
|
+
return "button";
|
|
2409
|
+
case "a":
|
|
2410
|
+
return element.hasAttribute("href") ? "link" : "generic";
|
|
2411
|
+
case "input": {
|
|
2412
|
+
const type = (element.getAttribute("type") || "text").toLowerCase();
|
|
2413
|
+
switch (type) {
|
|
2414
|
+
case "button":
|
|
2415
|
+
case "submit":
|
|
2416
|
+
case "reset":
|
|
2417
|
+
case "image":
|
|
2418
|
+
return "button";
|
|
2419
|
+
case "checkbox":
|
|
2420
|
+
return "checkbox";
|
|
2421
|
+
case "radio":
|
|
2422
|
+
return "radio";
|
|
2423
|
+
case "range":
|
|
2424
|
+
return "slider";
|
|
2425
|
+
case "search":
|
|
2426
|
+
return "searchbox";
|
|
2427
|
+
default:
|
|
2428
|
+
return "textbox";
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
case "textarea":
|
|
2432
|
+
return "textbox";
|
|
2433
|
+
case "select":
|
|
2434
|
+
return element.hasAttribute("multiple") ? "listbox" : "combobox";
|
|
2435
|
+
case "nav":
|
|
2436
|
+
return "navigation";
|
|
2437
|
+
case "main":
|
|
2438
|
+
return "main";
|
|
2439
|
+
case "header":
|
|
2440
|
+
return "banner";
|
|
2441
|
+
case "footer":
|
|
2442
|
+
return "contentinfo";
|
|
2443
|
+
case "aside":
|
|
2444
|
+
return "complementary";
|
|
2445
|
+
case "form":
|
|
2446
|
+
return "form";
|
|
2447
|
+
case "section":
|
|
2448
|
+
return element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby") ? "region" : "generic";
|
|
2449
|
+
case "article":
|
|
2450
|
+
return "article";
|
|
2451
|
+
case "dialog":
|
|
2452
|
+
return "dialog";
|
|
2453
|
+
case "table":
|
|
2454
|
+
return "table";
|
|
2455
|
+
case "ul":
|
|
2456
|
+
case "ol":
|
|
2457
|
+
return "list";
|
|
2458
|
+
case "li":
|
|
2459
|
+
return "listitem";
|
|
2460
|
+
case "img":
|
|
2461
|
+
return element.getAttribute("alt") === "" ? "presentation" : "img";
|
|
2462
|
+
case "h1":
|
|
2463
|
+
case "h2":
|
|
2464
|
+
case "h3":
|
|
2465
|
+
case "h4":
|
|
2466
|
+
case "h5":
|
|
2467
|
+
case "h6":
|
|
2468
|
+
return "heading";
|
|
2469
|
+
case "option":
|
|
2470
|
+
return "option";
|
|
2471
|
+
case "progress":
|
|
2472
|
+
return "progressbar";
|
|
2473
|
+
case "output":
|
|
2474
|
+
return "status";
|
|
2475
|
+
case "menu":
|
|
2476
|
+
return "menu";
|
|
2477
|
+
default:
|
|
2478
|
+
return "generic";
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
function createMutationObserver(options) {
|
|
2482
|
+
const { document: doc, onEvent, getTimestamp } = options;
|
|
2483
|
+
let observer = null;
|
|
2484
|
+
let active = false;
|
|
2485
|
+
function handleMutations(mutations) {
|
|
2486
|
+
if (!active) return;
|
|
2487
|
+
for (const mutation of mutations) {
|
|
2488
|
+
if (mutation.type !== "attributes") {
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
const attributeName = mutation.attributeName;
|
|
2492
|
+
if (!attributeName) {
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
const target = mutation.target;
|
|
2496
|
+
if (!(target instanceof Element)) {
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
if (attributeName === ROLE_ATTRIBUTE) {
|
|
2500
|
+
handleRoleChange(target, mutation.oldValue);
|
|
2501
|
+
} else if (NAME_ATTRIBUTES.has(attributeName)) {
|
|
2502
|
+
handleNameChange(target, attributeName, mutation.oldValue);
|
|
2503
|
+
} else if (STATE_ATTRIBUTES.has(attributeName)) {
|
|
2504
|
+
handleStateChange(target, attributeName, mutation.oldValue);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
function handleRoleChange(element, oldValue) {
|
|
2509
|
+
const previousRole = oldValue?.trim() || getImplicitRole(element);
|
|
2510
|
+
const newRole = element.getAttribute("role")?.trim() || getImplicitRole(element);
|
|
2511
|
+
if (previousRole === newRole) {
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
const eventTarget = buildEventTarget2(element);
|
|
2515
|
+
const payload = {
|
|
2516
|
+
kind: "role_changed",
|
|
2517
|
+
previousRole,
|
|
2518
|
+
newRole
|
|
2519
|
+
};
|
|
2520
|
+
onEvent({
|
|
2521
|
+
type: "ROLE_CHANGED",
|
|
2522
|
+
timestamp: getTimestamp(),
|
|
2523
|
+
target: eventTarget,
|
|
2524
|
+
payload
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
function handleNameChange(element, attributeName, oldValue) {
|
|
2528
|
+
let previousName;
|
|
2529
|
+
let newName;
|
|
2530
|
+
if (attributeName === "aria-label") {
|
|
2531
|
+
previousName = oldValue?.trim() || "";
|
|
2532
|
+
newName = computeAccessibleName3(element, doc);
|
|
2533
|
+
if (!previousName && oldValue === null) {
|
|
2534
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
2535
|
+
if (labelledBy) {
|
|
2536
|
+
previousName = resolveAriaLabelledBy(element, labelledBy);
|
|
2537
|
+
} else {
|
|
2538
|
+
previousName = element.textContent?.trim() || "";
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
} else {
|
|
2542
|
+
previousName = oldValue ? resolveAriaLabelledBy(element, oldValue) : "";
|
|
2543
|
+
if (!previousName && oldValue === null) {
|
|
2544
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
2545
|
+
if (ariaLabel) {
|
|
2546
|
+
previousName = ariaLabel.trim();
|
|
2547
|
+
} else {
|
|
2548
|
+
previousName = element.textContent?.trim() || "";
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
newName = computeAccessibleName3(element, doc);
|
|
2552
|
+
}
|
|
2553
|
+
if (previousName === newName) {
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
const eventTarget = buildEventTarget2(element);
|
|
2557
|
+
const payload = {
|
|
2558
|
+
kind: "accessible_name_changed",
|
|
2559
|
+
previousName,
|
|
2560
|
+
newName
|
|
2561
|
+
};
|
|
2562
|
+
onEvent({
|
|
2563
|
+
type: "ACCESSIBLE_NAME_CHANGED",
|
|
2564
|
+
timestamp: getTimestamp(),
|
|
2565
|
+
target: eventTarget,
|
|
2566
|
+
payload
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
function handleStateChange(element, attributeName, oldValue) {
|
|
2570
|
+
const currentValue = element.getAttribute(attributeName);
|
|
2571
|
+
const normalizedOld = normalizeStateValue(oldValue);
|
|
2572
|
+
const normalizedNew = normalizeStateValue(currentValue);
|
|
2573
|
+
if (normalizedOld === normalizedNew) {
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
const eventTarget = buildEventTarget2(element);
|
|
2577
|
+
const payload = {
|
|
2578
|
+
kind: "state_changed",
|
|
2579
|
+
attribute: attributeName,
|
|
2580
|
+
previousValue: normalizedOld,
|
|
2581
|
+
newValue: normalizedNew
|
|
2582
|
+
};
|
|
2583
|
+
onEvent({
|
|
2584
|
+
type: "STATE_CHANGED",
|
|
2585
|
+
timestamp: getTimestamp(),
|
|
2586
|
+
target: eventTarget,
|
|
2587
|
+
payload
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
function buildEventTarget2(element) {
|
|
2591
|
+
return {
|
|
2592
|
+
role: getElementRole2(element),
|
|
2593
|
+
accessibleName: computeAccessibleName3(element, doc),
|
|
2594
|
+
selector: generateSelector(element)
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
function getImplicitRole(element) {
|
|
2598
|
+
const tagName = element.tagName.toLowerCase();
|
|
2599
|
+
switch (tagName) {
|
|
2600
|
+
case "button":
|
|
2601
|
+
return "button";
|
|
2602
|
+
case "a":
|
|
2603
|
+
return element.hasAttribute("href") ? "link" : "generic";
|
|
2604
|
+
case "input": {
|
|
2605
|
+
const type = (element.getAttribute("type") || "text").toLowerCase();
|
|
2606
|
+
switch (type) {
|
|
2607
|
+
case "button":
|
|
2608
|
+
case "submit":
|
|
2609
|
+
case "reset":
|
|
2610
|
+
case "image":
|
|
2611
|
+
return "button";
|
|
2612
|
+
case "checkbox":
|
|
2613
|
+
return "checkbox";
|
|
2614
|
+
case "radio":
|
|
2615
|
+
return "radio";
|
|
2616
|
+
default:
|
|
2617
|
+
return "textbox";
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
default:
|
|
2621
|
+
return "generic";
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
function resolveAriaLabelledBy(element, idRefs) {
|
|
2625
|
+
const ownerDoc = element.ownerDocument;
|
|
2626
|
+
if (!ownerDoc) {
|
|
2627
|
+
return "";
|
|
2628
|
+
}
|
|
2629
|
+
const ids = idRefs.trim().split(/\s+/).filter(Boolean);
|
|
2630
|
+
const parts = [];
|
|
2631
|
+
for (const id of ids) {
|
|
2632
|
+
const referenced = ownerDoc.getElementById(id);
|
|
2633
|
+
if (referenced && referenced.textContent) {
|
|
2634
|
+
parts.push(referenced.textContent.trim());
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
return parts.join(" ");
|
|
2638
|
+
}
|
|
2639
|
+
function normalizeStateValue(value) {
|
|
2640
|
+
if (value === null) {
|
|
2641
|
+
return null;
|
|
2642
|
+
}
|
|
2643
|
+
if (value === "true") {
|
|
2644
|
+
return true;
|
|
2645
|
+
}
|
|
2646
|
+
if (value === "false") {
|
|
2647
|
+
return false;
|
|
2648
|
+
}
|
|
2649
|
+
return value;
|
|
2650
|
+
}
|
|
2651
|
+
return {
|
|
2652
|
+
start() {
|
|
2653
|
+
if (active) return;
|
|
2654
|
+
active = true;
|
|
2655
|
+
observer = new MutationObserver(handleMutations);
|
|
2656
|
+
observer.observe(doc.body ?? doc.documentElement, {
|
|
2657
|
+
attributes: true,
|
|
2658
|
+
attributeOldValue: true,
|
|
2659
|
+
attributeFilter: OBSERVED_ATTRIBUTES,
|
|
2660
|
+
subtree: true
|
|
2661
|
+
});
|
|
2662
|
+
},
|
|
2663
|
+
stop() {
|
|
2664
|
+
if (!active) return;
|
|
2665
|
+
active = false;
|
|
2666
|
+
if (observer) {
|
|
2667
|
+
observer.disconnect();
|
|
2668
|
+
observer = null;
|
|
2669
|
+
}
|
|
2670
|
+
},
|
|
2671
|
+
get isActive() {
|
|
2672
|
+
return active;
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// src/runtime/observers/live-region-observer.ts
|
|
2678
|
+
var IMPLICIT_LIVE_ROLES = /* @__PURE__ */ new Set(["alert", "status", "log", "marquee", "timer"]);
|
|
2679
|
+
var ROLE_POLITENESS_MAP = {
|
|
2680
|
+
alert: "assertive",
|
|
2681
|
+
status: "polite",
|
|
2682
|
+
log: "polite",
|
|
2683
|
+
marquee: "polite",
|
|
2684
|
+
timer: "polite"
|
|
2685
|
+
};
|
|
2686
|
+
function createLiveRegionObserver(document, callback) {
|
|
2687
|
+
let contentObserver = null;
|
|
2688
|
+
let documentObserver = null;
|
|
2689
|
+
const trackedRegions = /* @__PURE__ */ new Set();
|
|
2690
|
+
function getLiveRegionPoliteness(element) {
|
|
2691
|
+
const ariaLive = element.getAttribute("aria-live");
|
|
2692
|
+
if (ariaLive === "assertive") return "assertive";
|
|
2693
|
+
if (ariaLive === "polite") return "polite";
|
|
2694
|
+
if (ariaLive === "off") return null;
|
|
2695
|
+
const role = element.getAttribute("role");
|
|
2696
|
+
if (role && IMPLICIT_LIVE_ROLES.has(role)) {
|
|
2697
|
+
return ROLE_POLITENESS_MAP[role] || "polite";
|
|
2698
|
+
}
|
|
2699
|
+
if (ariaLive && ariaLive !== "off") return "polite";
|
|
2700
|
+
return null;
|
|
2701
|
+
}
|
|
2702
|
+
function isAtomic(element) {
|
|
2703
|
+
return element.getAttribute("aria-atomic") === "true";
|
|
2704
|
+
}
|
|
2705
|
+
function buildEventTarget2(element) {
|
|
2706
|
+
const role = element.getAttribute("role") || "region";
|
|
2707
|
+
const accessibleName = element.getAttribute("aria-label") || element.getAttribute("aria-labelledby") || "";
|
|
2708
|
+
const selector = generateSelector(element);
|
|
2709
|
+
return { role, accessibleName, selector };
|
|
2710
|
+
}
|
|
2711
|
+
function getTextFromNodes(nodes) {
|
|
2712
|
+
const parts = [];
|
|
2713
|
+
nodes.forEach((node) => {
|
|
2714
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
2715
|
+
const text = node.textContent?.trim();
|
|
2716
|
+
if (text) parts.push(text);
|
|
2717
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
2718
|
+
const text = node.textContent?.trim();
|
|
2719
|
+
if (text) parts.push(text);
|
|
2720
|
+
}
|
|
2721
|
+
});
|
|
2722
|
+
return parts.join(" ").trim();
|
|
2723
|
+
}
|
|
2724
|
+
function emitAnnouncement(liveRegion, text, politeness) {
|
|
2725
|
+
if (!text) return;
|
|
2726
|
+
const target = buildEventTarget2(liveRegion);
|
|
2727
|
+
callback({
|
|
2728
|
+
type: "ANNOUNCEMENT",
|
|
2729
|
+
target,
|
|
2730
|
+
payload: {
|
|
2731
|
+
kind: "announcement",
|
|
2732
|
+
politeness,
|
|
2733
|
+
text
|
|
2734
|
+
}
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
function findLiveRegionAncestor(node) {
|
|
2738
|
+
let current = node;
|
|
2739
|
+
while (current) {
|
|
2740
|
+
if (current.nodeType === Node.ELEMENT_NODE && trackedRegions.has(current)) {
|
|
2741
|
+
return current;
|
|
2742
|
+
}
|
|
2743
|
+
current = current.parentNode;
|
|
2744
|
+
}
|
|
2745
|
+
return null;
|
|
2746
|
+
}
|
|
2747
|
+
function handleContentMutations(mutations) {
|
|
2748
|
+
const regionChanges = /* @__PURE__ */ new Map();
|
|
2749
|
+
for (const mutation of mutations) {
|
|
2750
|
+
const liveRegion = findLiveRegionAncestor(mutation.target);
|
|
2751
|
+
if (!liveRegion) continue;
|
|
2752
|
+
if (!regionChanges.has(liveRegion)) {
|
|
2753
|
+
regionChanges.set(liveRegion, { addedText: [], charDataChanged: false });
|
|
2754
|
+
}
|
|
2755
|
+
const changes = regionChanges.get(liveRegion);
|
|
2756
|
+
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
|
2757
|
+
const text = getTextFromNodes(mutation.addedNodes);
|
|
2758
|
+
if (text) {
|
|
2759
|
+
changes.addedText.push(text);
|
|
2760
|
+
}
|
|
2761
|
+
} else if (mutation.type === "characterData") {
|
|
2762
|
+
changes.charDataChanged = true;
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
for (const [region, changes] of regionChanges) {
|
|
2766
|
+
const politeness = getLiveRegionPoliteness(region);
|
|
2767
|
+
if (!politeness) continue;
|
|
2768
|
+
let text;
|
|
2769
|
+
if (isAtomic(region)) {
|
|
2770
|
+
text = region.textContent?.trim() || "";
|
|
2771
|
+
} else {
|
|
2772
|
+
if (changes.addedText.length > 0) {
|
|
2773
|
+
text = changes.addedText.join(" ").trim();
|
|
2774
|
+
} else if (changes.charDataChanged) {
|
|
2775
|
+
text = region.textContent?.trim() || "";
|
|
2776
|
+
} else {
|
|
2777
|
+
text = "";
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
emitAnnouncement(region, text, politeness);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
function handleDocumentMutations(mutations) {
|
|
2784
|
+
for (const mutation of mutations) {
|
|
2785
|
+
if (mutation.type !== "childList") continue;
|
|
2786
|
+
mutation.addedNodes.forEach((node) => {
|
|
2787
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
2788
|
+
const element = node;
|
|
2789
|
+
if (getLiveRegionPoliteness(element) !== null && !trackedRegions.has(element)) {
|
|
2790
|
+
trackRegion(element);
|
|
2791
|
+
}
|
|
2792
|
+
const descendants = element.querySelectorAll("[aria-live], [role]");
|
|
2793
|
+
descendants.forEach((desc) => {
|
|
2794
|
+
if (getLiveRegionPoliteness(desc) !== null && !trackedRegions.has(desc)) {
|
|
2795
|
+
trackRegion(desc);
|
|
2796
|
+
}
|
|
2797
|
+
});
|
|
2798
|
+
});
|
|
2799
|
+
mutation.removedNodes.forEach((node) => {
|
|
2800
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
2801
|
+
const element = node;
|
|
2802
|
+
if (trackedRegions.has(element)) {
|
|
2803
|
+
trackedRegions.delete(element);
|
|
2804
|
+
}
|
|
2805
|
+
const descendants = element.querySelectorAll("[aria-live], [role]");
|
|
2806
|
+
descendants.forEach((desc) => {
|
|
2807
|
+
trackedRegions.delete(desc);
|
|
2808
|
+
});
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
function trackRegion(element) {
|
|
2813
|
+
trackedRegions.add(element);
|
|
2814
|
+
}
|
|
2815
|
+
function discoverExistingRegions() {
|
|
2816
|
+
const ariaLiveElements = document.querySelectorAll("[aria-live]");
|
|
2817
|
+
ariaLiveElements.forEach((el) => {
|
|
2818
|
+
if (getLiveRegionPoliteness(el) !== null) {
|
|
2819
|
+
trackRegion(el);
|
|
2820
|
+
}
|
|
2821
|
+
});
|
|
2822
|
+
IMPLICIT_LIVE_ROLES.forEach((role) => {
|
|
2823
|
+
const roleElements = document.querySelectorAll(`[role="${role}"]`);
|
|
2824
|
+
roleElements.forEach((el) => {
|
|
2825
|
+
if (!trackedRegions.has(el)) {
|
|
2826
|
+
trackRegion(el);
|
|
2827
|
+
}
|
|
2828
|
+
});
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
function start() {
|
|
2832
|
+
discoverExistingRegions();
|
|
2833
|
+
contentObserver = new MutationObserver(handleContentMutations);
|
|
2834
|
+
const body = document.body;
|
|
2835
|
+
if (body) {
|
|
2836
|
+
contentObserver.observe(body, {
|
|
2837
|
+
childList: true,
|
|
2838
|
+
characterData: true,
|
|
2839
|
+
subtree: true,
|
|
2840
|
+
characterDataOldValue: true
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
documentObserver = new MutationObserver(handleDocumentMutations);
|
|
2844
|
+
if (body) {
|
|
2845
|
+
documentObserver.observe(body, {
|
|
2846
|
+
childList: true,
|
|
2847
|
+
subtree: true
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
function stop() {
|
|
2852
|
+
if (contentObserver) {
|
|
2853
|
+
contentObserver.disconnect();
|
|
2854
|
+
contentObserver = null;
|
|
2855
|
+
}
|
|
2856
|
+
if (documentObserver) {
|
|
2857
|
+
documentObserver.disconnect();
|
|
2858
|
+
documentObserver = null;
|
|
2859
|
+
}
|
|
2860
|
+
trackedRegions.clear();
|
|
2861
|
+
}
|
|
2862
|
+
return { start, stop };
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// src/runtime/observers/dialog-observer.ts
|
|
2866
|
+
function computeDialogAccessibleName(element, document) {
|
|
2867
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
2868
|
+
if (ariaLabel && ariaLabel.trim()) {
|
|
2869
|
+
return ariaLabel.trim();
|
|
2870
|
+
}
|
|
2871
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
2872
|
+
if (labelledBy && labelledBy.trim()) {
|
|
2873
|
+
const ids = labelledBy.trim().split(/\s+/);
|
|
2874
|
+
const parts = [];
|
|
2875
|
+
for (const id of ids) {
|
|
2876
|
+
const referenced = document.getElementById(id);
|
|
2877
|
+
if (referenced && referenced.textContent) {
|
|
2878
|
+
parts.push(referenced.textContent.trim());
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
const combined = parts.join(" ");
|
|
2882
|
+
if (combined) {
|
|
2883
|
+
return combined;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const title = element.getAttribute("title");
|
|
2887
|
+
if (title && title.trim()) {
|
|
2888
|
+
return title.trim();
|
|
2889
|
+
}
|
|
2890
|
+
const heading = element.querySelector("h1, h2, h3, h4, h5, h6");
|
|
2891
|
+
if (heading && heading.textContent && heading.textContent.trim()) {
|
|
2892
|
+
return heading.textContent.trim();
|
|
2893
|
+
}
|
|
2894
|
+
return "";
|
|
2895
|
+
}
|
|
2896
|
+
function getDialogRole(element) {
|
|
2897
|
+
const explicitRole = element.getAttribute("role");
|
|
2898
|
+
if (explicitRole && explicitRole.trim()) {
|
|
2899
|
+
return explicitRole.trim();
|
|
2900
|
+
}
|
|
2901
|
+
if (element.tagName.toLowerCase() === "dialog") {
|
|
2902
|
+
return "dialog";
|
|
2903
|
+
}
|
|
2904
|
+
return "generic";
|
|
2905
|
+
}
|
|
2906
|
+
function isDialogElement(element) {
|
|
2907
|
+
if (element.tagName.toLowerCase() === "dialog") {
|
|
2908
|
+
return true;
|
|
2909
|
+
}
|
|
2910
|
+
const role = element.getAttribute("role");
|
|
2911
|
+
return role === "dialog" || role === "alertdialog";
|
|
2912
|
+
}
|
|
2913
|
+
function isDialogModal(element) {
|
|
2914
|
+
const ariaModal = element.getAttribute("aria-modal");
|
|
2915
|
+
if (ariaModal === "true") {
|
|
2916
|
+
return true;
|
|
2917
|
+
}
|
|
2918
|
+
return false;
|
|
2919
|
+
}
|
|
2920
|
+
function createDialogObserver(options) {
|
|
2921
|
+
const { document: doc, onEvent, getTimestamp } = options;
|
|
2922
|
+
let active = false;
|
|
2923
|
+
let mutationObserver = null;
|
|
2924
|
+
const openDialogStack = [];
|
|
2925
|
+
function buildEventTarget2(element) {
|
|
2926
|
+
return {
|
|
2927
|
+
role: getDialogRole(element),
|
|
2928
|
+
accessibleName: computeDialogAccessibleName(element, doc),
|
|
2929
|
+
selector: generateSelector(element)
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
function emitDialogOpened(element) {
|
|
2933
|
+
const name = computeDialogAccessibleName(element, doc);
|
|
2934
|
+
const isModal = isDialogModal(element);
|
|
2935
|
+
const target = buildEventTarget2(element);
|
|
2936
|
+
openDialogStack.push({
|
|
2937
|
+
element,
|
|
2938
|
+
name,
|
|
2939
|
+
selector: target.selector
|
|
2940
|
+
});
|
|
2941
|
+
const a11yEvent = {
|
|
2942
|
+
type: "DIALOG_OPENED",
|
|
2943
|
+
timestamp: getTimestamp(),
|
|
2944
|
+
target,
|
|
2945
|
+
payload: {
|
|
2946
|
+
kind: "dialog_opened",
|
|
2947
|
+
dialogName: name,
|
|
2948
|
+
isModal
|
|
2949
|
+
}
|
|
2950
|
+
};
|
|
2951
|
+
onEvent(a11yEvent);
|
|
2952
|
+
}
|
|
2953
|
+
function emitDialogClosed(element) {
|
|
2954
|
+
const name = computeDialogAccessibleName(element, doc);
|
|
2955
|
+
const target = buildEventTarget2(element);
|
|
2956
|
+
const index = openDialogStack.findIndex((d) => d.element === element);
|
|
2957
|
+
if (index !== -1) {
|
|
2958
|
+
openDialogStack.splice(index, 1);
|
|
2959
|
+
}
|
|
2960
|
+
const a11yEvent = {
|
|
2961
|
+
type: "DIALOG_CLOSED",
|
|
2962
|
+
timestamp: getTimestamp(),
|
|
2963
|
+
target,
|
|
2964
|
+
payload: {
|
|
2965
|
+
kind: "dialog_closed",
|
|
2966
|
+
dialogName: name
|
|
2967
|
+
}
|
|
2968
|
+
};
|
|
2969
|
+
onEvent(a11yEvent);
|
|
2970
|
+
}
|
|
2971
|
+
function isTrackedAsOpen(element) {
|
|
2972
|
+
return openDialogStack.some((d) => d.element === element);
|
|
2973
|
+
}
|
|
2974
|
+
function handleMutations(mutations) {
|
|
2975
|
+
if (!active) return;
|
|
2976
|
+
for (const mutation of mutations) {
|
|
2977
|
+
if (mutation.type === "attributes") {
|
|
2978
|
+
const target = mutation.target;
|
|
2979
|
+
if (!(target instanceof doc.defaultView.Element)) continue;
|
|
2980
|
+
if (mutation.attributeName === "open" && target.tagName.toLowerCase() === "dialog") {
|
|
2981
|
+
if (target.hasAttribute("open")) {
|
|
2982
|
+
if (!isTrackedAsOpen(target)) {
|
|
2983
|
+
emitDialogOpened(target);
|
|
2984
|
+
}
|
|
2985
|
+
} else {
|
|
2986
|
+
if (isTrackedAsOpen(target)) {
|
|
2987
|
+
emitDialogClosed(target);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
if (mutation.attributeName === "aria-modal" && isDialogElement(target)) {
|
|
2992
|
+
if (!isTrackedAsOpen(target) && isVisibleDialog(target)) {
|
|
2993
|
+
emitDialogOpened(target);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
if (mutation.attributeName === "role") {
|
|
2997
|
+
const oldRole = mutation.oldValue;
|
|
2998
|
+
const newRole = target.getAttribute("role");
|
|
2999
|
+
const wasDialog = oldRole === "dialog" || oldRole === "alertdialog";
|
|
3000
|
+
const isNowDialog = newRole === "dialog" || newRole === "alertdialog";
|
|
3001
|
+
if (!wasDialog && isNowDialog && isVisibleDialog(target)) {
|
|
3002
|
+
emitDialogOpened(target);
|
|
3003
|
+
} else if (wasDialog && !isNowDialog && isTrackedAsOpen(target)) {
|
|
3004
|
+
emitDialogClosed(target);
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
if (mutation.type === "childList") {
|
|
3009
|
+
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
3010
|
+
const node = mutation.addedNodes[i];
|
|
3011
|
+
if (!(node instanceof doc.defaultView.Element)) continue;
|
|
3012
|
+
if (isDialogElement(node) && isVisibleDialog(node)) {
|
|
3013
|
+
if (!isTrackedAsOpen(node)) {
|
|
3014
|
+
emitDialogOpened(node);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
const dialogDescendants = node.querySelectorAll(
|
|
3018
|
+
'dialog[open], [role="dialog"], [role="alertdialog"]'
|
|
3019
|
+
);
|
|
3020
|
+
for (let j = 0; j < dialogDescendants.length; j++) {
|
|
3021
|
+
const dialog = dialogDescendants[j];
|
|
3022
|
+
if (isVisibleDialog(dialog) && !isTrackedAsOpen(dialog)) {
|
|
3023
|
+
emitDialogOpened(dialog);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
for (let i = 0; i < mutation.removedNodes.length; i++) {
|
|
3028
|
+
const node = mutation.removedNodes[i];
|
|
3029
|
+
if (!(node instanceof doc.defaultView.Element)) continue;
|
|
3030
|
+
if (isTrackedAsOpen(node)) {
|
|
3031
|
+
emitDialogClosed(node);
|
|
3032
|
+
}
|
|
3033
|
+
const trackedInRemoved = openDialogStack.filter(
|
|
3034
|
+
(d) => node.contains(d.element)
|
|
3035
|
+
);
|
|
3036
|
+
for (const tracked of trackedInRemoved) {
|
|
3037
|
+
emitDialogClosed(tracked.element);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
function isVisibleDialog(element) {
|
|
3044
|
+
if (element.tagName.toLowerCase() === "dialog") {
|
|
3045
|
+
return element.hasAttribute("open");
|
|
3046
|
+
}
|
|
3047
|
+
return true;
|
|
3048
|
+
}
|
|
3049
|
+
return {
|
|
3050
|
+
start() {
|
|
3051
|
+
if (active) return;
|
|
3052
|
+
active = true;
|
|
3053
|
+
openDialogStack.length = 0;
|
|
3054
|
+
const existingDialogs = doc.querySelectorAll(
|
|
3055
|
+
'dialog[open], [role="dialog"], [role="alertdialog"]'
|
|
3056
|
+
);
|
|
3057
|
+
for (let i = 0; i < existingDialogs.length; i++) {
|
|
3058
|
+
const dialog = existingDialogs[i];
|
|
3059
|
+
if (isVisibleDialog(dialog)) {
|
|
3060
|
+
openDialogStack.push({
|
|
3061
|
+
element: dialog,
|
|
3062
|
+
name: computeDialogAccessibleName(dialog, doc),
|
|
3063
|
+
selector: generateSelector(dialog)
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
mutationObserver = new MutationObserver(handleMutations);
|
|
3068
|
+
mutationObserver.observe(doc.body || doc.documentElement, {
|
|
3069
|
+
attributes: true,
|
|
3070
|
+
attributeFilter: ["open", "role", "aria-modal"],
|
|
3071
|
+
attributeOldValue: true,
|
|
3072
|
+
childList: true,
|
|
3073
|
+
subtree: true
|
|
3074
|
+
});
|
|
3075
|
+
},
|
|
3076
|
+
stop() {
|
|
3077
|
+
if (!active) return;
|
|
3078
|
+
active = false;
|
|
3079
|
+
if (mutationObserver) {
|
|
3080
|
+
mutationObserver.disconnect();
|
|
3081
|
+
mutationObserver = null;
|
|
3082
|
+
}
|
|
3083
|
+
openDialogStack.length = 0;
|
|
3084
|
+
},
|
|
3085
|
+
get isActive() {
|
|
3086
|
+
return active;
|
|
3087
|
+
},
|
|
3088
|
+
getOpenDialogs() {
|
|
3089
|
+
return openDialogStack.map((d) => d.name);
|
|
3090
|
+
}
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
// src/runtime/engine.ts
|
|
3095
|
+
function createEngine(options) {
|
|
3096
|
+
const { document: doc } = options;
|
|
3097
|
+
let attached = false;
|
|
3098
|
+
let sessionStart = 0;
|
|
3099
|
+
let events = [];
|
|
3100
|
+
function getTimestamp() {
|
|
3101
|
+
return Date.now() - sessionStart;
|
|
3102
|
+
}
|
|
3103
|
+
function collectEvent(event) {
|
|
3104
|
+
events.push(event);
|
|
3105
|
+
}
|
|
3106
|
+
function collectLiveRegionEvent(event) {
|
|
3107
|
+
const fullEvent = {
|
|
3108
|
+
...event,
|
|
3109
|
+
timestamp: getTimestamp()
|
|
3110
|
+
};
|
|
3111
|
+
events.push(fullEvent);
|
|
3112
|
+
}
|
|
3113
|
+
const focusObs = createFocusObserver({
|
|
3114
|
+
document: doc,
|
|
3115
|
+
onEvent: collectEvent,
|
|
3116
|
+
getTimestamp
|
|
3117
|
+
});
|
|
3118
|
+
const mutationObs = createMutationObserver({
|
|
3119
|
+
document: doc,
|
|
3120
|
+
onEvent: collectEvent,
|
|
3121
|
+
getTimestamp
|
|
3122
|
+
});
|
|
3123
|
+
const liveRegionObs = createLiveRegionObserver(
|
|
3124
|
+
doc,
|
|
3125
|
+
collectLiveRegionEvent
|
|
3126
|
+
);
|
|
3127
|
+
const dialogObs = createDialogObserver({
|
|
3128
|
+
document: doc,
|
|
3129
|
+
onEvent: collectEvent,
|
|
3130
|
+
getTimestamp
|
|
3131
|
+
});
|
|
3132
|
+
return {
|
|
3133
|
+
attach() {
|
|
3134
|
+
if (attached) return;
|
|
3135
|
+
attached = true;
|
|
3136
|
+
sessionStart = Date.now();
|
|
3137
|
+
events = [];
|
|
3138
|
+
focusObs.start();
|
|
3139
|
+
mutationObs.start();
|
|
3140
|
+
liveRegionObs.start();
|
|
3141
|
+
dialogObs.start();
|
|
3142
|
+
},
|
|
3143
|
+
detach() {
|
|
3144
|
+
if (!attached) return;
|
|
3145
|
+
attached = false;
|
|
3146
|
+
focusObs.stop();
|
|
3147
|
+
mutationObs.stop();
|
|
3148
|
+
liveRegionObs.stop();
|
|
3149
|
+
dialogObs.stop();
|
|
3150
|
+
},
|
|
3151
|
+
getEvents() {
|
|
3152
|
+
return [...events];
|
|
3153
|
+
},
|
|
3154
|
+
reset() {
|
|
3155
|
+
events = [];
|
|
3156
|
+
},
|
|
3157
|
+
get isAttached() {
|
|
3158
|
+
return attached;
|
|
3159
|
+
},
|
|
3160
|
+
get dialogObserver() {
|
|
3161
|
+
return {
|
|
3162
|
+
getOpenDialogs() {
|
|
3163
|
+
return dialogObs.getOpenDialogs();
|
|
3164
|
+
}
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
};
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// src/runtime/interactions.ts
|
|
3171
|
+
var KEY_MAP = {
|
|
3172
|
+
Tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
3173
|
+
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
3174
|
+
Enter: { key: "Enter", code: "Enter", keyCode: 13 },
|
|
3175
|
+
" ": { key: " ", code: "Space", keyCode: 32 },
|
|
3176
|
+
ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
3177
|
+
ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
3178
|
+
ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
3179
|
+
ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 }
|
|
3180
|
+
};
|
|
3181
|
+
var FOCUSABLE_SELECTOR = [
|
|
3182
|
+
"button:not([disabled])",
|
|
3183
|
+
"a[href]",
|
|
3184
|
+
"input:not([disabled])",
|
|
3185
|
+
"select:not([disabled])",
|
|
3186
|
+
"textarea:not([disabled])",
|
|
3187
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
3188
|
+
].join(", ");
|
|
3189
|
+
function getFocusableElements(document) {
|
|
3190
|
+
return Array.from(document.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
3191
|
+
}
|
|
3192
|
+
function computeAccessibleName4(element, document) {
|
|
3193
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
3194
|
+
if (ariaLabel && ariaLabel.trim()) {
|
|
3195
|
+
return ariaLabel.trim();
|
|
3196
|
+
}
|
|
3197
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
3198
|
+
if (labelledBy && labelledBy.trim()) {
|
|
3199
|
+
const ids = labelledBy.trim().split(/\s+/);
|
|
3200
|
+
const parts = [];
|
|
3201
|
+
for (const id of ids) {
|
|
3202
|
+
const referenced = document.getElementById(id);
|
|
3203
|
+
if (referenced && referenced.textContent) {
|
|
3204
|
+
parts.push(referenced.textContent.trim());
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
const combined = parts.join(" ");
|
|
3208
|
+
if (combined) return combined;
|
|
3209
|
+
}
|
|
3210
|
+
const textContent = element.textContent;
|
|
3211
|
+
if (textContent && textContent.trim()) {
|
|
3212
|
+
return textContent.trim();
|
|
3213
|
+
}
|
|
3214
|
+
return "";
|
|
3215
|
+
}
|
|
3216
|
+
function getElementRole3(element) {
|
|
3217
|
+
const explicitRole = element.getAttribute("role");
|
|
3218
|
+
if (explicitRole && explicitRole.trim()) {
|
|
3219
|
+
return explicitRole.trim();
|
|
3220
|
+
}
|
|
3221
|
+
const tagName = element.tagName.toLowerCase();
|
|
3222
|
+
switch (tagName) {
|
|
3223
|
+
case "button":
|
|
3224
|
+
return "button";
|
|
3225
|
+
case "a":
|
|
3226
|
+
return element.hasAttribute("href") ? "link" : "generic";
|
|
3227
|
+
case "input": {
|
|
3228
|
+
const type = (element.getAttribute("type") || "text").toLowerCase();
|
|
3229
|
+
switch (type) {
|
|
3230
|
+
case "button":
|
|
3231
|
+
case "submit":
|
|
3232
|
+
case "reset":
|
|
3233
|
+
case "image":
|
|
3234
|
+
return "button";
|
|
3235
|
+
case "checkbox":
|
|
3236
|
+
return "checkbox";
|
|
3237
|
+
case "radio":
|
|
3238
|
+
return "radio";
|
|
3239
|
+
case "range":
|
|
3240
|
+
return "slider";
|
|
3241
|
+
case "search":
|
|
3242
|
+
return "searchbox";
|
|
3243
|
+
default:
|
|
3244
|
+
return "textbox";
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
case "textarea":
|
|
3248
|
+
return "textbox";
|
|
3249
|
+
case "select":
|
|
3250
|
+
return element.hasAttribute("multiple") ? "listbox" : "combobox";
|
|
3251
|
+
case "dialog":
|
|
3252
|
+
return "dialog";
|
|
3253
|
+
default:
|
|
3254
|
+
return "generic";
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
function buildEventTarget(element, document) {
|
|
3258
|
+
return {
|
|
3259
|
+
role: getElementRole3(element),
|
|
3260
|
+
accessibleName: computeAccessibleName4(element, document),
|
|
3261
|
+
selector: generateSelector(element)
|
|
3262
|
+
};
|
|
3263
|
+
}
|
|
3264
|
+
function dispatchKeyboardEvent(document, options) {
|
|
3265
|
+
const target = document.activeElement || document.body;
|
|
3266
|
+
const commonProps = {
|
|
3267
|
+
key: options.key,
|
|
3268
|
+
code: options.code,
|
|
3269
|
+
keyCode: options.keyCode,
|
|
3270
|
+
which: options.keyCode,
|
|
3271
|
+
shiftKey: options.shiftKey || false,
|
|
3272
|
+
ctrlKey: options.ctrlKey || false,
|
|
3273
|
+
altKey: options.altKey || false,
|
|
3274
|
+
metaKey: options.metaKey || false,
|
|
3275
|
+
bubbles: true,
|
|
3276
|
+
cancelable: true
|
|
3277
|
+
};
|
|
3278
|
+
target.dispatchEvent(new KeyboardEvent("keydown", commonProps));
|
|
3279
|
+
if (options.key.length === 1) {
|
|
3280
|
+
target.dispatchEvent(new KeyboardEvent("keypress", commonProps));
|
|
3281
|
+
}
|
|
3282
|
+
target.dispatchEvent(new KeyboardEvent("keyup", commonProps));
|
|
3283
|
+
}
|
|
3284
|
+
function parseKeyCombo(combo) {
|
|
3285
|
+
const parts = combo.split("+");
|
|
3286
|
+
const modifiers = [];
|
|
3287
|
+
let shiftKey = false;
|
|
3288
|
+
let ctrlKey = false;
|
|
3289
|
+
let altKey = false;
|
|
3290
|
+
let metaKey = false;
|
|
3291
|
+
const key = parts[parts.length - 1];
|
|
3292
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
3293
|
+
const mod = parts[i].toLowerCase();
|
|
3294
|
+
switch (mod) {
|
|
3295
|
+
case "shift":
|
|
3296
|
+
shiftKey = true;
|
|
3297
|
+
modifiers.push("Shift");
|
|
3298
|
+
break;
|
|
3299
|
+
case "control":
|
|
3300
|
+
case "ctrl":
|
|
3301
|
+
ctrlKey = true;
|
|
3302
|
+
modifiers.push("Control");
|
|
3303
|
+
break;
|
|
3304
|
+
case "alt":
|
|
3305
|
+
altKey = true;
|
|
3306
|
+
modifiers.push("Alt");
|
|
3307
|
+
break;
|
|
3308
|
+
case "meta":
|
|
3309
|
+
case "cmd":
|
|
3310
|
+
case "command":
|
|
3311
|
+
metaKey = true;
|
|
3312
|
+
modifiers.push("Meta");
|
|
3313
|
+
break;
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
return { key, modifiers, shiftKey, ctrlKey, altKey, metaKey };
|
|
3317
|
+
}
|
|
3318
|
+
function getKeyInfo(key) {
|
|
3319
|
+
if (KEY_MAP[key]) {
|
|
3320
|
+
return KEY_MAP[key];
|
|
3321
|
+
}
|
|
3322
|
+
if (key.length === 1) {
|
|
3323
|
+
return {
|
|
3324
|
+
key,
|
|
3325
|
+
code: `Key${key.toUpperCase()}`,
|
|
3326
|
+
keyCode: key.toUpperCase().charCodeAt(0)
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
return {
|
|
3330
|
+
key,
|
|
3331
|
+
code: key,
|
|
3332
|
+
keyCode: 0
|
|
3333
|
+
};
|
|
3334
|
+
}
|
|
3335
|
+
function executeClick(selector, document, onEvent, getTimestamp) {
|
|
3336
|
+
const element = document.querySelector(selector);
|
|
3337
|
+
if (!element) {
|
|
3338
|
+
const warningEvent = {
|
|
3339
|
+
type: "WARNING",
|
|
3340
|
+
timestamp: getTimestamp(),
|
|
3341
|
+
target: {
|
|
3342
|
+
role: "generic",
|
|
3343
|
+
accessibleName: "",
|
|
3344
|
+
selector
|
|
3345
|
+
},
|
|
3346
|
+
payload: {
|
|
3347
|
+
kind: "warning",
|
|
3348
|
+
message: `Element not found: ${selector}`
|
|
3349
|
+
}
|
|
3350
|
+
};
|
|
3351
|
+
onEvent(warningEvent);
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
const eventProps = { bubbles: true, cancelable: true };
|
|
3355
|
+
element.dispatchEvent(new MouseEvent("mousedown", eventProps));
|
|
3356
|
+
element.dispatchEvent(new MouseEvent("mouseup", eventProps));
|
|
3357
|
+
element.dispatchEvent(new MouseEvent("click", eventProps));
|
|
3358
|
+
if ("focus" in element && typeof element.focus === "function") {
|
|
3359
|
+
element.focus();
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
function executeTab(document, onEvent, getTimestamp) {
|
|
3363
|
+
const focusableElements = getFocusableElements(document);
|
|
3364
|
+
if (focusableElements.length === 0) return;
|
|
3365
|
+
const activeElement = document.activeElement;
|
|
3366
|
+
let currentIndex = -1;
|
|
3367
|
+
if (activeElement) {
|
|
3368
|
+
currentIndex = focusableElements.indexOf(activeElement);
|
|
3369
|
+
}
|
|
3370
|
+
const nextIndex = (currentIndex + 1) % focusableElements.length;
|
|
3371
|
+
const nextElement = focusableElements[nextIndex];
|
|
3372
|
+
dispatchKeyboardEvent(document, {
|
|
3373
|
+
...KEY_MAP["Tab"],
|
|
3374
|
+
shiftKey: false
|
|
3375
|
+
});
|
|
3376
|
+
if (nextElement && typeof nextElement.focus === "function") {
|
|
3377
|
+
nextElement.focus();
|
|
3378
|
+
}
|
|
3379
|
+
const target = document.activeElement || document.body;
|
|
3380
|
+
const keyboardEvent = {
|
|
3381
|
+
type: "KEYBOARD_ACTION",
|
|
3382
|
+
timestamp: getTimestamp(),
|
|
3383
|
+
target: buildEventTarget(target, document),
|
|
3384
|
+
payload: {
|
|
3385
|
+
kind: "keyboard_action",
|
|
3386
|
+
key: "Tab",
|
|
3387
|
+
modifiers: []
|
|
3388
|
+
}
|
|
3389
|
+
};
|
|
3390
|
+
onEvent(keyboardEvent);
|
|
3391
|
+
}
|
|
3392
|
+
function executeShiftTab(document, onEvent, getTimestamp) {
|
|
3393
|
+
const focusableElements = getFocusableElements(document);
|
|
3394
|
+
if (focusableElements.length === 0) return;
|
|
3395
|
+
const activeElement = document.activeElement;
|
|
3396
|
+
let currentIndex = -1;
|
|
3397
|
+
if (activeElement) {
|
|
3398
|
+
currentIndex = focusableElements.indexOf(activeElement);
|
|
3399
|
+
}
|
|
3400
|
+
const prevIndex = currentIndex <= 0 ? focusableElements.length - 1 : currentIndex - 1;
|
|
3401
|
+
const prevElement = focusableElements[prevIndex];
|
|
3402
|
+
dispatchKeyboardEvent(document, {
|
|
3403
|
+
...KEY_MAP["Tab"],
|
|
3404
|
+
shiftKey: true
|
|
3405
|
+
});
|
|
3406
|
+
if (prevElement && typeof prevElement.focus === "function") {
|
|
3407
|
+
prevElement.focus();
|
|
3408
|
+
}
|
|
3409
|
+
const target = document.activeElement || document.body;
|
|
3410
|
+
const keyboardEvent = {
|
|
3411
|
+
type: "KEYBOARD_ACTION",
|
|
3412
|
+
timestamp: getTimestamp(),
|
|
3413
|
+
target: buildEventTarget(target, document),
|
|
3414
|
+
payload: {
|
|
3415
|
+
kind: "keyboard_action",
|
|
3416
|
+
key: "Tab",
|
|
3417
|
+
modifiers: ["Shift"]
|
|
3418
|
+
}
|
|
3419
|
+
};
|
|
3420
|
+
onEvent(keyboardEvent);
|
|
3421
|
+
}
|
|
3422
|
+
function executeKeyAction(keyName, document, onEvent, getTimestamp) {
|
|
3423
|
+
const keyInfo = KEY_MAP[keyName];
|
|
3424
|
+
if (!keyInfo) return;
|
|
3425
|
+
dispatchKeyboardEvent(document, {
|
|
3426
|
+
...keyInfo
|
|
3427
|
+
});
|
|
3428
|
+
const target = document.activeElement || document.body;
|
|
3429
|
+
const keyboardEvent = {
|
|
3430
|
+
type: "KEYBOARD_ACTION",
|
|
3431
|
+
timestamp: getTimestamp(),
|
|
3432
|
+
target: buildEventTarget(target, document),
|
|
3433
|
+
payload: {
|
|
3434
|
+
kind: "keyboard_action",
|
|
3435
|
+
key: keyName,
|
|
3436
|
+
modifiers: []
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3439
|
+
onEvent(keyboardEvent);
|
|
3440
|
+
}
|
|
3441
|
+
function executeKeyCombo(combo, document, onEvent, getTimestamp) {
|
|
3442
|
+
const parsed = parseKeyCombo(combo);
|
|
3443
|
+
const keyInfo = getKeyInfo(parsed.key);
|
|
3444
|
+
dispatchKeyboardEvent(document, {
|
|
3445
|
+
...keyInfo,
|
|
3446
|
+
shiftKey: parsed.shiftKey,
|
|
3447
|
+
ctrlKey: parsed.ctrlKey,
|
|
3448
|
+
altKey: parsed.altKey,
|
|
3449
|
+
metaKey: parsed.metaKey
|
|
3450
|
+
});
|
|
3451
|
+
const target = document.activeElement || document.body;
|
|
3452
|
+
const keyboardEvent = {
|
|
3453
|
+
type: "KEYBOARD_ACTION",
|
|
3454
|
+
timestamp: getTimestamp(),
|
|
3455
|
+
target: buildEventTarget(target, document),
|
|
3456
|
+
payload: {
|
|
3457
|
+
kind: "keyboard_action",
|
|
3458
|
+
key: parsed.key,
|
|
3459
|
+
modifiers: parsed.modifiers
|
|
3460
|
+
}
|
|
3461
|
+
};
|
|
3462
|
+
onEvent(keyboardEvent);
|
|
3463
|
+
}
|
|
3464
|
+
async function executeAction(action, document, onEvent, getTimestamp) {
|
|
3465
|
+
switch (action.type) {
|
|
3466
|
+
case "click":
|
|
3467
|
+
executeClick(action.selector, document, onEvent, getTimestamp);
|
|
3468
|
+
break;
|
|
3469
|
+
case "tab":
|
|
3470
|
+
executeTab(document, onEvent, getTimestamp);
|
|
3471
|
+
break;
|
|
3472
|
+
case "shiftTab":
|
|
3473
|
+
executeShiftTab(document, onEvent, getTimestamp);
|
|
3474
|
+
break;
|
|
3475
|
+
case "escape":
|
|
3476
|
+
executeKeyAction("Escape", document, onEvent, getTimestamp);
|
|
3477
|
+
break;
|
|
3478
|
+
case "enter":
|
|
3479
|
+
executeKeyAction("Enter", document, onEvent, getTimestamp);
|
|
3480
|
+
break;
|
|
3481
|
+
case "space":
|
|
3482
|
+
executeKeyAction(" ", document, onEvent, getTimestamp);
|
|
3483
|
+
break;
|
|
3484
|
+
case "arrowUp":
|
|
3485
|
+
executeKeyAction("ArrowUp", document, onEvent, getTimestamp);
|
|
3486
|
+
break;
|
|
3487
|
+
case "arrowDown":
|
|
3488
|
+
executeKeyAction("ArrowDown", document, onEvent, getTimestamp);
|
|
3489
|
+
break;
|
|
3490
|
+
case "arrowLeft":
|
|
3491
|
+
executeKeyAction("ArrowLeft", document, onEvent, getTimestamp);
|
|
3492
|
+
break;
|
|
3493
|
+
case "arrowRight":
|
|
3494
|
+
executeKeyAction("ArrowRight", document, onEvent, getTimestamp);
|
|
3495
|
+
break;
|
|
3496
|
+
case "key":
|
|
3497
|
+
executeKeyCombo(action.combo, document, onEvent, getTimestamp);
|
|
3498
|
+
break;
|
|
3499
|
+
case "wait":
|
|
3500
|
+
await new Promise((resolve) => setTimeout(resolve, action.ms));
|
|
3501
|
+
break;
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
async function executeSequence(sequence, document, onEvent, getTimestamp, settleTime = 100) {
|
|
3505
|
+
for (let i = 0; i < sequence.actions.length; i++) {
|
|
3506
|
+
const action = sequence.actions[i];
|
|
3507
|
+
await executeAction(action, document, onEvent, getTimestamp);
|
|
3508
|
+
if (i < sequence.actions.length - 1) {
|
|
3509
|
+
await new Promise((resolve) => setTimeout(resolve, settleTime));
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
// src/runtime/heuristics.ts
|
|
3515
|
+
var DEFAULT_CONFIG = {
|
|
3516
|
+
dialogFocusTimeout: 100,
|
|
3517
|
+
rapidAnnouncementWindow: 500,
|
|
3518
|
+
rapidAnnouncementThreshold: 3,
|
|
3519
|
+
keyboardResponseTimeout: 200
|
|
3520
|
+
};
|
|
3521
|
+
function createHeuristicAnalyzer(config) {
|
|
3522
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
3523
|
+
const trackedDialogs = [];
|
|
3524
|
+
const pendingDialogFocusChecks = [];
|
|
3525
|
+
const pendingKeyboardResponses = [];
|
|
3526
|
+
const announcementTimestamps = [];
|
|
3527
|
+
let lastFocusedSelector = null;
|
|
3528
|
+
function createWarning(timestamp, message, target, relatedEvent) {
|
|
3529
|
+
const payload = {
|
|
3530
|
+
kind: "warning",
|
|
3531
|
+
message,
|
|
3532
|
+
relatedEvent
|
|
3533
|
+
};
|
|
3534
|
+
return {
|
|
3535
|
+
type: "WARNING",
|
|
3536
|
+
timestamp,
|
|
3537
|
+
target,
|
|
3538
|
+
payload
|
|
3539
|
+
};
|
|
3540
|
+
}
|
|
3541
|
+
function checkExpiredDialogFocusTimeouts(currentTimestamp) {
|
|
3542
|
+
const warnings = [];
|
|
3543
|
+
for (let i = pendingDialogFocusChecks.length - 1; i >= 0; i--) {
|
|
3544
|
+
const pending = pendingDialogFocusChecks[i];
|
|
3545
|
+
if (pending.resolved) continue;
|
|
3546
|
+
const elapsed = currentTimestamp - pending.openedAt;
|
|
3547
|
+
if (elapsed > cfg.dialogFocusTimeout) {
|
|
3548
|
+
if (pending.isModal) {
|
|
3549
|
+
warnings.push(
|
|
3550
|
+
createWarning(
|
|
3551
|
+
currentTimestamp,
|
|
3552
|
+
"Focus was not moved into modal dialog",
|
|
3553
|
+
{
|
|
3554
|
+
role: "dialog",
|
|
3555
|
+
accessibleName: pending.dialogName,
|
|
3556
|
+
selector: pending.dialogSelector
|
|
3557
|
+
}
|
|
3558
|
+
)
|
|
3559
|
+
);
|
|
3560
|
+
}
|
|
3561
|
+
pending.resolved = true;
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
return warnings;
|
|
3565
|
+
}
|
|
3566
|
+
function checkExpiredKeyboardResponses(currentTimestamp) {
|
|
3567
|
+
const warnings = [];
|
|
3568
|
+
for (let i = pendingKeyboardResponses.length - 1; i >= 0; i--) {
|
|
3569
|
+
const pending = pendingKeyboardResponses[i];
|
|
3570
|
+
if (pending.resolved) continue;
|
|
3571
|
+
const elapsed = currentTimestamp - pending.timestamp;
|
|
3572
|
+
if (elapsed > cfg.keyboardResponseTimeout) {
|
|
3573
|
+
warnings.push(
|
|
3574
|
+
createWarning(
|
|
3575
|
+
currentTimestamp,
|
|
3576
|
+
"Keyboard action produced no accessibility response",
|
|
3577
|
+
pending.event.target,
|
|
3578
|
+
pending.event
|
|
3579
|
+
)
|
|
3580
|
+
);
|
|
3581
|
+
pending.resolved = true;
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
return warnings;
|
|
3585
|
+
}
|
|
3586
|
+
function checkRapidAnnouncements(currentTimestamp) {
|
|
3587
|
+
const warnings = [];
|
|
3588
|
+
const windowStart = currentTimestamp - cfg.rapidAnnouncementWindow;
|
|
3589
|
+
while (announcementTimestamps.length > 0 && announcementTimestamps[0] < windowStart) {
|
|
3590
|
+
announcementTimestamps.shift();
|
|
3591
|
+
}
|
|
3592
|
+
if (announcementTimestamps.length > cfg.rapidAnnouncementThreshold) {
|
|
3593
|
+
warnings.push(
|
|
3594
|
+
createWarning(
|
|
3595
|
+
currentTimestamp,
|
|
3596
|
+
"Rapid announcements detected: assistive technology users may miss content",
|
|
3597
|
+
{
|
|
3598
|
+
role: "region",
|
|
3599
|
+
accessibleName: "",
|
|
3600
|
+
selector: "body"
|
|
3601
|
+
}
|
|
3602
|
+
)
|
|
3603
|
+
);
|
|
3604
|
+
announcementTimestamps.length = 0;
|
|
3605
|
+
}
|
|
3606
|
+
return warnings;
|
|
3607
|
+
}
|
|
3608
|
+
function checkFocusEscape(event) {
|
|
3609
|
+
const warnings = [];
|
|
3610
|
+
if (event.type !== "FOCUS_CHANGED") return warnings;
|
|
3611
|
+
const modalDialogs = trackedDialogs.filter((d) => d.isModal);
|
|
3612
|
+
if (modalDialogs.length === 0) return warnings;
|
|
3613
|
+
const focusedSelector = event.target.selector;
|
|
3614
|
+
for (const dialog of modalDialogs) {
|
|
3615
|
+
if (!isSelectorInside(focusedSelector, dialog.selector)) {
|
|
3616
|
+
warnings.push(
|
|
3617
|
+
createWarning(
|
|
3618
|
+
event.timestamp,
|
|
3619
|
+
"Focus escaped modal dialog",
|
|
3620
|
+
event.target,
|
|
3621
|
+
event
|
|
3622
|
+
)
|
|
3623
|
+
);
|
|
3624
|
+
break;
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
return warnings;
|
|
3628
|
+
}
|
|
3629
|
+
function checkFocusedElementRemoved(event) {
|
|
3630
|
+
const warnings = [];
|
|
3631
|
+
if (event.type === "DOM_MUTATION" && lastFocusedSelector !== null) {
|
|
3632
|
+
if (event.target.selector === lastFocusedSelector) {
|
|
3633
|
+
warnings.push(
|
|
3634
|
+
createWarning(
|
|
3635
|
+
event.timestamp,
|
|
3636
|
+
"Focused element removed without focus management",
|
|
3637
|
+
event.target,
|
|
3638
|
+
event
|
|
3639
|
+
)
|
|
3640
|
+
);
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
return warnings;
|
|
3644
|
+
}
|
|
3645
|
+
function isSelectorInside(childSelector, containerSelector) {
|
|
3646
|
+
if (childSelector === containerSelector) return true;
|
|
3647
|
+
if (childSelector.startsWith(containerSelector + " ")) return true;
|
|
3648
|
+
if (childSelector.startsWith(containerSelector + " > ")) return true;
|
|
3649
|
+
if (childSelector.startsWith(containerSelector + ">")) return true;
|
|
3650
|
+
return false;
|
|
3651
|
+
}
|
|
3652
|
+
function processEvent(event) {
|
|
3653
|
+
const warnings = [];
|
|
3654
|
+
warnings.push(...checkExpiredDialogFocusTimeouts(event.timestamp));
|
|
3655
|
+
warnings.push(...checkExpiredKeyboardResponses(event.timestamp));
|
|
3656
|
+
switch (event.type) {
|
|
3657
|
+
case "DIALOG_OPENED": {
|
|
3658
|
+
const payload = event.payload;
|
|
3659
|
+
const dialog = {
|
|
3660
|
+
selector: event.target.selector,
|
|
3661
|
+
name: payload.dialogName,
|
|
3662
|
+
isModal: payload.isModal
|
|
3663
|
+
};
|
|
3664
|
+
trackedDialogs.push(dialog);
|
|
3665
|
+
if (payload.isModal) {
|
|
3666
|
+
pendingDialogFocusChecks.push({
|
|
3667
|
+
dialogSelector: event.target.selector,
|
|
3668
|
+
dialogName: payload.dialogName,
|
|
3669
|
+
openedAt: event.timestamp,
|
|
3670
|
+
isModal: true,
|
|
3671
|
+
resolved: false
|
|
3672
|
+
});
|
|
3673
|
+
}
|
|
3674
|
+
break;
|
|
3675
|
+
}
|
|
3676
|
+
case "DIALOG_CLOSED": {
|
|
3677
|
+
const closedSelector = event.target.selector;
|
|
3678
|
+
const idx = trackedDialogs.findIndex(
|
|
3679
|
+
(d) => d.selector === closedSelector
|
|
3680
|
+
);
|
|
3681
|
+
if (idx !== -1) {
|
|
3682
|
+
trackedDialogs.splice(idx, 1);
|
|
3683
|
+
}
|
|
3684
|
+
for (const pending of pendingDialogFocusChecks) {
|
|
3685
|
+
if (pending.dialogSelector === closedSelector && !pending.resolved) {
|
|
3686
|
+
pending.resolved = true;
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
break;
|
|
3690
|
+
}
|
|
3691
|
+
case "FOCUS_CHANGED": {
|
|
3692
|
+
for (const pending of pendingDialogFocusChecks) {
|
|
3693
|
+
if (pending.resolved) continue;
|
|
3694
|
+
if (isSelectorInside(event.target.selector, pending.dialogSelector)) {
|
|
3695
|
+
pending.resolved = true;
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
for (const pending of pendingKeyboardResponses) {
|
|
3699
|
+
if (!pending.resolved) {
|
|
3700
|
+
pending.resolved = true;
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
warnings.push(...checkFocusEscape(event));
|
|
3704
|
+
lastFocusedSelector = event.target.selector;
|
|
3705
|
+
break;
|
|
3706
|
+
}
|
|
3707
|
+
case "ANNOUNCEMENT": {
|
|
3708
|
+
announcementTimestamps.push(event.timestamp);
|
|
3709
|
+
for (const pending of pendingKeyboardResponses) {
|
|
3710
|
+
if (!pending.resolved) {
|
|
3711
|
+
pending.resolved = true;
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
warnings.push(...checkRapidAnnouncements(event.timestamp));
|
|
3715
|
+
break;
|
|
3716
|
+
}
|
|
3717
|
+
case "KEYBOARD_ACTION": {
|
|
3718
|
+
pendingKeyboardResponses.push({
|
|
3719
|
+
event,
|
|
3720
|
+
timestamp: event.timestamp,
|
|
3721
|
+
resolved: false
|
|
3722
|
+
});
|
|
3723
|
+
break;
|
|
3724
|
+
}
|
|
3725
|
+
case "DOM_MUTATION": {
|
|
3726
|
+
warnings.push(...checkFocusedElementRemoved(event));
|
|
3727
|
+
for (const pending of pendingKeyboardResponses) {
|
|
3728
|
+
if (!pending.resolved) {
|
|
3729
|
+
pending.resolved = true;
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
break;
|
|
3733
|
+
}
|
|
3734
|
+
case "STATE_CHANGED":
|
|
3735
|
+
case "ROLE_CHANGED":
|
|
3736
|
+
case "ACCESSIBLE_NAME_CHANGED": {
|
|
3737
|
+
for (const pending of pendingKeyboardResponses) {
|
|
3738
|
+
if (!pending.resolved) {
|
|
3739
|
+
pending.resolved = true;
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
break;
|
|
3743
|
+
}
|
|
3744
|
+
default:
|
|
3745
|
+
break;
|
|
3746
|
+
}
|
|
3747
|
+
cleanupResolvedChecks();
|
|
3748
|
+
return warnings;
|
|
3749
|
+
}
|
|
3750
|
+
function cleanupResolvedChecks() {
|
|
3751
|
+
for (let i = pendingDialogFocusChecks.length - 1; i >= 0; i--) {
|
|
3752
|
+
if (pendingDialogFocusChecks[i].resolved) {
|
|
3753
|
+
pendingDialogFocusChecks.splice(i, 1);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
for (let i = pendingKeyboardResponses.length - 1; i >= 0; i--) {
|
|
3757
|
+
if (pendingKeyboardResponses[i].resolved) {
|
|
3758
|
+
pendingKeyboardResponses.splice(i, 1);
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
return {
|
|
3763
|
+
process(event) {
|
|
3764
|
+
return processEvent(event);
|
|
3765
|
+
},
|
|
3766
|
+
get openDialogs() {
|
|
3767
|
+
return trackedDialogs.map((d) => d.selector);
|
|
3768
|
+
}
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3772
|
+
// src/version.ts
|
|
3773
|
+
var SPEAKABLE_VERSION = "1.7.0" ? "1.7.0" : "dev";
|
|
3774
|
+
|
|
3775
|
+
// src/runtime/timeline-generator.ts
|
|
3776
|
+
var TIMELINE_FORMAT_VERSION = "1.0";
|
|
3777
|
+
var DEFAULT_SETTLE_PERIOD = 500;
|
|
3778
|
+
var DEFAULT_LOAD_TIMEOUT = 1e4;
|
|
3779
|
+
var DOM_STABILITY_WAIT = 100;
|
|
3780
|
+
function createTimelineGenerator(options) {
|
|
3781
|
+
const {
|
|
3782
|
+
document: doc,
|
|
3783
|
+
componentName,
|
|
3784
|
+
storyName,
|
|
3785
|
+
settlePeriod = DEFAULT_SETTLE_PERIOD,
|
|
3786
|
+
loadTimeout = DEFAULT_LOAD_TIMEOUT,
|
|
3787
|
+
heuristics: enableHeuristics = true
|
|
3788
|
+
} = options;
|
|
3789
|
+
let aborted = false;
|
|
3790
|
+
let currentEngine = null;
|
|
3791
|
+
return {
|
|
3792
|
+
async capture(sequence) {
|
|
3793
|
+
aborted = false;
|
|
3794
|
+
await waitForDocumentReady(doc, loadTimeout);
|
|
3795
|
+
if (aborted) {
|
|
3796
|
+
return buildAbortedTimeline(componentName, storyName, sequence);
|
|
3797
|
+
}
|
|
3798
|
+
const engine = createEngine({ document: doc });
|
|
3799
|
+
currentEngine = engine;
|
|
3800
|
+
const heuristicAnalyzer = enableHeuristics ? createHeuristicAnalyzer() : null;
|
|
3801
|
+
const interactionEvents = [];
|
|
3802
|
+
const sessionStart = Date.now();
|
|
3803
|
+
engine.attach();
|
|
3804
|
+
if (aborted) {
|
|
3805
|
+
engine.detach();
|
|
3806
|
+
currentEngine = null;
|
|
3807
|
+
return buildAbortedTimeline(componentName, storyName, sequence);
|
|
3808
|
+
}
|
|
3809
|
+
function getTimestamp() {
|
|
3810
|
+
return Date.now() - sessionStart;
|
|
3811
|
+
}
|
|
3812
|
+
await executeSequence(
|
|
3813
|
+
sequence,
|
|
3814
|
+
doc,
|
|
3815
|
+
(event) => {
|
|
3816
|
+
interactionEvents.push(event);
|
|
3817
|
+
},
|
|
3818
|
+
getTimestamp,
|
|
3819
|
+
DOM_STABILITY_WAIT
|
|
3820
|
+
);
|
|
3821
|
+
if (aborted) {
|
|
3822
|
+
engine.detach();
|
|
3823
|
+
currentEngine = null;
|
|
3824
|
+
return buildAbortedTimeline(componentName, storyName, sequence);
|
|
3825
|
+
}
|
|
3826
|
+
await delay(settlePeriod);
|
|
3827
|
+
engine.detach();
|
|
3828
|
+
currentEngine = null;
|
|
3829
|
+
const duration = Date.now() - sessionStart;
|
|
3830
|
+
const engineEvents = engine.getEvents();
|
|
3831
|
+
const allEvents = mergeEvents(engineEvents, interactionEvents);
|
|
3832
|
+
if (heuristicAnalyzer) {
|
|
3833
|
+
const heuristicWarnings = [];
|
|
3834
|
+
for (const event of allEvents) {
|
|
3835
|
+
const warnings = heuristicAnalyzer.process(event);
|
|
3836
|
+
heuristicWarnings.push(...warnings);
|
|
3837
|
+
}
|
|
3838
|
+
for (const warning of heuristicWarnings) {
|
|
3839
|
+
allEvents.push(warning);
|
|
3840
|
+
}
|
|
3841
|
+
allEvents.sort((a, b) => a.timestamp - b.timestamp);
|
|
3842
|
+
}
|
|
3843
|
+
const allWarnings = allEvents.filter((e) => e.type === "WARNING");
|
|
3844
|
+
const metadata = {
|
|
3845
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3846
|
+
speakableVersion: SPEAKABLE_VERSION,
|
|
3847
|
+
sourceUrl: getSourceUrl(doc),
|
|
3848
|
+
userAgent: getUserAgent()
|
|
3849
|
+
};
|
|
3850
|
+
const timeline = {
|
|
3851
|
+
version: TIMELINE_FORMAT_VERSION,
|
|
3852
|
+
component: componentName,
|
|
3853
|
+
story: storyName || null,
|
|
3854
|
+
interactionSequence: sequence.description,
|
|
3855
|
+
duration,
|
|
3856
|
+
events: allEvents,
|
|
3857
|
+
warnings: allWarnings,
|
|
3858
|
+
metadata
|
|
3859
|
+
};
|
|
3860
|
+
return timeline;
|
|
3861
|
+
},
|
|
3862
|
+
abort() {
|
|
3863
|
+
aborted = true;
|
|
3864
|
+
if (currentEngine && currentEngine.isAttached) {
|
|
3865
|
+
currentEngine.detach();
|
|
3866
|
+
currentEngine = null;
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
};
|
|
3870
|
+
}
|
|
3871
|
+
async function waitForDocumentReady(doc, timeout) {
|
|
3872
|
+
if (doc.body) return;
|
|
3873
|
+
return new Promise((resolve, reject) => {
|
|
3874
|
+
const timeoutId = setTimeout(() => {
|
|
3875
|
+
reject(
|
|
3876
|
+
new Error(
|
|
3877
|
+
`Document load timeout (${timeout}ms): ${getSourceUrl(doc)}`
|
|
3878
|
+
)
|
|
3879
|
+
);
|
|
3880
|
+
}, timeout);
|
|
3881
|
+
const interval = setInterval(() => {
|
|
3882
|
+
if (doc.body) {
|
|
3883
|
+
clearTimeout(timeoutId);
|
|
3884
|
+
clearInterval(interval);
|
|
3885
|
+
resolve();
|
|
3886
|
+
}
|
|
3887
|
+
}, 50);
|
|
3888
|
+
});
|
|
3889
|
+
}
|
|
3890
|
+
function mergeEvents(engineEvents, interactionEvents) {
|
|
3891
|
+
const all = [...engineEvents, ...interactionEvents];
|
|
3892
|
+
all.sort((a, b) => a.timestamp - b.timestamp);
|
|
3893
|
+
return all;
|
|
3894
|
+
}
|
|
3895
|
+
function getSourceUrl(doc) {
|
|
3896
|
+
try {
|
|
3897
|
+
const url = doc.URL;
|
|
3898
|
+
if (url && url !== "about:blank" && url !== "") {
|
|
3899
|
+
return url;
|
|
3900
|
+
}
|
|
3901
|
+
} catch {
|
|
3902
|
+
}
|
|
3903
|
+
return "local";
|
|
3904
|
+
}
|
|
3905
|
+
function getUserAgent() {
|
|
3906
|
+
try {
|
|
3907
|
+
if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
3908
|
+
return navigator.userAgent;
|
|
3909
|
+
}
|
|
3910
|
+
} catch {
|
|
3911
|
+
}
|
|
3912
|
+
return "jsdom";
|
|
3913
|
+
}
|
|
3914
|
+
function delay(ms) {
|
|
3915
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3916
|
+
}
|
|
3917
|
+
function buildAbortedTimeline(componentName, storyName, sequence) {
|
|
3918
|
+
return {
|
|
3919
|
+
version: TIMELINE_FORMAT_VERSION,
|
|
3920
|
+
component: componentName,
|
|
3921
|
+
story: storyName || null,
|
|
3922
|
+
interactionSequence: sequence.description,
|
|
3923
|
+
duration: 0,
|
|
3924
|
+
events: [],
|
|
3925
|
+
warnings: [],
|
|
3926
|
+
metadata: {
|
|
3927
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3928
|
+
speakableVersion: SPEAKABLE_VERSION,
|
|
3929
|
+
sourceUrl: "local",
|
|
3930
|
+
userAgent: getUserAgent()
|
|
3931
|
+
}
|
|
3932
|
+
};
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
// src/browser/capture.ts
|
|
3936
|
+
async function captureTimeline(document, options) {
|
|
3937
|
+
const {
|
|
3938
|
+
componentName,
|
|
3939
|
+
storyName,
|
|
3940
|
+
sequence,
|
|
3941
|
+
settlePeriod,
|
|
3942
|
+
awaitUpgrade = true,
|
|
3943
|
+
upgradeTimeoutMs
|
|
3944
|
+
} = options;
|
|
3945
|
+
let upgradeWarnings = [];
|
|
3946
|
+
if (awaitUpgrade) {
|
|
3947
|
+
const warnings = await awaitCustomElementsReady(document, upgradeTimeoutMs);
|
|
3948
|
+
upgradeWarnings = warnings.map((w) => w.message);
|
|
3949
|
+
}
|
|
3950
|
+
const generator = createTimelineGenerator({
|
|
3951
|
+
document,
|
|
3952
|
+
componentName,
|
|
3953
|
+
storyName,
|
|
3954
|
+
...settlePeriod !== void 0 && { settlePeriod }
|
|
3955
|
+
});
|
|
3956
|
+
const timeline = await generator.capture(sequence);
|
|
3957
|
+
if (upgradeWarnings.length > 0) {
|
|
3958
|
+
const upgradeEvents = upgradeWarnings.map((message) => ({
|
|
3959
|
+
type: "WARNING",
|
|
3960
|
+
timestamp: 0,
|
|
3961
|
+
target: { role: "generic", accessibleName: "", selector: ":root" },
|
|
3962
|
+
payload: { kind: "warning", message }
|
|
3963
|
+
}));
|
|
3964
|
+
return {
|
|
3965
|
+
...timeline,
|
|
3966
|
+
events: [...upgradeEvents, ...timeline.events],
|
|
3967
|
+
warnings: [...upgradeEvents, ...timeline.warnings]
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3970
|
+
return timeline;
|
|
3971
|
+
}
|
|
3972
|
+
export {
|
|
3973
|
+
analyzeElement,
|
|
3974
|
+
analyzeElementWithUpgrade,
|
|
3975
|
+
awaitCustomElementsReady,
|
|
3976
|
+
captureTimeline
|
|
3977
|
+
};
|
|
3978
|
+
//# sourceMappingURL=index.js.map
|