@reticular/speakable 1.6.0 → 1.8.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 +4100 -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 +164 -21
- 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.d.ts +1 -1
- package/dist/index.js +168 -24
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +164 -21
- 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-CL3DJBD7.js → chunk-JSBRDJBE.js} +1 -11
- package/dist/storybook/dist/chunk-QVJRQZT5.js +16 -0
- package/dist/storybook/dist/index.js +6 -1
- package/dist/storybook/dist/manager.js +404 -5
- package/dist/storybook/dist/preview.js +2241 -49
- 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 +13 -1
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EVENT_RESULT,
|
|
3
|
+
EVENT_TIMELINE
|
|
4
|
+
} from "./chunk-QVJRQZT5.js";
|
|
5
|
+
import {
|
|
6
|
+
awaitCustomElementsReady
|
|
7
|
+
} from "./chunk-3SVI6IQE.js";
|
|
8
|
+
import {
|
|
3
9
|
__commonJS,
|
|
4
10
|
__toESM
|
|
5
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-JSBRDJBE.js";
|
|
6
12
|
|
|
7
13
|
// ../node_modules/picocolors/picocolors.browser.js
|
|
8
14
|
var require_picocolors_browser = __commonJS({
|
|
@@ -61,6 +67,12 @@ var SUPPORTED_ROLES = [
|
|
|
61
67
|
"meter",
|
|
62
68
|
"progressbar",
|
|
63
69
|
"status",
|
|
70
|
+
"switch",
|
|
71
|
+
"slider",
|
|
72
|
+
"spinbutton",
|
|
73
|
+
"tab",
|
|
74
|
+
"tablist",
|
|
75
|
+
"tabpanel",
|
|
64
76
|
"document",
|
|
65
77
|
"application",
|
|
66
78
|
"separator"
|
|
@@ -360,7 +372,10 @@ var INPUT_TYPE_ROLE_MAP = {
|
|
|
360
372
|
"search": "textbox",
|
|
361
373
|
"tel": "textbox",
|
|
362
374
|
"url": "textbox",
|
|
363
|
-
"number"
|
|
375
|
+
// Per ARIA, <input type="number"> has an implicit role of spinbutton
|
|
376
|
+
// (VoiceOver announces it as "stepper").
|
|
377
|
+
"number": "spinbutton",
|
|
378
|
+
"range": "slider"
|
|
364
379
|
};
|
|
365
380
|
function computeRole(element) {
|
|
366
381
|
const warnings = [];
|
|
@@ -393,6 +408,11 @@ function computeImplicitRole(element, _warnings) {
|
|
|
393
408
|
if (tagName === "input") {
|
|
394
409
|
return computeInputRole(element);
|
|
395
410
|
}
|
|
411
|
+
if (tagName === "select") {
|
|
412
|
+
const multiple = element.hasAttribute("multiple");
|
|
413
|
+
const size = parseInt(element.getAttribute("size") || "0", 10);
|
|
414
|
+
return multiple || size > 1 ? "listbox" : "combobox";
|
|
415
|
+
}
|
|
396
416
|
if (tagName === "a") {
|
|
397
417
|
return element.hasAttribute("href") ? "link" : null;
|
|
398
418
|
}
|
|
@@ -796,6 +816,7 @@ function processChildNode(node, children, warnings) {
|
|
|
796
816
|
if (node.nodeType === 1) {
|
|
797
817
|
const element = node;
|
|
798
818
|
if (isHidden(element)) return;
|
|
819
|
+
if (isConsumedLabel(element)) return;
|
|
799
820
|
if (!isAccessible(element)) {
|
|
800
821
|
const grandchildren = collectChildrenFromRolelessElement(element, warnings);
|
|
801
822
|
children.push(...grandchildren);
|
|
@@ -805,18 +826,74 @@ function processChildNode(node, children, warnings) {
|
|
|
805
826
|
if (childNode) children.push(childNode);
|
|
806
827
|
}
|
|
807
828
|
}
|
|
829
|
+
function isConsumedLabel(element) {
|
|
830
|
+
if (element.tagName.toLowerCase() !== "label") return false;
|
|
831
|
+
const forId = element.getAttribute("for");
|
|
832
|
+
if (forId) {
|
|
833
|
+
const doc = element.ownerDocument;
|
|
834
|
+
const target = doc?.getElementById(forId);
|
|
835
|
+
if (target && isLabelableControl(target)) return true;
|
|
836
|
+
}
|
|
837
|
+
const wrapped = element.querySelector("input, select, textarea, button, meter, progress");
|
|
838
|
+
if (wrapped && isLabelableControl(wrapped)) return true;
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
function isLabelableControl(element) {
|
|
842
|
+
const tag = element.tagName.toLowerCase();
|
|
843
|
+
if (tag === "input") {
|
|
844
|
+
return (element.getAttribute("type") || "text").toLowerCase() !== "hidden";
|
|
845
|
+
}
|
|
846
|
+
return tag === "select" || tag === "textarea" || tag === "button" || tag === "meter" || tag === "progress";
|
|
847
|
+
}
|
|
808
848
|
function collectChildrenFromRolelessElement(element, warnings) {
|
|
849
|
+
return collectAccessibleChildren(element, warnings);
|
|
850
|
+
}
|
|
851
|
+
function collectAccessibleChildren(element, warnings) {
|
|
809
852
|
const children = [];
|
|
810
853
|
if (element.shadowRoot) {
|
|
811
854
|
for (const child of Array.from(element.shadowRoot.childNodes)) {
|
|
812
|
-
|
|
855
|
+
processShadowNode(child, children, warnings);
|
|
813
856
|
}
|
|
857
|
+
return children;
|
|
858
|
+
}
|
|
859
|
+
if (element.localName && element.localName.includes("-") && element.childNodes.length === 0) {
|
|
860
|
+
warnings.push({
|
|
861
|
+
message: `Custom element <${element.localName}> has no accessible children and may have a closed shadow root; its content could not be analyzed.`,
|
|
862
|
+
element
|
|
863
|
+
});
|
|
814
864
|
}
|
|
815
865
|
for (const child of Array.from(element.childNodes)) {
|
|
816
866
|
processChildNode(child, children, warnings);
|
|
817
867
|
}
|
|
818
868
|
return children;
|
|
819
869
|
}
|
|
870
|
+
function processShadowNode(node, children, warnings) {
|
|
871
|
+
if (node.nodeType === 1) {
|
|
872
|
+
const element = node;
|
|
873
|
+
if (element.localName === "slot") {
|
|
874
|
+
const slot = element;
|
|
875
|
+
let assigned = [];
|
|
876
|
+
if (typeof slot.assignedNodes === "function") {
|
|
877
|
+
try {
|
|
878
|
+
assigned = Array.from(slot.assignedNodes({ flatten: true }));
|
|
879
|
+
} catch {
|
|
880
|
+
assigned = [];
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (assigned.length > 0) {
|
|
884
|
+
for (const assignedNode of assigned) {
|
|
885
|
+
processChildNode(assignedNode, children, warnings);
|
|
886
|
+
}
|
|
887
|
+
} else {
|
|
888
|
+
for (const child of Array.from(slot.childNodes)) {
|
|
889
|
+
processShadowNode(child, children, warnings);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
processChildNode(node, children, warnings);
|
|
896
|
+
}
|
|
820
897
|
function buildAccessibilityTree(rootElement, sourceHash) {
|
|
821
898
|
const warnings = [];
|
|
822
899
|
if (isHidden(rootElement)) {
|
|
@@ -848,6 +925,30 @@ function buildAccessibilityTree(rootElement, sourceHash) {
|
|
|
848
925
|
};
|
|
849
926
|
return { model, warnings };
|
|
850
927
|
}
|
|
928
|
+
var SET_MEMBER_ROLES = /* @__PURE__ */ new Set([
|
|
929
|
+
"radio",
|
|
930
|
+
"option",
|
|
931
|
+
"listitem",
|
|
932
|
+
"tab"
|
|
933
|
+
]);
|
|
934
|
+
function assignImplicitSetPosition(children) {
|
|
935
|
+
const totals = /* @__PURE__ */ new Map();
|
|
936
|
+
for (const child of children) {
|
|
937
|
+
if (SET_MEMBER_ROLES.has(child.role)) {
|
|
938
|
+
totals.set(child.role, (totals.get(child.role) ?? 0) + 1);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const seen = /* @__PURE__ */ new Map();
|
|
942
|
+
for (const child of children) {
|
|
943
|
+
if (!SET_MEMBER_ROLES.has(child.role)) continue;
|
|
944
|
+
const total = totals.get(child.role);
|
|
945
|
+
if (total < 2) continue;
|
|
946
|
+
const pos = (seen.get(child.role) ?? 0) + 1;
|
|
947
|
+
seen.set(child.role, pos);
|
|
948
|
+
if (child.state.posinset === void 0) child.state.posinset = pos;
|
|
949
|
+
if (child.state.setsize === void 0) child.state.setsize = total;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
851
952
|
function buildNodeRecursive(element, warnings) {
|
|
852
953
|
if (isHidden(element)) {
|
|
853
954
|
return null;
|
|
@@ -884,15 +985,15 @@ function buildNodeRecursive(element, warnings) {
|
|
|
884
985
|
element: w.element
|
|
885
986
|
})));
|
|
886
987
|
const focusInfo = extractFocusInfo(element);
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
988
|
+
let children = collectAccessibleChildren(element, warnings);
|
|
989
|
+
const isNativeSelect = element.tagName.toLowerCase() === "select";
|
|
990
|
+
if (isNativeSelect && roleResult.role === "combobox") {
|
|
991
|
+
children = [];
|
|
992
|
+
if (stateResult.state.expanded === void 0) {
|
|
993
|
+
stateResult.state.expanded = false;
|
|
891
994
|
}
|
|
892
995
|
}
|
|
893
|
-
|
|
894
|
-
processChildNode(child, children, warnings);
|
|
895
|
-
}
|
|
996
|
+
assignImplicitSetPosition(children);
|
|
896
997
|
const node = {
|
|
897
998
|
role: roleResult.role,
|
|
898
999
|
name: nameResult.name,
|
|
@@ -905,15 +1006,7 @@ function buildNodeRecursive(element, warnings) {
|
|
|
905
1006
|
return node;
|
|
906
1007
|
}
|
|
907
1008
|
function createGenericContainer(rootElement, warnings) {
|
|
908
|
-
const children =
|
|
909
|
-
if (rootElement.shadowRoot) {
|
|
910
|
-
for (const child of Array.from(rootElement.shadowRoot.childNodes)) {
|
|
911
|
-
processChildNode(child, children, warnings);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
for (const child of Array.from(rootElement.childNodes)) {
|
|
915
|
-
processChildNode(child, children, warnings);
|
|
916
|
-
}
|
|
1009
|
+
const children = collectAccessibleChildren(rootElement, warnings);
|
|
917
1010
|
return {
|
|
918
1011
|
role: "generic",
|
|
919
1012
|
name: "",
|
|
@@ -1397,8 +1490,45 @@ function renderNodeVoiceOver(node, announcements, c) {
|
|
|
1397
1490
|
renderNodeVoiceOver(child, announcements, c);
|
|
1398
1491
|
}
|
|
1399
1492
|
}
|
|
1493
|
+
function isValueFirstWidget(role) {
|
|
1494
|
+
return role === "slider" || role === "spinbutton" || role === "combobox";
|
|
1495
|
+
}
|
|
1496
|
+
function isValueBeforeRoleWidget(role) {
|
|
1497
|
+
return role === "progressbar" || role === "meter";
|
|
1498
|
+
}
|
|
1499
|
+
function isSilentContainerVoiceOver(node) {
|
|
1500
|
+
if (node.role !== "list" && node.role !== "status") return false;
|
|
1501
|
+
if (node.name) return false;
|
|
1502
|
+
return node.children.length > 0;
|
|
1503
|
+
}
|
|
1400
1504
|
function formatNodeVoiceOver(node, c) {
|
|
1505
|
+
if (isSilentContainerVoiceOver(node)) {
|
|
1506
|
+
return "";
|
|
1507
|
+
}
|
|
1401
1508
|
const parts = [];
|
|
1509
|
+
const valueFirst = isValueFirstWidget(node.role) && node.value !== void 0;
|
|
1510
|
+
if (valueFirst) {
|
|
1511
|
+
const valueText = node.value.text || String(node.value.current);
|
|
1512
|
+
if (valueText) parts.push(valueText);
|
|
1513
|
+
if (node.name) parts.push(c.elementName(node.name));
|
|
1514
|
+
const roleText = formatRoleVoiceOver(node);
|
|
1515
|
+
if (roleText) parts.push(c.roleName(roleText));
|
|
1516
|
+
const stateText2 = formatStatesVoiceOver(node);
|
|
1517
|
+
if (stateText2) parts.push(c.stateName(stateText2));
|
|
1518
|
+
if (node.description) parts.push(c.description(node.description));
|
|
1519
|
+
return parts.join(", ");
|
|
1520
|
+
}
|
|
1521
|
+
if (isValueBeforeRoleWidget(node.role) && node.value !== void 0) {
|
|
1522
|
+
if (node.name) parts.push(c.elementName(node.name));
|
|
1523
|
+
const valueText = node.value.text || String(node.value.current);
|
|
1524
|
+
if (valueText) parts.push(valueText);
|
|
1525
|
+
const roleText = formatRoleVoiceOver(node);
|
|
1526
|
+
if (roleText) parts.push(c.roleName(roleText));
|
|
1527
|
+
const stateText2 = formatStatesVoiceOver(node);
|
|
1528
|
+
if (stateText2) parts.push(c.stateName(stateText2));
|
|
1529
|
+
if (node.description) parts.push(c.description(node.description));
|
|
1530
|
+
return parts.join(", ");
|
|
1531
|
+
}
|
|
1402
1532
|
const roleFirst = shouldAnnounceRoleFirst(node.role);
|
|
1403
1533
|
if (roleFirst) {
|
|
1404
1534
|
const roleText = formatRoleVoiceOver(node);
|
|
@@ -1427,6 +1557,9 @@ function formatNodeVoiceOver(node, c) {
|
|
|
1427
1557
|
parts.push(valueText);
|
|
1428
1558
|
}
|
|
1429
1559
|
}
|
|
1560
|
+
if (node.state.posinset !== void 0 && node.state.setsize !== void 0) {
|
|
1561
|
+
parts.push(`${node.state.posinset} of ${node.state.setsize}`);
|
|
1562
|
+
}
|
|
1430
1563
|
if (node.description) {
|
|
1431
1564
|
parts.push(c.description(node.description));
|
|
1432
1565
|
}
|
|
@@ -1462,7 +1595,7 @@ function formatRoleVoiceOver(node) {
|
|
|
1462
1595
|
case "list":
|
|
1463
1596
|
return "list";
|
|
1464
1597
|
case "listitem":
|
|
1465
|
-
return "
|
|
1598
|
+
return "";
|
|
1466
1599
|
case "navigation":
|
|
1467
1600
|
return "navigation";
|
|
1468
1601
|
case "main":
|
|
@@ -1518,6 +1651,18 @@ function formatRoleVoiceOver(node) {
|
|
|
1518
1651
|
return "progress indicator";
|
|
1519
1652
|
case "status":
|
|
1520
1653
|
return "status";
|
|
1654
|
+
case "switch":
|
|
1655
|
+
return "switch";
|
|
1656
|
+
case "slider":
|
|
1657
|
+
return "slider";
|
|
1658
|
+
case "spinbutton":
|
|
1659
|
+
return "stepper";
|
|
1660
|
+
case "tab":
|
|
1661
|
+
return "tab";
|
|
1662
|
+
case "tablist":
|
|
1663
|
+
return node.name ? "tab group" : "tab group";
|
|
1664
|
+
case "tabpanel":
|
|
1665
|
+
return "tab panel";
|
|
1521
1666
|
case "group":
|
|
1522
1667
|
return node.name ? "group" : "";
|
|
1523
1668
|
case "document":
|
|
@@ -1538,6 +1683,10 @@ function formatStatesVoiceOver(node) {
|
|
|
1538
1683
|
if (node.state.checked !== void 0) {
|
|
1539
1684
|
if (node.state.checked === "mixed") {
|
|
1540
1685
|
states.push("mixed");
|
|
1686
|
+
} else if (node.role === "switch") {
|
|
1687
|
+
states.push(node.state.checked ? "on" : "off");
|
|
1688
|
+
} else if (node.role === "radio") {
|
|
1689
|
+
if (node.state.checked) states.push("selected");
|
|
1541
1690
|
} else {
|
|
1542
1691
|
states.push(node.state.checked ? "checked" : "unchecked");
|
|
1543
1692
|
}
|
|
@@ -1546,10 +1695,10 @@ function formatStatesVoiceOver(node) {
|
|
|
1546
1695
|
if (node.state.pressed === "mixed") {
|
|
1547
1696
|
states.push("mixed");
|
|
1548
1697
|
} else {
|
|
1549
|
-
states.push(node.state.pressed ? "
|
|
1698
|
+
states.push(node.state.pressed ? "selected" : "unselected");
|
|
1550
1699
|
}
|
|
1551
1700
|
}
|
|
1552
|
-
if (node.state.selected !== void 0) {
|
|
1701
|
+
if (node.state.selected !== void 0 && node.role !== "tab") {
|
|
1553
1702
|
states.push(node.state.selected ? "selected" : "unselected");
|
|
1554
1703
|
}
|
|
1555
1704
|
if (node.state.disabled) {
|
|
@@ -1999,7 +2148,7 @@ function detectInteractiveIssues(elements, issues) {
|
|
|
1999
2148
|
});
|
|
2000
2149
|
}
|
|
2001
2150
|
|
|
2002
|
-
// src/
|
|
2151
|
+
// ../src/browser/analyze.ts
|
|
2003
2152
|
var LANDMARK_ROLES = /* @__PURE__ */ new Set([
|
|
2004
2153
|
"navigation",
|
|
2005
2154
|
"main",
|
|
@@ -2042,37 +2191,2078 @@ function convertAuditFindings(model) {
|
|
|
2042
2191
|
selector: issue.element ? `${issue.element.role}${issue.element.name ? ` "${issue.element.name}"` : ""}` : issue.suggestion || ""
|
|
2043
2192
|
}));
|
|
2044
2193
|
}
|
|
2045
|
-
function
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2194
|
+
function emptyResult(extraWarnings = []) {
|
|
2195
|
+
return {
|
|
2196
|
+
nvda: [],
|
|
2197
|
+
jaws: [],
|
|
2198
|
+
voiceover: [],
|
|
2199
|
+
narrator: [],
|
|
2200
|
+
audit: [],
|
|
2201
|
+
stats: { totalElements: 0, interactiveElements: 0, landmarks: 0, headings: 0 },
|
|
2202
|
+
warnings: [...extraWarnings]
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
function analyzeElement(root, extraWarnings = []) {
|
|
2206
|
+
if (!root) {
|
|
2207
|
+
return emptyResult(extraWarnings);
|
|
2208
|
+
}
|
|
2209
|
+
let built;
|
|
2210
|
+
try {
|
|
2211
|
+
built = buildAccessibilityTree(root);
|
|
2212
|
+
} catch (err) {
|
|
2213
|
+
return emptyResult([
|
|
2214
|
+
...extraWarnings,
|
|
2215
|
+
`Analysis failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2216
|
+
]);
|
|
2217
|
+
}
|
|
2218
|
+
const { model, warnings: treeWarnings } = built;
|
|
2219
|
+
const nvda = renderNVDA(model, false).split("\n").filter(Boolean);
|
|
2220
|
+
const jaws = renderJAWS(model, false).split("\n").filter(Boolean);
|
|
2221
|
+
const voiceover = renderVoiceOver(model, false).split("\n").filter(Boolean);
|
|
2222
|
+
const narrator = renderNarrator(model, false).split("\n").filter(Boolean);
|
|
2055
2223
|
const audit = convertAuditFindings(model);
|
|
2056
2224
|
const stats = countStats(model.root);
|
|
2057
|
-
|
|
2225
|
+
const warnings = [
|
|
2226
|
+
...extraWarnings,
|
|
2227
|
+
...treeWarnings.map((w) => w.message)
|
|
2228
|
+
];
|
|
2229
|
+
return { nvda, jaws, voiceover, narrator, audit, stats, warnings };
|
|
2058
2230
|
}
|
|
2059
2231
|
|
|
2060
|
-
// src/
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
function getChannel() {
|
|
2064
|
-
return window.__STORYBOOK_ADDONS_CHANNEL__;
|
|
2232
|
+
// src/analyzer.ts
|
|
2233
|
+
function analyzeDOM(root) {
|
|
2234
|
+
return analyzeElement(root);
|
|
2065
2235
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2236
|
+
|
|
2237
|
+
// ../src/runtime/selector.ts
|
|
2238
|
+
function generateSelector(element) {
|
|
2239
|
+
if (element.id) {
|
|
2240
|
+
return `#${escapeCSSIdentifier(element.id)}`;
|
|
2241
|
+
}
|
|
2242
|
+
const document2 = element.ownerDocument;
|
|
2243
|
+
if (!document2) {
|
|
2244
|
+
return getTagName(element);
|
|
2245
|
+
}
|
|
2246
|
+
const segments = [];
|
|
2247
|
+
let current = element;
|
|
2248
|
+
while (current && current !== document2.documentElement) {
|
|
2249
|
+
const segment = buildSegment(current, document2);
|
|
2250
|
+
segments.unshift(segment);
|
|
2251
|
+
const partialSelector = segments.join(" > ");
|
|
2252
|
+
try {
|
|
2253
|
+
const matches = document2.querySelectorAll(partialSelector);
|
|
2254
|
+
if (matches.length === 1 && matches[0] === element) {
|
|
2255
|
+
return partialSelector;
|
|
2256
|
+
}
|
|
2257
|
+
} catch {
|
|
2258
|
+
}
|
|
2259
|
+
current = current.parentElement;
|
|
2260
|
+
}
|
|
2261
|
+
if (segments.length === 0) {
|
|
2262
|
+
return getTagName(element);
|
|
2263
|
+
}
|
|
2264
|
+
const fullSelector = segments.join(" > ");
|
|
2265
|
+
return fullSelector;
|
|
2068
2266
|
}
|
|
2069
|
-
function
|
|
2070
|
-
const
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2267
|
+
function buildSegment(element, _document) {
|
|
2268
|
+
const tag = getTagName(element);
|
|
2269
|
+
const parent = element.parentElement;
|
|
2270
|
+
if (parent) {
|
|
2271
|
+
const siblingsWithSameTag = Array.from(parent.children).filter(
|
|
2272
|
+
(child) => getTagName(child) === tag
|
|
2273
|
+
);
|
|
2274
|
+
if (siblingsWithSameTag.length === 1) {
|
|
2275
|
+
return tag;
|
|
2276
|
+
}
|
|
2277
|
+
} else {
|
|
2278
|
+
return tag;
|
|
2279
|
+
}
|
|
2280
|
+
const classes = getStableClasses(element);
|
|
2281
|
+
if (classes.length > 0) {
|
|
2282
|
+
const tagWithClasses = `${tag}.${classes.join(".")}`;
|
|
2283
|
+
if (parent) {
|
|
2284
|
+
const siblingsMatchingClasses = Array.from(parent.children).filter(
|
|
2285
|
+
(child) => {
|
|
2286
|
+
if (getTagName(child) !== tag) return false;
|
|
2287
|
+
return classes.every((cls) => child.classList.contains(cls));
|
|
2288
|
+
}
|
|
2289
|
+
);
|
|
2290
|
+
if (siblingsMatchingClasses.length === 1) {
|
|
2291
|
+
return tagWithClasses;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const index = getNthChildIndex(element);
|
|
2296
|
+
return `${tag}:nth-child(${index})`;
|
|
2297
|
+
}
|
|
2298
|
+
function getTagName(element) {
|
|
2299
|
+
return element.tagName.toLowerCase();
|
|
2300
|
+
}
|
|
2301
|
+
function getStableClasses(element) {
|
|
2302
|
+
const classes = Array.from(element.classList);
|
|
2303
|
+
if (classes.length === 0) return [];
|
|
2304
|
+
const stableClasses = classes.filter((cls) => !looksLikeDynamicClass(cls));
|
|
2305
|
+
return stableClasses.sort().map(escapeCSSIdentifier);
|
|
2306
|
+
}
|
|
2307
|
+
function looksLikeDynamicClass(className) {
|
|
2308
|
+
if (/^[a-f0-9]{5,}$/i.test(className)) return true;
|
|
2309
|
+
if (/[_-][a-f0-9]{5,}$/i.test(className)) return true;
|
|
2310
|
+
if (/_{2,}[A-Za-z0-9]{4,}$/.test(className)) return true;
|
|
2311
|
+
return false;
|
|
2312
|
+
}
|
|
2313
|
+
function getNthChildIndex(element) {
|
|
2314
|
+
const parent = element.parentElement;
|
|
2315
|
+
if (!parent) return 1;
|
|
2316
|
+
const children = Array.from(parent.children);
|
|
2317
|
+
const index = children.indexOf(element);
|
|
2318
|
+
return index + 1;
|
|
2319
|
+
}
|
|
2320
|
+
function escapeCSSIdentifier(value) {
|
|
2321
|
+
if (!value) return value;
|
|
2322
|
+
let result = "";
|
|
2323
|
+
for (let i = 0; i < value.length; i++) {
|
|
2324
|
+
const char = value[i];
|
|
2325
|
+
const code = char.charCodeAt(0);
|
|
2326
|
+
if (i === 0 && code >= 48 && code <= 57) {
|
|
2327
|
+
result += `\\3${char} `;
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
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 === " ") {
|
|
2331
|
+
result += `\\${char}`;
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
result += char;
|
|
2335
|
+
}
|
|
2336
|
+
return result;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// ../src/runtime/observers/focus-observer.ts
|
|
2340
|
+
function computeAccessibleName2(element, document2) {
|
|
2341
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
2342
|
+
if (ariaLabel && ariaLabel.trim()) {
|
|
2343
|
+
return ariaLabel.trim();
|
|
2344
|
+
}
|
|
2345
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
2346
|
+
if (labelledBy && labelledBy.trim()) {
|
|
2347
|
+
const ids = labelledBy.trim().split(/\s+/);
|
|
2348
|
+
const parts = [];
|
|
2349
|
+
for (const id of ids) {
|
|
2350
|
+
const referenced = document2.getElementById(id);
|
|
2351
|
+
if (referenced && referenced.textContent) {
|
|
2352
|
+
parts.push(referenced.textContent.trim());
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
const combined = parts.join(" ");
|
|
2356
|
+
if (combined) {
|
|
2357
|
+
return combined;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const textContent = element.textContent;
|
|
2361
|
+
if (textContent && textContent.trim()) {
|
|
2362
|
+
return textContent.trim();
|
|
2363
|
+
}
|
|
2364
|
+
return "";
|
|
2365
|
+
}
|
|
2366
|
+
function getElementRole(element) {
|
|
2367
|
+
const explicitRole = element.getAttribute("role");
|
|
2368
|
+
if (explicitRole && explicitRole.trim()) {
|
|
2369
|
+
return explicitRole.trim();
|
|
2370
|
+
}
|
|
2371
|
+
const tagName = element.tagName.toLowerCase();
|
|
2372
|
+
switch (tagName) {
|
|
2373
|
+
case "button":
|
|
2374
|
+
return "button";
|
|
2375
|
+
case "a":
|
|
2376
|
+
return element.hasAttribute("href") ? "link" : "generic";
|
|
2377
|
+
case "input": {
|
|
2378
|
+
const type = (element.getAttribute("type") || "text").toLowerCase();
|
|
2379
|
+
switch (type) {
|
|
2380
|
+
case "button":
|
|
2381
|
+
case "submit":
|
|
2382
|
+
case "reset":
|
|
2383
|
+
case "image":
|
|
2384
|
+
return "button";
|
|
2385
|
+
case "checkbox":
|
|
2386
|
+
return "checkbox";
|
|
2387
|
+
case "radio":
|
|
2388
|
+
return "radio";
|
|
2389
|
+
case "range":
|
|
2390
|
+
return "slider";
|
|
2391
|
+
case "search":
|
|
2392
|
+
return "searchbox";
|
|
2393
|
+
default:
|
|
2394
|
+
return "textbox";
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
case "textarea":
|
|
2398
|
+
return "textbox";
|
|
2399
|
+
case "select":
|
|
2400
|
+
return element.hasAttribute("multiple") ? "listbox" : "combobox";
|
|
2401
|
+
case "nav":
|
|
2402
|
+
return "navigation";
|
|
2403
|
+
case "main":
|
|
2404
|
+
return "main";
|
|
2405
|
+
case "header":
|
|
2406
|
+
return "banner";
|
|
2407
|
+
case "footer":
|
|
2408
|
+
return "contentinfo";
|
|
2409
|
+
case "aside":
|
|
2410
|
+
return "complementary";
|
|
2411
|
+
case "form":
|
|
2412
|
+
return "form";
|
|
2413
|
+
case "section":
|
|
2414
|
+
return element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby") ? "region" : "generic";
|
|
2415
|
+
case "article":
|
|
2416
|
+
return "article";
|
|
2417
|
+
case "dialog":
|
|
2418
|
+
return "dialog";
|
|
2419
|
+
case "table":
|
|
2420
|
+
return "table";
|
|
2421
|
+
case "ul":
|
|
2422
|
+
case "ol":
|
|
2423
|
+
return "list";
|
|
2424
|
+
case "li":
|
|
2425
|
+
return "listitem";
|
|
2426
|
+
case "img":
|
|
2427
|
+
return element.getAttribute("alt") === "" ? "presentation" : "img";
|
|
2428
|
+
case "h1":
|
|
2429
|
+
case "h2":
|
|
2430
|
+
case "h3":
|
|
2431
|
+
case "h4":
|
|
2432
|
+
case "h5":
|
|
2433
|
+
case "h6":
|
|
2434
|
+
return "heading";
|
|
2435
|
+
case "option":
|
|
2436
|
+
return "option";
|
|
2437
|
+
case "progress":
|
|
2438
|
+
return "progressbar";
|
|
2439
|
+
case "output":
|
|
2440
|
+
return "status";
|
|
2441
|
+
case "menu":
|
|
2442
|
+
return "menu";
|
|
2443
|
+
default:
|
|
2444
|
+
return "generic";
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
function createFocusObserver(options) {
|
|
2448
|
+
const { document: doc, onEvent, getTimestamp } = options;
|
|
2449
|
+
let active = false;
|
|
2450
|
+
let previousTarget = null;
|
|
2451
|
+
function buildEventTarget2(element) {
|
|
2452
|
+
return {
|
|
2453
|
+
role: getElementRole(element),
|
|
2454
|
+
accessibleName: computeAccessibleName2(element, doc),
|
|
2455
|
+
selector: generateSelector(element)
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
function handleFocusIn(event) {
|
|
2459
|
+
if (!active) return;
|
|
2460
|
+
const target = event.target;
|
|
2461
|
+
if (!target || !(target instanceof doc.defaultView.Element)) {
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
const currentTarget = buildEventTarget2(target);
|
|
2465
|
+
const a11yEvent = {
|
|
2466
|
+
type: "FOCUS_CHANGED",
|
|
2467
|
+
timestamp: getTimestamp(),
|
|
2468
|
+
target: currentTarget,
|
|
2469
|
+
payload: {
|
|
2470
|
+
kind: "focus_changed",
|
|
2471
|
+
previousTarget
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
onEvent(a11yEvent);
|
|
2475
|
+
previousTarget = currentTarget;
|
|
2476
|
+
}
|
|
2477
|
+
return {
|
|
2478
|
+
start() {
|
|
2479
|
+
if (active) return;
|
|
2480
|
+
active = true;
|
|
2481
|
+
previousTarget = null;
|
|
2482
|
+
doc.addEventListener("focusin", handleFocusIn);
|
|
2483
|
+
},
|
|
2484
|
+
stop() {
|
|
2485
|
+
if (!active) return;
|
|
2486
|
+
active = false;
|
|
2487
|
+
doc.removeEventListener("focusin", handleFocusIn);
|
|
2488
|
+
},
|
|
2489
|
+
get isActive() {
|
|
2490
|
+
return active;
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
// ../src/runtime/observers/mutation-observer.ts
|
|
2496
|
+
var ROLE_ATTRIBUTE = "role";
|
|
2497
|
+
var NAME_ATTRIBUTES = /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]);
|
|
2498
|
+
var STATE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
2499
|
+
"aria-expanded",
|
|
2500
|
+
"aria-selected",
|
|
2501
|
+
"aria-checked",
|
|
2502
|
+
"aria-disabled",
|
|
2503
|
+
"aria-hidden"
|
|
2504
|
+
]);
|
|
2505
|
+
var OBSERVED_ATTRIBUTES = [
|
|
2506
|
+
ROLE_ATTRIBUTE,
|
|
2507
|
+
"aria-label",
|
|
2508
|
+
"aria-labelledby",
|
|
2509
|
+
"aria-expanded",
|
|
2510
|
+
"aria-selected",
|
|
2511
|
+
"aria-checked",
|
|
2512
|
+
"aria-disabled",
|
|
2513
|
+
"aria-hidden"
|
|
2514
|
+
];
|
|
2515
|
+
function computeAccessibleName3(element, document2) {
|
|
2516
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
2517
|
+
if (ariaLabel && ariaLabel.trim()) {
|
|
2518
|
+
return ariaLabel.trim();
|
|
2519
|
+
}
|
|
2520
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
2521
|
+
if (labelledBy && labelledBy.trim()) {
|
|
2522
|
+
const ids = labelledBy.trim().split(/\s+/);
|
|
2523
|
+
const parts = [];
|
|
2524
|
+
for (const id of ids) {
|
|
2525
|
+
const referenced = document2.getElementById(id);
|
|
2526
|
+
if (referenced && referenced.textContent) {
|
|
2527
|
+
parts.push(referenced.textContent.trim());
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
const combined = parts.join(" ");
|
|
2531
|
+
if (combined) {
|
|
2532
|
+
return combined;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
const textContent = element.textContent;
|
|
2536
|
+
if (textContent && textContent.trim()) {
|
|
2537
|
+
return textContent.trim();
|
|
2538
|
+
}
|
|
2539
|
+
return "";
|
|
2540
|
+
}
|
|
2541
|
+
function getElementRole2(element) {
|
|
2542
|
+
const explicitRole = element.getAttribute("role");
|
|
2543
|
+
if (explicitRole && explicitRole.trim()) {
|
|
2544
|
+
return explicitRole.trim();
|
|
2545
|
+
}
|
|
2546
|
+
const tagName = element.tagName.toLowerCase();
|
|
2547
|
+
switch (tagName) {
|
|
2548
|
+
case "button":
|
|
2549
|
+
return "button";
|
|
2550
|
+
case "a":
|
|
2551
|
+
return element.hasAttribute("href") ? "link" : "generic";
|
|
2552
|
+
case "input": {
|
|
2553
|
+
const type = (element.getAttribute("type") || "text").toLowerCase();
|
|
2554
|
+
switch (type) {
|
|
2555
|
+
case "button":
|
|
2556
|
+
case "submit":
|
|
2557
|
+
case "reset":
|
|
2558
|
+
case "image":
|
|
2559
|
+
return "button";
|
|
2560
|
+
case "checkbox":
|
|
2561
|
+
return "checkbox";
|
|
2562
|
+
case "radio":
|
|
2563
|
+
return "radio";
|
|
2564
|
+
case "range":
|
|
2565
|
+
return "slider";
|
|
2566
|
+
case "search":
|
|
2567
|
+
return "searchbox";
|
|
2568
|
+
default:
|
|
2569
|
+
return "textbox";
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
case "textarea":
|
|
2573
|
+
return "textbox";
|
|
2574
|
+
case "select":
|
|
2575
|
+
return element.hasAttribute("multiple") ? "listbox" : "combobox";
|
|
2576
|
+
case "nav":
|
|
2577
|
+
return "navigation";
|
|
2578
|
+
case "main":
|
|
2579
|
+
return "main";
|
|
2580
|
+
case "header":
|
|
2581
|
+
return "banner";
|
|
2582
|
+
case "footer":
|
|
2583
|
+
return "contentinfo";
|
|
2584
|
+
case "aside":
|
|
2585
|
+
return "complementary";
|
|
2586
|
+
case "form":
|
|
2587
|
+
return "form";
|
|
2588
|
+
case "section":
|
|
2589
|
+
return element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby") ? "region" : "generic";
|
|
2590
|
+
case "article":
|
|
2591
|
+
return "article";
|
|
2592
|
+
case "dialog":
|
|
2593
|
+
return "dialog";
|
|
2594
|
+
case "table":
|
|
2595
|
+
return "table";
|
|
2596
|
+
case "ul":
|
|
2597
|
+
case "ol":
|
|
2598
|
+
return "list";
|
|
2599
|
+
case "li":
|
|
2600
|
+
return "listitem";
|
|
2601
|
+
case "img":
|
|
2602
|
+
return element.getAttribute("alt") === "" ? "presentation" : "img";
|
|
2603
|
+
case "h1":
|
|
2604
|
+
case "h2":
|
|
2605
|
+
case "h3":
|
|
2606
|
+
case "h4":
|
|
2607
|
+
case "h5":
|
|
2608
|
+
case "h6":
|
|
2609
|
+
return "heading";
|
|
2610
|
+
case "option":
|
|
2611
|
+
return "option";
|
|
2612
|
+
case "progress":
|
|
2613
|
+
return "progressbar";
|
|
2614
|
+
case "output":
|
|
2615
|
+
return "status";
|
|
2616
|
+
case "menu":
|
|
2617
|
+
return "menu";
|
|
2618
|
+
default:
|
|
2619
|
+
return "generic";
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
function createMutationObserver(options) {
|
|
2623
|
+
const { document: doc, onEvent, getTimestamp } = options;
|
|
2624
|
+
let observer = null;
|
|
2625
|
+
let active = false;
|
|
2626
|
+
function handleMutations(mutations) {
|
|
2627
|
+
if (!active) return;
|
|
2628
|
+
for (const mutation of mutations) {
|
|
2629
|
+
if (mutation.type !== "attributes") {
|
|
2630
|
+
continue;
|
|
2631
|
+
}
|
|
2632
|
+
const attributeName = mutation.attributeName;
|
|
2633
|
+
if (!attributeName) {
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2636
|
+
const target = mutation.target;
|
|
2637
|
+
if (!(target instanceof Element)) {
|
|
2638
|
+
continue;
|
|
2639
|
+
}
|
|
2640
|
+
if (attributeName === ROLE_ATTRIBUTE) {
|
|
2641
|
+
handleRoleChange(target, mutation.oldValue);
|
|
2642
|
+
} else if (NAME_ATTRIBUTES.has(attributeName)) {
|
|
2643
|
+
handleNameChange(target, attributeName, mutation.oldValue);
|
|
2644
|
+
} else if (STATE_ATTRIBUTES.has(attributeName)) {
|
|
2645
|
+
handleStateChange(target, attributeName, mutation.oldValue);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
function handleRoleChange(element, oldValue) {
|
|
2650
|
+
const previousRole = oldValue?.trim() || getImplicitRole(element);
|
|
2651
|
+
const newRole = element.getAttribute("role")?.trim() || getImplicitRole(element);
|
|
2652
|
+
if (previousRole === newRole) {
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
const eventTarget = buildEventTarget2(element);
|
|
2656
|
+
const payload = {
|
|
2657
|
+
kind: "role_changed",
|
|
2658
|
+
previousRole,
|
|
2659
|
+
newRole
|
|
2660
|
+
};
|
|
2661
|
+
onEvent({
|
|
2662
|
+
type: "ROLE_CHANGED",
|
|
2663
|
+
timestamp: getTimestamp(),
|
|
2664
|
+
target: eventTarget,
|
|
2665
|
+
payload
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
function handleNameChange(element, attributeName, oldValue) {
|
|
2669
|
+
let previousName;
|
|
2670
|
+
let newName;
|
|
2671
|
+
if (attributeName === "aria-label") {
|
|
2672
|
+
previousName = oldValue?.trim() || "";
|
|
2673
|
+
newName = computeAccessibleName3(element, doc);
|
|
2674
|
+
if (!previousName && oldValue === null) {
|
|
2675
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
2676
|
+
if (labelledBy) {
|
|
2677
|
+
previousName = resolveAriaLabelledBy(element, labelledBy);
|
|
2678
|
+
} else {
|
|
2679
|
+
previousName = element.textContent?.trim() || "";
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
} else {
|
|
2683
|
+
previousName = oldValue ? resolveAriaLabelledBy(element, oldValue) : "";
|
|
2684
|
+
if (!previousName && oldValue === null) {
|
|
2685
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
2686
|
+
if (ariaLabel) {
|
|
2687
|
+
previousName = ariaLabel.trim();
|
|
2688
|
+
} else {
|
|
2689
|
+
previousName = element.textContent?.trim() || "";
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
newName = computeAccessibleName3(element, doc);
|
|
2693
|
+
}
|
|
2694
|
+
if (previousName === newName) {
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const eventTarget = buildEventTarget2(element);
|
|
2698
|
+
const payload = {
|
|
2699
|
+
kind: "accessible_name_changed",
|
|
2700
|
+
previousName,
|
|
2701
|
+
newName
|
|
2702
|
+
};
|
|
2703
|
+
onEvent({
|
|
2704
|
+
type: "ACCESSIBLE_NAME_CHANGED",
|
|
2705
|
+
timestamp: getTimestamp(),
|
|
2706
|
+
target: eventTarget,
|
|
2707
|
+
payload
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
function handleStateChange(element, attributeName, oldValue) {
|
|
2711
|
+
const currentValue = element.getAttribute(attributeName);
|
|
2712
|
+
const normalizedOld = normalizeStateValue(oldValue);
|
|
2713
|
+
const normalizedNew = normalizeStateValue(currentValue);
|
|
2714
|
+
if (normalizedOld === normalizedNew) {
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
const eventTarget = buildEventTarget2(element);
|
|
2718
|
+
const payload = {
|
|
2719
|
+
kind: "state_changed",
|
|
2720
|
+
attribute: attributeName,
|
|
2721
|
+
previousValue: normalizedOld,
|
|
2722
|
+
newValue: normalizedNew
|
|
2723
|
+
};
|
|
2724
|
+
onEvent({
|
|
2725
|
+
type: "STATE_CHANGED",
|
|
2726
|
+
timestamp: getTimestamp(),
|
|
2727
|
+
target: eventTarget,
|
|
2728
|
+
payload
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
function buildEventTarget2(element) {
|
|
2732
|
+
return {
|
|
2733
|
+
role: getElementRole2(element),
|
|
2734
|
+
accessibleName: computeAccessibleName3(element, doc),
|
|
2735
|
+
selector: generateSelector(element)
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
function getImplicitRole(element) {
|
|
2739
|
+
const tagName = element.tagName.toLowerCase();
|
|
2740
|
+
switch (tagName) {
|
|
2741
|
+
case "button":
|
|
2742
|
+
return "button";
|
|
2743
|
+
case "a":
|
|
2744
|
+
return element.hasAttribute("href") ? "link" : "generic";
|
|
2745
|
+
case "input": {
|
|
2746
|
+
const type = (element.getAttribute("type") || "text").toLowerCase();
|
|
2747
|
+
switch (type) {
|
|
2748
|
+
case "button":
|
|
2749
|
+
case "submit":
|
|
2750
|
+
case "reset":
|
|
2751
|
+
case "image":
|
|
2752
|
+
return "button";
|
|
2753
|
+
case "checkbox":
|
|
2754
|
+
return "checkbox";
|
|
2755
|
+
case "radio":
|
|
2756
|
+
return "radio";
|
|
2757
|
+
default:
|
|
2758
|
+
return "textbox";
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
default:
|
|
2762
|
+
return "generic";
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
function resolveAriaLabelledBy(element, idRefs) {
|
|
2766
|
+
const ownerDoc = element.ownerDocument;
|
|
2767
|
+
if (!ownerDoc) {
|
|
2768
|
+
return "";
|
|
2769
|
+
}
|
|
2770
|
+
const ids = idRefs.trim().split(/\s+/).filter(Boolean);
|
|
2771
|
+
const parts = [];
|
|
2772
|
+
for (const id of ids) {
|
|
2773
|
+
const referenced = ownerDoc.getElementById(id);
|
|
2774
|
+
if (referenced && referenced.textContent) {
|
|
2775
|
+
parts.push(referenced.textContent.trim());
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
return parts.join(" ");
|
|
2779
|
+
}
|
|
2780
|
+
function normalizeStateValue(value) {
|
|
2781
|
+
if (value === null) {
|
|
2782
|
+
return null;
|
|
2783
|
+
}
|
|
2784
|
+
if (value === "true") {
|
|
2785
|
+
return true;
|
|
2786
|
+
}
|
|
2787
|
+
if (value === "false") {
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
return value;
|
|
2791
|
+
}
|
|
2792
|
+
return {
|
|
2793
|
+
start() {
|
|
2794
|
+
if (active) return;
|
|
2795
|
+
active = true;
|
|
2796
|
+
observer = new MutationObserver(handleMutations);
|
|
2797
|
+
observer.observe(doc.body ?? doc.documentElement, {
|
|
2798
|
+
attributes: true,
|
|
2799
|
+
attributeOldValue: true,
|
|
2800
|
+
attributeFilter: OBSERVED_ATTRIBUTES,
|
|
2801
|
+
subtree: true
|
|
2802
|
+
});
|
|
2803
|
+
},
|
|
2804
|
+
stop() {
|
|
2805
|
+
if (!active) return;
|
|
2806
|
+
active = false;
|
|
2807
|
+
if (observer) {
|
|
2808
|
+
observer.disconnect();
|
|
2809
|
+
observer = null;
|
|
2810
|
+
}
|
|
2811
|
+
},
|
|
2812
|
+
get isActive() {
|
|
2813
|
+
return active;
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// ../src/runtime/observers/live-region-observer.ts
|
|
2819
|
+
var IMPLICIT_LIVE_ROLES = /* @__PURE__ */ new Set(["alert", "status", "log", "marquee", "timer"]);
|
|
2820
|
+
var ROLE_POLITENESS_MAP = {
|
|
2821
|
+
alert: "assertive",
|
|
2822
|
+
status: "polite",
|
|
2823
|
+
log: "polite",
|
|
2824
|
+
marquee: "polite",
|
|
2825
|
+
timer: "polite"
|
|
2826
|
+
};
|
|
2827
|
+
function createLiveRegionObserver(document2, callback) {
|
|
2828
|
+
let contentObserver = null;
|
|
2829
|
+
let documentObserver = null;
|
|
2830
|
+
const trackedRegions = /* @__PURE__ */ new Set();
|
|
2831
|
+
function getLiveRegionPoliteness(element) {
|
|
2832
|
+
const ariaLive = element.getAttribute("aria-live");
|
|
2833
|
+
if (ariaLive === "assertive") return "assertive";
|
|
2834
|
+
if (ariaLive === "polite") return "polite";
|
|
2835
|
+
if (ariaLive === "off") return null;
|
|
2836
|
+
const role = element.getAttribute("role");
|
|
2837
|
+
if (role && IMPLICIT_LIVE_ROLES.has(role)) {
|
|
2838
|
+
return ROLE_POLITENESS_MAP[role] || "polite";
|
|
2839
|
+
}
|
|
2840
|
+
if (ariaLive && ariaLive !== "off") return "polite";
|
|
2841
|
+
return null;
|
|
2842
|
+
}
|
|
2843
|
+
function isAtomic(element) {
|
|
2844
|
+
return element.getAttribute("aria-atomic") === "true";
|
|
2845
|
+
}
|
|
2846
|
+
function buildEventTarget2(element) {
|
|
2847
|
+
const role = element.getAttribute("role") || "region";
|
|
2848
|
+
const accessibleName = element.getAttribute("aria-label") || element.getAttribute("aria-labelledby") || "";
|
|
2849
|
+
const selector = generateSelector(element);
|
|
2850
|
+
return { role, accessibleName, selector };
|
|
2851
|
+
}
|
|
2852
|
+
function getTextFromNodes(nodes) {
|
|
2853
|
+
const parts = [];
|
|
2854
|
+
nodes.forEach((node) => {
|
|
2855
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
2856
|
+
const text = node.textContent?.trim();
|
|
2857
|
+
if (text) parts.push(text);
|
|
2858
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
2859
|
+
const text = node.textContent?.trim();
|
|
2860
|
+
if (text) parts.push(text);
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
return parts.join(" ").trim();
|
|
2864
|
+
}
|
|
2865
|
+
function emitAnnouncement(liveRegion, text, politeness) {
|
|
2866
|
+
if (!text) return;
|
|
2867
|
+
const target = buildEventTarget2(liveRegion);
|
|
2868
|
+
callback({
|
|
2869
|
+
type: "ANNOUNCEMENT",
|
|
2870
|
+
target,
|
|
2871
|
+
payload: {
|
|
2872
|
+
kind: "announcement",
|
|
2873
|
+
politeness,
|
|
2874
|
+
text
|
|
2875
|
+
}
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
function findLiveRegionAncestor(node) {
|
|
2879
|
+
let current = node;
|
|
2880
|
+
while (current) {
|
|
2881
|
+
if (current.nodeType === Node.ELEMENT_NODE && trackedRegions.has(current)) {
|
|
2882
|
+
return current;
|
|
2883
|
+
}
|
|
2884
|
+
current = current.parentNode;
|
|
2885
|
+
}
|
|
2886
|
+
return null;
|
|
2887
|
+
}
|
|
2888
|
+
function handleContentMutations(mutations) {
|
|
2889
|
+
const regionChanges = /* @__PURE__ */ new Map();
|
|
2890
|
+
for (const mutation of mutations) {
|
|
2891
|
+
const liveRegion = findLiveRegionAncestor(mutation.target);
|
|
2892
|
+
if (!liveRegion) continue;
|
|
2893
|
+
if (!regionChanges.has(liveRegion)) {
|
|
2894
|
+
regionChanges.set(liveRegion, { addedText: [], charDataChanged: false });
|
|
2895
|
+
}
|
|
2896
|
+
const changes = regionChanges.get(liveRegion);
|
|
2897
|
+
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
|
2898
|
+
const text = getTextFromNodes(mutation.addedNodes);
|
|
2899
|
+
if (text) {
|
|
2900
|
+
changes.addedText.push(text);
|
|
2901
|
+
}
|
|
2902
|
+
} else if (mutation.type === "characterData") {
|
|
2903
|
+
changes.charDataChanged = true;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
for (const [region, changes] of regionChanges) {
|
|
2907
|
+
const politeness = getLiveRegionPoliteness(region);
|
|
2908
|
+
if (!politeness) continue;
|
|
2909
|
+
let text;
|
|
2910
|
+
if (isAtomic(region)) {
|
|
2911
|
+
text = region.textContent?.trim() || "";
|
|
2912
|
+
} else {
|
|
2913
|
+
if (changes.addedText.length > 0) {
|
|
2914
|
+
text = changes.addedText.join(" ").trim();
|
|
2915
|
+
} else if (changes.charDataChanged) {
|
|
2916
|
+
text = region.textContent?.trim() || "";
|
|
2917
|
+
} else {
|
|
2918
|
+
text = "";
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
emitAnnouncement(region, text, politeness);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
function handleDocumentMutations(mutations) {
|
|
2925
|
+
for (const mutation of mutations) {
|
|
2926
|
+
if (mutation.type !== "childList") continue;
|
|
2927
|
+
mutation.addedNodes.forEach((node) => {
|
|
2928
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
2929
|
+
const element = node;
|
|
2930
|
+
if (getLiveRegionPoliteness(element) !== null && !trackedRegions.has(element)) {
|
|
2931
|
+
trackRegion(element);
|
|
2932
|
+
}
|
|
2933
|
+
const descendants = element.querySelectorAll("[aria-live], [role]");
|
|
2934
|
+
descendants.forEach((desc) => {
|
|
2935
|
+
if (getLiveRegionPoliteness(desc) !== null && !trackedRegions.has(desc)) {
|
|
2936
|
+
trackRegion(desc);
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
});
|
|
2940
|
+
mutation.removedNodes.forEach((node) => {
|
|
2941
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
2942
|
+
const element = node;
|
|
2943
|
+
if (trackedRegions.has(element)) {
|
|
2944
|
+
trackedRegions.delete(element);
|
|
2945
|
+
}
|
|
2946
|
+
const descendants = element.querySelectorAll("[aria-live], [role]");
|
|
2947
|
+
descendants.forEach((desc) => {
|
|
2948
|
+
trackedRegions.delete(desc);
|
|
2949
|
+
});
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
function trackRegion(element) {
|
|
2954
|
+
trackedRegions.add(element);
|
|
2955
|
+
}
|
|
2956
|
+
function discoverExistingRegions() {
|
|
2957
|
+
const ariaLiveElements = document2.querySelectorAll("[aria-live]");
|
|
2958
|
+
ariaLiveElements.forEach((el) => {
|
|
2959
|
+
if (getLiveRegionPoliteness(el) !== null) {
|
|
2960
|
+
trackRegion(el);
|
|
2961
|
+
}
|
|
2962
|
+
});
|
|
2963
|
+
IMPLICIT_LIVE_ROLES.forEach((role) => {
|
|
2964
|
+
const roleElements = document2.querySelectorAll(`[role="${role}"]`);
|
|
2965
|
+
roleElements.forEach((el) => {
|
|
2966
|
+
if (!trackedRegions.has(el)) {
|
|
2967
|
+
trackRegion(el);
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
function start() {
|
|
2973
|
+
discoverExistingRegions();
|
|
2974
|
+
contentObserver = new MutationObserver(handleContentMutations);
|
|
2975
|
+
const body = document2.body;
|
|
2976
|
+
if (body) {
|
|
2977
|
+
contentObserver.observe(body, {
|
|
2978
|
+
childList: true,
|
|
2979
|
+
characterData: true,
|
|
2980
|
+
subtree: true,
|
|
2981
|
+
characterDataOldValue: true
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
documentObserver = new MutationObserver(handleDocumentMutations);
|
|
2985
|
+
if (body) {
|
|
2986
|
+
documentObserver.observe(body, {
|
|
2987
|
+
childList: true,
|
|
2988
|
+
subtree: true
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
function stop() {
|
|
2993
|
+
if (contentObserver) {
|
|
2994
|
+
contentObserver.disconnect();
|
|
2995
|
+
contentObserver = null;
|
|
2996
|
+
}
|
|
2997
|
+
if (documentObserver) {
|
|
2998
|
+
documentObserver.disconnect();
|
|
2999
|
+
documentObserver = null;
|
|
3000
|
+
}
|
|
3001
|
+
trackedRegions.clear();
|
|
3002
|
+
}
|
|
3003
|
+
return { start, stop };
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
// ../src/runtime/observers/dialog-observer.ts
|
|
3007
|
+
function computeDialogAccessibleName(element, document2) {
|
|
3008
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
3009
|
+
if (ariaLabel && ariaLabel.trim()) {
|
|
3010
|
+
return ariaLabel.trim();
|
|
3011
|
+
}
|
|
3012
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
3013
|
+
if (labelledBy && labelledBy.trim()) {
|
|
3014
|
+
const ids = labelledBy.trim().split(/\s+/);
|
|
3015
|
+
const parts = [];
|
|
3016
|
+
for (const id of ids) {
|
|
3017
|
+
const referenced = document2.getElementById(id);
|
|
3018
|
+
if (referenced && referenced.textContent) {
|
|
3019
|
+
parts.push(referenced.textContent.trim());
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
const combined = parts.join(" ");
|
|
3023
|
+
if (combined) {
|
|
3024
|
+
return combined;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
const title = element.getAttribute("title");
|
|
3028
|
+
if (title && title.trim()) {
|
|
3029
|
+
return title.trim();
|
|
3030
|
+
}
|
|
3031
|
+
const heading = element.querySelector("h1, h2, h3, h4, h5, h6");
|
|
3032
|
+
if (heading && heading.textContent && heading.textContent.trim()) {
|
|
3033
|
+
return heading.textContent.trim();
|
|
3034
|
+
}
|
|
3035
|
+
return "";
|
|
3036
|
+
}
|
|
3037
|
+
function getDialogRole(element) {
|
|
3038
|
+
const explicitRole = element.getAttribute("role");
|
|
3039
|
+
if (explicitRole && explicitRole.trim()) {
|
|
3040
|
+
return explicitRole.trim();
|
|
3041
|
+
}
|
|
3042
|
+
if (element.tagName.toLowerCase() === "dialog") {
|
|
3043
|
+
return "dialog";
|
|
3044
|
+
}
|
|
3045
|
+
return "generic";
|
|
3046
|
+
}
|
|
3047
|
+
function isDialogElement(element) {
|
|
3048
|
+
if (element.tagName.toLowerCase() === "dialog") {
|
|
3049
|
+
return true;
|
|
3050
|
+
}
|
|
3051
|
+
const role = element.getAttribute("role");
|
|
3052
|
+
return role === "dialog" || role === "alertdialog";
|
|
3053
|
+
}
|
|
3054
|
+
function isDialogModal(element) {
|
|
3055
|
+
const ariaModal = element.getAttribute("aria-modal");
|
|
3056
|
+
if (ariaModal === "true") {
|
|
3057
|
+
return true;
|
|
3058
|
+
}
|
|
3059
|
+
return false;
|
|
3060
|
+
}
|
|
3061
|
+
function createDialogObserver(options) {
|
|
3062
|
+
const { document: doc, onEvent, getTimestamp } = options;
|
|
3063
|
+
let active = false;
|
|
3064
|
+
let mutationObserver = null;
|
|
3065
|
+
const openDialogStack = [];
|
|
3066
|
+
function buildEventTarget2(element) {
|
|
3067
|
+
return {
|
|
3068
|
+
role: getDialogRole(element),
|
|
3069
|
+
accessibleName: computeDialogAccessibleName(element, doc),
|
|
3070
|
+
selector: generateSelector(element)
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
function emitDialogOpened(element) {
|
|
3074
|
+
const name = computeDialogAccessibleName(element, doc);
|
|
3075
|
+
const isModal = isDialogModal(element);
|
|
3076
|
+
const target = buildEventTarget2(element);
|
|
3077
|
+
openDialogStack.push({
|
|
3078
|
+
element,
|
|
3079
|
+
name,
|
|
3080
|
+
selector: target.selector
|
|
3081
|
+
});
|
|
3082
|
+
const a11yEvent = {
|
|
3083
|
+
type: "DIALOG_OPENED",
|
|
3084
|
+
timestamp: getTimestamp(),
|
|
3085
|
+
target,
|
|
3086
|
+
payload: {
|
|
3087
|
+
kind: "dialog_opened",
|
|
3088
|
+
dialogName: name,
|
|
3089
|
+
isModal
|
|
3090
|
+
}
|
|
3091
|
+
};
|
|
3092
|
+
onEvent(a11yEvent);
|
|
3093
|
+
}
|
|
3094
|
+
function emitDialogClosed(element) {
|
|
3095
|
+
const name = computeDialogAccessibleName(element, doc);
|
|
3096
|
+
const target = buildEventTarget2(element);
|
|
3097
|
+
const index = openDialogStack.findIndex((d) => d.element === element);
|
|
3098
|
+
if (index !== -1) {
|
|
3099
|
+
openDialogStack.splice(index, 1);
|
|
3100
|
+
}
|
|
3101
|
+
const a11yEvent = {
|
|
3102
|
+
type: "DIALOG_CLOSED",
|
|
3103
|
+
timestamp: getTimestamp(),
|
|
3104
|
+
target,
|
|
3105
|
+
payload: {
|
|
3106
|
+
kind: "dialog_closed",
|
|
3107
|
+
dialogName: name
|
|
3108
|
+
}
|
|
3109
|
+
};
|
|
3110
|
+
onEvent(a11yEvent);
|
|
3111
|
+
}
|
|
3112
|
+
function isTrackedAsOpen(element) {
|
|
3113
|
+
return openDialogStack.some((d) => d.element === element);
|
|
3114
|
+
}
|
|
3115
|
+
function handleMutations(mutations) {
|
|
3116
|
+
if (!active) return;
|
|
3117
|
+
for (const mutation of mutations) {
|
|
3118
|
+
if (mutation.type === "attributes") {
|
|
3119
|
+
const target = mutation.target;
|
|
3120
|
+
if (!(target instanceof doc.defaultView.Element)) continue;
|
|
3121
|
+
if (mutation.attributeName === "open" && target.tagName.toLowerCase() === "dialog") {
|
|
3122
|
+
if (target.hasAttribute("open")) {
|
|
3123
|
+
if (!isTrackedAsOpen(target)) {
|
|
3124
|
+
emitDialogOpened(target);
|
|
3125
|
+
}
|
|
3126
|
+
} else {
|
|
3127
|
+
if (isTrackedAsOpen(target)) {
|
|
3128
|
+
emitDialogClosed(target);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
if (mutation.attributeName === "aria-modal" && isDialogElement(target)) {
|
|
3133
|
+
if (!isTrackedAsOpen(target) && isVisibleDialog(target)) {
|
|
3134
|
+
emitDialogOpened(target);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
if (mutation.attributeName === "role") {
|
|
3138
|
+
const oldRole = mutation.oldValue;
|
|
3139
|
+
const newRole = target.getAttribute("role");
|
|
3140
|
+
const wasDialog = oldRole === "dialog" || oldRole === "alertdialog";
|
|
3141
|
+
const isNowDialog = newRole === "dialog" || newRole === "alertdialog";
|
|
3142
|
+
if (!wasDialog && isNowDialog && isVisibleDialog(target)) {
|
|
3143
|
+
emitDialogOpened(target);
|
|
3144
|
+
} else if (wasDialog && !isNowDialog && isTrackedAsOpen(target)) {
|
|
3145
|
+
emitDialogClosed(target);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
if (mutation.type === "childList") {
|
|
3150
|
+
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
3151
|
+
const node = mutation.addedNodes[i];
|
|
3152
|
+
if (!(node instanceof doc.defaultView.Element)) continue;
|
|
3153
|
+
if (isDialogElement(node) && isVisibleDialog(node)) {
|
|
3154
|
+
if (!isTrackedAsOpen(node)) {
|
|
3155
|
+
emitDialogOpened(node);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
const dialogDescendants = node.querySelectorAll(
|
|
3159
|
+
'dialog[open], [role="dialog"], [role="alertdialog"]'
|
|
3160
|
+
);
|
|
3161
|
+
for (let j = 0; j < dialogDescendants.length; j++) {
|
|
3162
|
+
const dialog = dialogDescendants[j];
|
|
3163
|
+
if (isVisibleDialog(dialog) && !isTrackedAsOpen(dialog)) {
|
|
3164
|
+
emitDialogOpened(dialog);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
for (let i = 0; i < mutation.removedNodes.length; i++) {
|
|
3169
|
+
const node = mutation.removedNodes[i];
|
|
3170
|
+
if (!(node instanceof doc.defaultView.Element)) continue;
|
|
3171
|
+
if (isTrackedAsOpen(node)) {
|
|
3172
|
+
emitDialogClosed(node);
|
|
3173
|
+
}
|
|
3174
|
+
const trackedInRemoved = openDialogStack.filter(
|
|
3175
|
+
(d) => node.contains(d.element)
|
|
3176
|
+
);
|
|
3177
|
+
for (const tracked of trackedInRemoved) {
|
|
3178
|
+
emitDialogClosed(tracked.element);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
function isVisibleDialog(element) {
|
|
3185
|
+
if (element.tagName.toLowerCase() === "dialog") {
|
|
3186
|
+
return element.hasAttribute("open");
|
|
3187
|
+
}
|
|
3188
|
+
return true;
|
|
3189
|
+
}
|
|
3190
|
+
return {
|
|
3191
|
+
start() {
|
|
3192
|
+
if (active) return;
|
|
3193
|
+
active = true;
|
|
3194
|
+
openDialogStack.length = 0;
|
|
3195
|
+
const existingDialogs = doc.querySelectorAll(
|
|
3196
|
+
'dialog[open], [role="dialog"], [role="alertdialog"]'
|
|
3197
|
+
);
|
|
3198
|
+
for (let i = 0; i < existingDialogs.length; i++) {
|
|
3199
|
+
const dialog = existingDialogs[i];
|
|
3200
|
+
if (isVisibleDialog(dialog)) {
|
|
3201
|
+
openDialogStack.push({
|
|
3202
|
+
element: dialog,
|
|
3203
|
+
name: computeDialogAccessibleName(dialog, doc),
|
|
3204
|
+
selector: generateSelector(dialog)
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
mutationObserver = new MutationObserver(handleMutations);
|
|
3209
|
+
mutationObserver.observe(doc.body || doc.documentElement, {
|
|
3210
|
+
attributes: true,
|
|
3211
|
+
attributeFilter: ["open", "role", "aria-modal"],
|
|
3212
|
+
attributeOldValue: true,
|
|
3213
|
+
childList: true,
|
|
3214
|
+
subtree: true
|
|
3215
|
+
});
|
|
3216
|
+
},
|
|
3217
|
+
stop() {
|
|
3218
|
+
if (!active) return;
|
|
3219
|
+
active = false;
|
|
3220
|
+
if (mutationObserver) {
|
|
3221
|
+
mutationObserver.disconnect();
|
|
3222
|
+
mutationObserver = null;
|
|
3223
|
+
}
|
|
3224
|
+
openDialogStack.length = 0;
|
|
3225
|
+
},
|
|
3226
|
+
get isActive() {
|
|
3227
|
+
return active;
|
|
3228
|
+
},
|
|
3229
|
+
getOpenDialogs() {
|
|
3230
|
+
return openDialogStack.map((d) => d.name);
|
|
3231
|
+
}
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
// ../src/runtime/engine.ts
|
|
3236
|
+
function createEngine(options) {
|
|
3237
|
+
const { document: doc } = options;
|
|
3238
|
+
let attached = false;
|
|
3239
|
+
let sessionStart = 0;
|
|
3240
|
+
let events = [];
|
|
3241
|
+
function getTimestamp() {
|
|
3242
|
+
return Date.now() - sessionStart;
|
|
3243
|
+
}
|
|
3244
|
+
function collectEvent(event) {
|
|
3245
|
+
events.push(event);
|
|
3246
|
+
}
|
|
3247
|
+
function collectLiveRegionEvent(event) {
|
|
3248
|
+
const fullEvent = {
|
|
3249
|
+
...event,
|
|
3250
|
+
timestamp: getTimestamp()
|
|
3251
|
+
};
|
|
3252
|
+
events.push(fullEvent);
|
|
3253
|
+
}
|
|
3254
|
+
const focusObs = createFocusObserver({
|
|
3255
|
+
document: doc,
|
|
3256
|
+
onEvent: collectEvent,
|
|
3257
|
+
getTimestamp
|
|
3258
|
+
});
|
|
3259
|
+
const mutationObs = createMutationObserver({
|
|
3260
|
+
document: doc,
|
|
3261
|
+
onEvent: collectEvent,
|
|
3262
|
+
getTimestamp
|
|
3263
|
+
});
|
|
3264
|
+
const liveRegionObs = createLiveRegionObserver(
|
|
3265
|
+
doc,
|
|
3266
|
+
collectLiveRegionEvent
|
|
3267
|
+
);
|
|
3268
|
+
const dialogObs = createDialogObserver({
|
|
3269
|
+
document: doc,
|
|
3270
|
+
onEvent: collectEvent,
|
|
3271
|
+
getTimestamp
|
|
3272
|
+
});
|
|
3273
|
+
return {
|
|
3274
|
+
attach() {
|
|
3275
|
+
if (attached) return;
|
|
3276
|
+
attached = true;
|
|
3277
|
+
sessionStart = Date.now();
|
|
3278
|
+
events = [];
|
|
3279
|
+
focusObs.start();
|
|
3280
|
+
mutationObs.start();
|
|
3281
|
+
liveRegionObs.start();
|
|
3282
|
+
dialogObs.start();
|
|
3283
|
+
},
|
|
3284
|
+
detach() {
|
|
3285
|
+
if (!attached) return;
|
|
3286
|
+
attached = false;
|
|
3287
|
+
focusObs.stop();
|
|
3288
|
+
mutationObs.stop();
|
|
3289
|
+
liveRegionObs.stop();
|
|
3290
|
+
dialogObs.stop();
|
|
3291
|
+
},
|
|
3292
|
+
getEvents() {
|
|
3293
|
+
return [...events];
|
|
3294
|
+
},
|
|
3295
|
+
reset() {
|
|
3296
|
+
events = [];
|
|
3297
|
+
},
|
|
3298
|
+
get isAttached() {
|
|
3299
|
+
return attached;
|
|
3300
|
+
},
|
|
3301
|
+
get dialogObserver() {
|
|
3302
|
+
return {
|
|
3303
|
+
getOpenDialogs() {
|
|
3304
|
+
return dialogObs.getOpenDialogs();
|
|
3305
|
+
}
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
// ../src/runtime/interactions.ts
|
|
3312
|
+
var KEY_MAP = {
|
|
3313
|
+
Tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
3314
|
+
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
3315
|
+
Enter: { key: "Enter", code: "Enter", keyCode: 13 },
|
|
3316
|
+
" ": { key: " ", code: "Space", keyCode: 32 },
|
|
3317
|
+
ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
3318
|
+
ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
3319
|
+
ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
3320
|
+
ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 }
|
|
3321
|
+
};
|
|
3322
|
+
var FOCUSABLE_SELECTOR = [
|
|
3323
|
+
"button:not([disabled])",
|
|
3324
|
+
"a[href]",
|
|
3325
|
+
"input:not([disabled])",
|
|
3326
|
+
"select:not([disabled])",
|
|
3327
|
+
"textarea:not([disabled])",
|
|
3328
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
3329
|
+
].join(", ");
|
|
3330
|
+
function getFocusableElements(document2) {
|
|
3331
|
+
return Array.from(document2.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
3332
|
+
}
|
|
3333
|
+
function computeAccessibleName4(element, document2) {
|
|
3334
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
3335
|
+
if (ariaLabel && ariaLabel.trim()) {
|
|
3336
|
+
return ariaLabel.trim();
|
|
3337
|
+
}
|
|
3338
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
3339
|
+
if (labelledBy && labelledBy.trim()) {
|
|
3340
|
+
const ids = labelledBy.trim().split(/\s+/);
|
|
3341
|
+
const parts = [];
|
|
3342
|
+
for (const id of ids) {
|
|
3343
|
+
const referenced = document2.getElementById(id);
|
|
3344
|
+
if (referenced && referenced.textContent) {
|
|
3345
|
+
parts.push(referenced.textContent.trim());
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
const combined = parts.join(" ");
|
|
3349
|
+
if (combined) return combined;
|
|
3350
|
+
}
|
|
3351
|
+
const textContent = element.textContent;
|
|
3352
|
+
if (textContent && textContent.trim()) {
|
|
3353
|
+
return textContent.trim();
|
|
3354
|
+
}
|
|
3355
|
+
return "";
|
|
3356
|
+
}
|
|
3357
|
+
function getElementRole3(element) {
|
|
3358
|
+
const explicitRole = element.getAttribute("role");
|
|
3359
|
+
if (explicitRole && explicitRole.trim()) {
|
|
3360
|
+
return explicitRole.trim();
|
|
3361
|
+
}
|
|
3362
|
+
const tagName = element.tagName.toLowerCase();
|
|
3363
|
+
switch (tagName) {
|
|
3364
|
+
case "button":
|
|
3365
|
+
return "button";
|
|
3366
|
+
case "a":
|
|
3367
|
+
return element.hasAttribute("href") ? "link" : "generic";
|
|
3368
|
+
case "input": {
|
|
3369
|
+
const type = (element.getAttribute("type") || "text").toLowerCase();
|
|
3370
|
+
switch (type) {
|
|
3371
|
+
case "button":
|
|
3372
|
+
case "submit":
|
|
3373
|
+
case "reset":
|
|
3374
|
+
case "image":
|
|
3375
|
+
return "button";
|
|
3376
|
+
case "checkbox":
|
|
3377
|
+
return "checkbox";
|
|
3378
|
+
case "radio":
|
|
3379
|
+
return "radio";
|
|
3380
|
+
case "range":
|
|
3381
|
+
return "slider";
|
|
3382
|
+
case "search":
|
|
3383
|
+
return "searchbox";
|
|
3384
|
+
default:
|
|
3385
|
+
return "textbox";
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
case "textarea":
|
|
3389
|
+
return "textbox";
|
|
3390
|
+
case "select":
|
|
3391
|
+
return element.hasAttribute("multiple") ? "listbox" : "combobox";
|
|
3392
|
+
case "dialog":
|
|
3393
|
+
return "dialog";
|
|
3394
|
+
default:
|
|
3395
|
+
return "generic";
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
function buildEventTarget(element, document2) {
|
|
3399
|
+
return {
|
|
3400
|
+
role: getElementRole3(element),
|
|
3401
|
+
accessibleName: computeAccessibleName4(element, document2),
|
|
3402
|
+
selector: generateSelector(element)
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
function dispatchKeyboardEvent(document2, options) {
|
|
3406
|
+
const target = document2.activeElement || document2.body;
|
|
3407
|
+
const commonProps = {
|
|
3408
|
+
key: options.key,
|
|
3409
|
+
code: options.code,
|
|
3410
|
+
keyCode: options.keyCode,
|
|
3411
|
+
which: options.keyCode,
|
|
3412
|
+
shiftKey: options.shiftKey || false,
|
|
3413
|
+
ctrlKey: options.ctrlKey || false,
|
|
3414
|
+
altKey: options.altKey || false,
|
|
3415
|
+
metaKey: options.metaKey || false,
|
|
3416
|
+
bubbles: true,
|
|
3417
|
+
cancelable: true
|
|
3418
|
+
};
|
|
3419
|
+
target.dispatchEvent(new KeyboardEvent("keydown", commonProps));
|
|
3420
|
+
if (options.key.length === 1) {
|
|
3421
|
+
target.dispatchEvent(new KeyboardEvent("keypress", commonProps));
|
|
3422
|
+
}
|
|
3423
|
+
target.dispatchEvent(new KeyboardEvent("keyup", commonProps));
|
|
3424
|
+
}
|
|
3425
|
+
function parseKeyCombo(combo) {
|
|
3426
|
+
const parts = combo.split("+");
|
|
3427
|
+
const modifiers = [];
|
|
3428
|
+
let shiftKey = false;
|
|
3429
|
+
let ctrlKey = false;
|
|
3430
|
+
let altKey = false;
|
|
3431
|
+
let metaKey = false;
|
|
3432
|
+
const key = parts[parts.length - 1];
|
|
3433
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
3434
|
+
const mod = parts[i].toLowerCase();
|
|
3435
|
+
switch (mod) {
|
|
3436
|
+
case "shift":
|
|
3437
|
+
shiftKey = true;
|
|
3438
|
+
modifiers.push("Shift");
|
|
3439
|
+
break;
|
|
3440
|
+
case "control":
|
|
3441
|
+
case "ctrl":
|
|
3442
|
+
ctrlKey = true;
|
|
3443
|
+
modifiers.push("Control");
|
|
3444
|
+
break;
|
|
3445
|
+
case "alt":
|
|
3446
|
+
altKey = true;
|
|
3447
|
+
modifiers.push("Alt");
|
|
3448
|
+
break;
|
|
3449
|
+
case "meta":
|
|
3450
|
+
case "cmd":
|
|
3451
|
+
case "command":
|
|
3452
|
+
metaKey = true;
|
|
3453
|
+
modifiers.push("Meta");
|
|
3454
|
+
break;
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
return { key, modifiers, shiftKey, ctrlKey, altKey, metaKey };
|
|
3458
|
+
}
|
|
3459
|
+
function getKeyInfo(key) {
|
|
3460
|
+
if (KEY_MAP[key]) {
|
|
3461
|
+
return KEY_MAP[key];
|
|
3462
|
+
}
|
|
3463
|
+
if (key.length === 1) {
|
|
3464
|
+
return {
|
|
3465
|
+
key,
|
|
3466
|
+
code: `Key${key.toUpperCase()}`,
|
|
3467
|
+
keyCode: key.toUpperCase().charCodeAt(0)
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
return {
|
|
3471
|
+
key,
|
|
3472
|
+
code: key,
|
|
3473
|
+
keyCode: 0
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
function executeClick(selector, document2, onEvent, getTimestamp) {
|
|
3477
|
+
const element = document2.querySelector(selector);
|
|
3478
|
+
if (!element) {
|
|
3479
|
+
const warningEvent = {
|
|
3480
|
+
type: "WARNING",
|
|
3481
|
+
timestamp: getTimestamp(),
|
|
3482
|
+
target: {
|
|
3483
|
+
role: "generic",
|
|
3484
|
+
accessibleName: "",
|
|
3485
|
+
selector
|
|
3486
|
+
},
|
|
3487
|
+
payload: {
|
|
3488
|
+
kind: "warning",
|
|
3489
|
+
message: `Element not found: ${selector}`
|
|
3490
|
+
}
|
|
3491
|
+
};
|
|
3492
|
+
onEvent(warningEvent);
|
|
3493
|
+
return;
|
|
3494
|
+
}
|
|
3495
|
+
const eventProps = { bubbles: true, cancelable: true };
|
|
3496
|
+
element.dispatchEvent(new MouseEvent("mousedown", eventProps));
|
|
3497
|
+
element.dispatchEvent(new MouseEvent("mouseup", eventProps));
|
|
3498
|
+
element.dispatchEvent(new MouseEvent("click", eventProps));
|
|
3499
|
+
if ("focus" in element && typeof element.focus === "function") {
|
|
3500
|
+
element.focus();
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
function executeTab(document2, onEvent, getTimestamp) {
|
|
3504
|
+
const focusableElements = getFocusableElements(document2);
|
|
3505
|
+
if (focusableElements.length === 0) return;
|
|
3506
|
+
const activeElement = document2.activeElement;
|
|
3507
|
+
let currentIndex = -1;
|
|
3508
|
+
if (activeElement) {
|
|
3509
|
+
currentIndex = focusableElements.indexOf(activeElement);
|
|
3510
|
+
}
|
|
3511
|
+
const nextIndex = (currentIndex + 1) % focusableElements.length;
|
|
3512
|
+
const nextElement = focusableElements[nextIndex];
|
|
3513
|
+
dispatchKeyboardEvent(document2, {
|
|
3514
|
+
...KEY_MAP["Tab"],
|
|
3515
|
+
shiftKey: false
|
|
3516
|
+
});
|
|
3517
|
+
if (nextElement && typeof nextElement.focus === "function") {
|
|
3518
|
+
nextElement.focus();
|
|
3519
|
+
}
|
|
3520
|
+
const target = document2.activeElement || document2.body;
|
|
3521
|
+
const keyboardEvent = {
|
|
3522
|
+
type: "KEYBOARD_ACTION",
|
|
3523
|
+
timestamp: getTimestamp(),
|
|
3524
|
+
target: buildEventTarget(target, document2),
|
|
3525
|
+
payload: {
|
|
3526
|
+
kind: "keyboard_action",
|
|
3527
|
+
key: "Tab",
|
|
3528
|
+
modifiers: []
|
|
3529
|
+
}
|
|
3530
|
+
};
|
|
3531
|
+
onEvent(keyboardEvent);
|
|
3532
|
+
}
|
|
3533
|
+
function executeShiftTab(document2, onEvent, getTimestamp) {
|
|
3534
|
+
const focusableElements = getFocusableElements(document2);
|
|
3535
|
+
if (focusableElements.length === 0) return;
|
|
3536
|
+
const activeElement = document2.activeElement;
|
|
3537
|
+
let currentIndex = -1;
|
|
3538
|
+
if (activeElement) {
|
|
3539
|
+
currentIndex = focusableElements.indexOf(activeElement);
|
|
3540
|
+
}
|
|
3541
|
+
const prevIndex = currentIndex <= 0 ? focusableElements.length - 1 : currentIndex - 1;
|
|
3542
|
+
const prevElement = focusableElements[prevIndex];
|
|
3543
|
+
dispatchKeyboardEvent(document2, {
|
|
3544
|
+
...KEY_MAP["Tab"],
|
|
3545
|
+
shiftKey: true
|
|
3546
|
+
});
|
|
3547
|
+
if (prevElement && typeof prevElement.focus === "function") {
|
|
3548
|
+
prevElement.focus();
|
|
3549
|
+
}
|
|
3550
|
+
const target = document2.activeElement || document2.body;
|
|
3551
|
+
const keyboardEvent = {
|
|
3552
|
+
type: "KEYBOARD_ACTION",
|
|
3553
|
+
timestamp: getTimestamp(),
|
|
3554
|
+
target: buildEventTarget(target, document2),
|
|
3555
|
+
payload: {
|
|
3556
|
+
kind: "keyboard_action",
|
|
3557
|
+
key: "Tab",
|
|
3558
|
+
modifiers: ["Shift"]
|
|
3559
|
+
}
|
|
3560
|
+
};
|
|
3561
|
+
onEvent(keyboardEvent);
|
|
3562
|
+
}
|
|
3563
|
+
function executeKeyAction(keyName, document2, onEvent, getTimestamp) {
|
|
3564
|
+
const keyInfo = KEY_MAP[keyName];
|
|
3565
|
+
if (!keyInfo) return;
|
|
3566
|
+
dispatchKeyboardEvent(document2, {
|
|
3567
|
+
...keyInfo
|
|
3568
|
+
});
|
|
3569
|
+
const target = document2.activeElement || document2.body;
|
|
3570
|
+
const keyboardEvent = {
|
|
3571
|
+
type: "KEYBOARD_ACTION",
|
|
3572
|
+
timestamp: getTimestamp(),
|
|
3573
|
+
target: buildEventTarget(target, document2),
|
|
3574
|
+
payload: {
|
|
3575
|
+
kind: "keyboard_action",
|
|
3576
|
+
key: keyName,
|
|
3577
|
+
modifiers: []
|
|
3578
|
+
}
|
|
3579
|
+
};
|
|
3580
|
+
onEvent(keyboardEvent);
|
|
3581
|
+
}
|
|
3582
|
+
function executeKeyCombo(combo, document2, onEvent, getTimestamp) {
|
|
3583
|
+
const parsed = parseKeyCombo(combo);
|
|
3584
|
+
const keyInfo = getKeyInfo(parsed.key);
|
|
3585
|
+
dispatchKeyboardEvent(document2, {
|
|
3586
|
+
...keyInfo,
|
|
3587
|
+
shiftKey: parsed.shiftKey,
|
|
3588
|
+
ctrlKey: parsed.ctrlKey,
|
|
3589
|
+
altKey: parsed.altKey,
|
|
3590
|
+
metaKey: parsed.metaKey
|
|
3591
|
+
});
|
|
3592
|
+
const target = document2.activeElement || document2.body;
|
|
3593
|
+
const keyboardEvent = {
|
|
3594
|
+
type: "KEYBOARD_ACTION",
|
|
3595
|
+
timestamp: getTimestamp(),
|
|
3596
|
+
target: buildEventTarget(target, document2),
|
|
3597
|
+
payload: {
|
|
3598
|
+
kind: "keyboard_action",
|
|
3599
|
+
key: parsed.key,
|
|
3600
|
+
modifiers: parsed.modifiers
|
|
3601
|
+
}
|
|
3602
|
+
};
|
|
3603
|
+
onEvent(keyboardEvent);
|
|
3604
|
+
}
|
|
3605
|
+
async function executeAction(action, document2, onEvent, getTimestamp) {
|
|
3606
|
+
switch (action.type) {
|
|
3607
|
+
case "click":
|
|
3608
|
+
executeClick(action.selector, document2, onEvent, getTimestamp);
|
|
3609
|
+
break;
|
|
3610
|
+
case "tab":
|
|
3611
|
+
executeTab(document2, onEvent, getTimestamp);
|
|
3612
|
+
break;
|
|
3613
|
+
case "shiftTab":
|
|
3614
|
+
executeShiftTab(document2, onEvent, getTimestamp);
|
|
3615
|
+
break;
|
|
3616
|
+
case "escape":
|
|
3617
|
+
executeKeyAction("Escape", document2, onEvent, getTimestamp);
|
|
3618
|
+
break;
|
|
3619
|
+
case "enter":
|
|
3620
|
+
executeKeyAction("Enter", document2, onEvent, getTimestamp);
|
|
3621
|
+
break;
|
|
3622
|
+
case "space":
|
|
3623
|
+
executeKeyAction(" ", document2, onEvent, getTimestamp);
|
|
3624
|
+
break;
|
|
3625
|
+
case "arrowUp":
|
|
3626
|
+
executeKeyAction("ArrowUp", document2, onEvent, getTimestamp);
|
|
3627
|
+
break;
|
|
3628
|
+
case "arrowDown":
|
|
3629
|
+
executeKeyAction("ArrowDown", document2, onEvent, getTimestamp);
|
|
3630
|
+
break;
|
|
3631
|
+
case "arrowLeft":
|
|
3632
|
+
executeKeyAction("ArrowLeft", document2, onEvent, getTimestamp);
|
|
3633
|
+
break;
|
|
3634
|
+
case "arrowRight":
|
|
3635
|
+
executeKeyAction("ArrowRight", document2, onEvent, getTimestamp);
|
|
3636
|
+
break;
|
|
3637
|
+
case "key":
|
|
3638
|
+
executeKeyCombo(action.combo, document2, onEvent, getTimestamp);
|
|
3639
|
+
break;
|
|
3640
|
+
case "wait":
|
|
3641
|
+
await new Promise((resolve) => setTimeout(resolve, action.ms));
|
|
3642
|
+
break;
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
async function executeSequence(sequence, document2, onEvent, getTimestamp, settleTime = 100) {
|
|
3646
|
+
for (let i = 0; i < sequence.actions.length; i++) {
|
|
3647
|
+
const action = sequence.actions[i];
|
|
3648
|
+
await executeAction(action, document2, onEvent, getTimestamp);
|
|
3649
|
+
if (i < sequence.actions.length - 1) {
|
|
3650
|
+
await new Promise((resolve) => setTimeout(resolve, settleTime));
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
// ../src/runtime/heuristics.ts
|
|
3656
|
+
var DEFAULT_CONFIG = {
|
|
3657
|
+
dialogFocusTimeout: 100,
|
|
3658
|
+
rapidAnnouncementWindow: 500,
|
|
3659
|
+
rapidAnnouncementThreshold: 3,
|
|
3660
|
+
keyboardResponseTimeout: 200
|
|
3661
|
+
};
|
|
3662
|
+
function createHeuristicAnalyzer(config) {
|
|
3663
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
3664
|
+
const trackedDialogs = [];
|
|
3665
|
+
const pendingDialogFocusChecks = [];
|
|
3666
|
+
const pendingKeyboardResponses = [];
|
|
3667
|
+
const announcementTimestamps = [];
|
|
3668
|
+
let lastFocusedSelector = null;
|
|
3669
|
+
function createWarning(timestamp, message, target, relatedEvent) {
|
|
3670
|
+
const payload = {
|
|
3671
|
+
kind: "warning",
|
|
3672
|
+
message,
|
|
3673
|
+
relatedEvent
|
|
3674
|
+
};
|
|
3675
|
+
return {
|
|
3676
|
+
type: "WARNING",
|
|
3677
|
+
timestamp,
|
|
3678
|
+
target,
|
|
3679
|
+
payload
|
|
3680
|
+
};
|
|
3681
|
+
}
|
|
3682
|
+
function checkExpiredDialogFocusTimeouts(currentTimestamp) {
|
|
3683
|
+
const warnings = [];
|
|
3684
|
+
for (let i = pendingDialogFocusChecks.length - 1; i >= 0; i--) {
|
|
3685
|
+
const pending = pendingDialogFocusChecks[i];
|
|
3686
|
+
if (pending.resolved) continue;
|
|
3687
|
+
const elapsed = currentTimestamp - pending.openedAt;
|
|
3688
|
+
if (elapsed > cfg.dialogFocusTimeout) {
|
|
3689
|
+
if (pending.isModal) {
|
|
3690
|
+
warnings.push(
|
|
3691
|
+
createWarning(
|
|
3692
|
+
currentTimestamp,
|
|
3693
|
+
"Focus was not moved into modal dialog",
|
|
3694
|
+
{
|
|
3695
|
+
role: "dialog",
|
|
3696
|
+
accessibleName: pending.dialogName,
|
|
3697
|
+
selector: pending.dialogSelector
|
|
3698
|
+
}
|
|
3699
|
+
)
|
|
3700
|
+
);
|
|
3701
|
+
}
|
|
3702
|
+
pending.resolved = true;
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
return warnings;
|
|
3706
|
+
}
|
|
3707
|
+
function checkExpiredKeyboardResponses(currentTimestamp) {
|
|
3708
|
+
const warnings = [];
|
|
3709
|
+
for (let i = pendingKeyboardResponses.length - 1; i >= 0; i--) {
|
|
3710
|
+
const pending = pendingKeyboardResponses[i];
|
|
3711
|
+
if (pending.resolved) continue;
|
|
3712
|
+
const elapsed = currentTimestamp - pending.timestamp;
|
|
3713
|
+
if (elapsed > cfg.keyboardResponseTimeout) {
|
|
3714
|
+
warnings.push(
|
|
3715
|
+
createWarning(
|
|
3716
|
+
currentTimestamp,
|
|
3717
|
+
"Keyboard action produced no accessibility response",
|
|
3718
|
+
pending.event.target,
|
|
3719
|
+
pending.event
|
|
3720
|
+
)
|
|
3721
|
+
);
|
|
3722
|
+
pending.resolved = true;
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
return warnings;
|
|
3726
|
+
}
|
|
3727
|
+
function checkRapidAnnouncements(currentTimestamp) {
|
|
3728
|
+
const warnings = [];
|
|
3729
|
+
const windowStart = currentTimestamp - cfg.rapidAnnouncementWindow;
|
|
3730
|
+
while (announcementTimestamps.length > 0 && announcementTimestamps[0] < windowStart) {
|
|
3731
|
+
announcementTimestamps.shift();
|
|
3732
|
+
}
|
|
3733
|
+
if (announcementTimestamps.length > cfg.rapidAnnouncementThreshold) {
|
|
3734
|
+
warnings.push(
|
|
3735
|
+
createWarning(
|
|
3736
|
+
currentTimestamp,
|
|
3737
|
+
"Rapid announcements detected: assistive technology users may miss content",
|
|
3738
|
+
{
|
|
3739
|
+
role: "region",
|
|
3740
|
+
accessibleName: "",
|
|
3741
|
+
selector: "body"
|
|
3742
|
+
}
|
|
3743
|
+
)
|
|
3744
|
+
);
|
|
3745
|
+
announcementTimestamps.length = 0;
|
|
3746
|
+
}
|
|
3747
|
+
return warnings;
|
|
3748
|
+
}
|
|
3749
|
+
function checkFocusEscape(event) {
|
|
3750
|
+
const warnings = [];
|
|
3751
|
+
if (event.type !== "FOCUS_CHANGED") return warnings;
|
|
3752
|
+
const modalDialogs = trackedDialogs.filter((d) => d.isModal);
|
|
3753
|
+
if (modalDialogs.length === 0) return warnings;
|
|
3754
|
+
const focusedSelector = event.target.selector;
|
|
3755
|
+
for (const dialog of modalDialogs) {
|
|
3756
|
+
if (!isSelectorInside(focusedSelector, dialog.selector)) {
|
|
3757
|
+
warnings.push(
|
|
3758
|
+
createWarning(
|
|
3759
|
+
event.timestamp,
|
|
3760
|
+
"Focus escaped modal dialog",
|
|
3761
|
+
event.target,
|
|
3762
|
+
event
|
|
3763
|
+
)
|
|
3764
|
+
);
|
|
3765
|
+
break;
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
return warnings;
|
|
3769
|
+
}
|
|
3770
|
+
function checkFocusedElementRemoved(event) {
|
|
3771
|
+
const warnings = [];
|
|
3772
|
+
if (event.type === "DOM_MUTATION" && lastFocusedSelector !== null) {
|
|
3773
|
+
if (event.target.selector === lastFocusedSelector) {
|
|
3774
|
+
warnings.push(
|
|
3775
|
+
createWarning(
|
|
3776
|
+
event.timestamp,
|
|
3777
|
+
"Focused element removed without focus management",
|
|
3778
|
+
event.target,
|
|
3779
|
+
event
|
|
3780
|
+
)
|
|
3781
|
+
);
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
return warnings;
|
|
3785
|
+
}
|
|
3786
|
+
function isSelectorInside(childSelector, containerSelector) {
|
|
3787
|
+
if (childSelector === containerSelector) return true;
|
|
3788
|
+
if (childSelector.startsWith(containerSelector + " ")) return true;
|
|
3789
|
+
if (childSelector.startsWith(containerSelector + " > ")) return true;
|
|
3790
|
+
if (childSelector.startsWith(containerSelector + ">")) return true;
|
|
3791
|
+
return false;
|
|
3792
|
+
}
|
|
3793
|
+
function processEvent(event) {
|
|
3794
|
+
const warnings = [];
|
|
3795
|
+
warnings.push(...checkExpiredDialogFocusTimeouts(event.timestamp));
|
|
3796
|
+
warnings.push(...checkExpiredKeyboardResponses(event.timestamp));
|
|
3797
|
+
switch (event.type) {
|
|
3798
|
+
case "DIALOG_OPENED": {
|
|
3799
|
+
const payload = event.payload;
|
|
3800
|
+
const dialog = {
|
|
3801
|
+
selector: event.target.selector,
|
|
3802
|
+
name: payload.dialogName,
|
|
3803
|
+
isModal: payload.isModal
|
|
3804
|
+
};
|
|
3805
|
+
trackedDialogs.push(dialog);
|
|
3806
|
+
if (payload.isModal) {
|
|
3807
|
+
pendingDialogFocusChecks.push({
|
|
3808
|
+
dialogSelector: event.target.selector,
|
|
3809
|
+
dialogName: payload.dialogName,
|
|
3810
|
+
openedAt: event.timestamp,
|
|
3811
|
+
isModal: true,
|
|
3812
|
+
resolved: false
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3815
|
+
break;
|
|
3816
|
+
}
|
|
3817
|
+
case "DIALOG_CLOSED": {
|
|
3818
|
+
const closedSelector = event.target.selector;
|
|
3819
|
+
const idx = trackedDialogs.findIndex(
|
|
3820
|
+
(d) => d.selector === closedSelector
|
|
3821
|
+
);
|
|
3822
|
+
if (idx !== -1) {
|
|
3823
|
+
trackedDialogs.splice(idx, 1);
|
|
3824
|
+
}
|
|
3825
|
+
for (const pending of pendingDialogFocusChecks) {
|
|
3826
|
+
if (pending.dialogSelector === closedSelector && !pending.resolved) {
|
|
3827
|
+
pending.resolved = true;
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
break;
|
|
3831
|
+
}
|
|
3832
|
+
case "FOCUS_CHANGED": {
|
|
3833
|
+
for (const pending of pendingDialogFocusChecks) {
|
|
3834
|
+
if (pending.resolved) continue;
|
|
3835
|
+
if (isSelectorInside(event.target.selector, pending.dialogSelector)) {
|
|
3836
|
+
pending.resolved = true;
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
for (const pending of pendingKeyboardResponses) {
|
|
3840
|
+
if (!pending.resolved) {
|
|
3841
|
+
pending.resolved = true;
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
warnings.push(...checkFocusEscape(event));
|
|
3845
|
+
lastFocusedSelector = event.target.selector;
|
|
3846
|
+
break;
|
|
3847
|
+
}
|
|
3848
|
+
case "ANNOUNCEMENT": {
|
|
3849
|
+
announcementTimestamps.push(event.timestamp);
|
|
3850
|
+
for (const pending of pendingKeyboardResponses) {
|
|
3851
|
+
if (!pending.resolved) {
|
|
3852
|
+
pending.resolved = true;
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
warnings.push(...checkRapidAnnouncements(event.timestamp));
|
|
3856
|
+
break;
|
|
3857
|
+
}
|
|
3858
|
+
case "KEYBOARD_ACTION": {
|
|
3859
|
+
pendingKeyboardResponses.push({
|
|
3860
|
+
event,
|
|
3861
|
+
timestamp: event.timestamp,
|
|
3862
|
+
resolved: false
|
|
3863
|
+
});
|
|
3864
|
+
break;
|
|
3865
|
+
}
|
|
3866
|
+
case "DOM_MUTATION": {
|
|
3867
|
+
warnings.push(...checkFocusedElementRemoved(event));
|
|
3868
|
+
for (const pending of pendingKeyboardResponses) {
|
|
3869
|
+
if (!pending.resolved) {
|
|
3870
|
+
pending.resolved = true;
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
break;
|
|
3874
|
+
}
|
|
3875
|
+
case "STATE_CHANGED":
|
|
3876
|
+
case "ROLE_CHANGED":
|
|
3877
|
+
case "ACCESSIBLE_NAME_CHANGED": {
|
|
3878
|
+
for (const pending of pendingKeyboardResponses) {
|
|
3879
|
+
if (!pending.resolved) {
|
|
3880
|
+
pending.resolved = true;
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
break;
|
|
3884
|
+
}
|
|
3885
|
+
default:
|
|
3886
|
+
break;
|
|
3887
|
+
}
|
|
3888
|
+
cleanupResolvedChecks();
|
|
3889
|
+
return warnings;
|
|
3890
|
+
}
|
|
3891
|
+
function cleanupResolvedChecks() {
|
|
3892
|
+
for (let i = pendingDialogFocusChecks.length - 1; i >= 0; i--) {
|
|
3893
|
+
if (pendingDialogFocusChecks[i].resolved) {
|
|
3894
|
+
pendingDialogFocusChecks.splice(i, 1);
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
for (let i = pendingKeyboardResponses.length - 1; i >= 0; i--) {
|
|
3898
|
+
if (pendingKeyboardResponses[i].resolved) {
|
|
3899
|
+
pendingKeyboardResponses.splice(i, 1);
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
return {
|
|
3904
|
+
process(event) {
|
|
3905
|
+
return processEvent(event);
|
|
3906
|
+
},
|
|
3907
|
+
get openDialogs() {
|
|
3908
|
+
return trackedDialogs.map((d) => d.selector);
|
|
3909
|
+
}
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
// ../src/version.ts
|
|
3914
|
+
var SPEAKABLE_VERSION = typeof __SPEAKABLE_VERSION__ !== "undefined" && __SPEAKABLE_VERSION__ ? __SPEAKABLE_VERSION__ : "dev";
|
|
3915
|
+
|
|
3916
|
+
// ../src/runtime/timeline-generator.ts
|
|
3917
|
+
var TIMELINE_FORMAT_VERSION = "1.0";
|
|
3918
|
+
var DEFAULT_SETTLE_PERIOD = 500;
|
|
3919
|
+
var DEFAULT_LOAD_TIMEOUT = 1e4;
|
|
3920
|
+
var DOM_STABILITY_WAIT = 100;
|
|
3921
|
+
function createTimelineGenerator(options) {
|
|
3922
|
+
const {
|
|
3923
|
+
document: doc,
|
|
3924
|
+
componentName,
|
|
3925
|
+
storyName,
|
|
3926
|
+
settlePeriod = DEFAULT_SETTLE_PERIOD,
|
|
3927
|
+
loadTimeout = DEFAULT_LOAD_TIMEOUT,
|
|
3928
|
+
heuristics: enableHeuristics = true
|
|
3929
|
+
} = options;
|
|
3930
|
+
let aborted = false;
|
|
3931
|
+
let currentEngine = null;
|
|
3932
|
+
return {
|
|
3933
|
+
async capture(sequence) {
|
|
3934
|
+
aborted = false;
|
|
3935
|
+
await waitForDocumentReady(doc, loadTimeout);
|
|
3936
|
+
if (aborted) {
|
|
3937
|
+
return buildAbortedTimeline(componentName, storyName, sequence);
|
|
3938
|
+
}
|
|
3939
|
+
const engine = createEngine({ document: doc });
|
|
3940
|
+
currentEngine = engine;
|
|
3941
|
+
const heuristicAnalyzer = enableHeuristics ? createHeuristicAnalyzer() : null;
|
|
3942
|
+
const interactionEvents = [];
|
|
3943
|
+
const sessionStart = Date.now();
|
|
3944
|
+
engine.attach();
|
|
3945
|
+
if (aborted) {
|
|
3946
|
+
engine.detach();
|
|
3947
|
+
currentEngine = null;
|
|
3948
|
+
return buildAbortedTimeline(componentName, storyName, sequence);
|
|
3949
|
+
}
|
|
3950
|
+
function getTimestamp() {
|
|
3951
|
+
return Date.now() - sessionStart;
|
|
3952
|
+
}
|
|
3953
|
+
await executeSequence(
|
|
3954
|
+
sequence,
|
|
3955
|
+
doc,
|
|
3956
|
+
(event) => {
|
|
3957
|
+
interactionEvents.push(event);
|
|
3958
|
+
},
|
|
3959
|
+
getTimestamp,
|
|
3960
|
+
DOM_STABILITY_WAIT
|
|
3961
|
+
);
|
|
3962
|
+
if (aborted) {
|
|
3963
|
+
engine.detach();
|
|
3964
|
+
currentEngine = null;
|
|
3965
|
+
return buildAbortedTimeline(componentName, storyName, sequence);
|
|
3966
|
+
}
|
|
3967
|
+
await delay(settlePeriod);
|
|
3968
|
+
engine.detach();
|
|
3969
|
+
currentEngine = null;
|
|
3970
|
+
const duration = Date.now() - sessionStart;
|
|
3971
|
+
const engineEvents = engine.getEvents();
|
|
3972
|
+
const allEvents = mergeEvents(engineEvents, interactionEvents);
|
|
3973
|
+
if (heuristicAnalyzer) {
|
|
3974
|
+
const heuristicWarnings = [];
|
|
3975
|
+
for (const event of allEvents) {
|
|
3976
|
+
const warnings = heuristicAnalyzer.process(event);
|
|
3977
|
+
heuristicWarnings.push(...warnings);
|
|
3978
|
+
}
|
|
3979
|
+
for (const warning of heuristicWarnings) {
|
|
3980
|
+
allEvents.push(warning);
|
|
3981
|
+
}
|
|
3982
|
+
allEvents.sort((a, b) => a.timestamp - b.timestamp);
|
|
3983
|
+
}
|
|
3984
|
+
const allWarnings = allEvents.filter((e) => e.type === "WARNING");
|
|
3985
|
+
const metadata = {
|
|
3986
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3987
|
+
speakableVersion: SPEAKABLE_VERSION,
|
|
3988
|
+
sourceUrl: getSourceUrl(doc),
|
|
3989
|
+
userAgent: getUserAgent()
|
|
3990
|
+
};
|
|
3991
|
+
const timeline = {
|
|
3992
|
+
version: TIMELINE_FORMAT_VERSION,
|
|
3993
|
+
component: componentName,
|
|
3994
|
+
story: storyName || null,
|
|
3995
|
+
interactionSequence: sequence.description,
|
|
3996
|
+
duration,
|
|
3997
|
+
events: allEvents,
|
|
3998
|
+
warnings: allWarnings,
|
|
3999
|
+
metadata
|
|
4000
|
+
};
|
|
4001
|
+
return timeline;
|
|
4002
|
+
},
|
|
4003
|
+
abort() {
|
|
4004
|
+
aborted = true;
|
|
4005
|
+
if (currentEngine && currentEngine.isAttached) {
|
|
4006
|
+
currentEngine.detach();
|
|
4007
|
+
currentEngine = null;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
};
|
|
4011
|
+
}
|
|
4012
|
+
async function waitForDocumentReady(doc, timeout) {
|
|
4013
|
+
if (doc.body) return;
|
|
4014
|
+
return new Promise((resolve, reject) => {
|
|
4015
|
+
const timeoutId = setTimeout(() => {
|
|
4016
|
+
reject(
|
|
4017
|
+
new Error(
|
|
4018
|
+
`Document load timeout (${timeout}ms): ${getSourceUrl(doc)}`
|
|
4019
|
+
)
|
|
4020
|
+
);
|
|
4021
|
+
}, timeout);
|
|
4022
|
+
const interval = setInterval(() => {
|
|
4023
|
+
if (doc.body) {
|
|
4024
|
+
clearTimeout(timeoutId);
|
|
4025
|
+
clearInterval(interval);
|
|
4026
|
+
resolve();
|
|
4027
|
+
}
|
|
4028
|
+
}, 50);
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
4031
|
+
function mergeEvents(engineEvents, interactionEvents) {
|
|
4032
|
+
const all = [...engineEvents, ...interactionEvents];
|
|
4033
|
+
all.sort((a, b) => a.timestamp - b.timestamp);
|
|
4034
|
+
return all;
|
|
4035
|
+
}
|
|
4036
|
+
function getSourceUrl(doc) {
|
|
4037
|
+
try {
|
|
4038
|
+
const url = doc.URL;
|
|
4039
|
+
if (url && url !== "about:blank" && url !== "") {
|
|
4040
|
+
return url;
|
|
4041
|
+
}
|
|
4042
|
+
} catch {
|
|
4043
|
+
}
|
|
4044
|
+
return "local";
|
|
4045
|
+
}
|
|
4046
|
+
function getUserAgent() {
|
|
4047
|
+
try {
|
|
4048
|
+
if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
4049
|
+
return navigator.userAgent;
|
|
4050
|
+
}
|
|
4051
|
+
} catch {
|
|
4052
|
+
}
|
|
4053
|
+
return "jsdom";
|
|
4054
|
+
}
|
|
4055
|
+
function delay(ms) {
|
|
4056
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4057
|
+
}
|
|
4058
|
+
function buildAbortedTimeline(componentName, storyName, sequence) {
|
|
4059
|
+
return {
|
|
4060
|
+
version: TIMELINE_FORMAT_VERSION,
|
|
4061
|
+
component: componentName,
|
|
4062
|
+
story: storyName || null,
|
|
4063
|
+
interactionSequence: sequence.description,
|
|
4064
|
+
duration: 0,
|
|
4065
|
+
events: [],
|
|
4066
|
+
warnings: [],
|
|
4067
|
+
metadata: {
|
|
4068
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4069
|
+
speakableVersion: SPEAKABLE_VERSION,
|
|
4070
|
+
sourceUrl: "local",
|
|
4071
|
+
userAgent: getUserAgent()
|
|
4072
|
+
}
|
|
4073
|
+
};
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
// ../src/browser/capture.ts
|
|
4077
|
+
async function captureTimeline(document2, options) {
|
|
4078
|
+
const {
|
|
4079
|
+
componentName,
|
|
4080
|
+
storyName,
|
|
4081
|
+
sequence,
|
|
4082
|
+
settlePeriod,
|
|
4083
|
+
awaitUpgrade = true,
|
|
4084
|
+
upgradeTimeoutMs
|
|
4085
|
+
} = options;
|
|
4086
|
+
let upgradeWarnings = [];
|
|
4087
|
+
if (awaitUpgrade) {
|
|
4088
|
+
const warnings = await awaitCustomElementsReady(document2, upgradeTimeoutMs);
|
|
4089
|
+
upgradeWarnings = warnings.map((w) => w.message);
|
|
4090
|
+
}
|
|
4091
|
+
const generator = createTimelineGenerator({
|
|
4092
|
+
document: document2,
|
|
4093
|
+
componentName,
|
|
4094
|
+
storyName,
|
|
4095
|
+
...settlePeriod !== void 0 && { settlePeriod }
|
|
4096
|
+
});
|
|
4097
|
+
const timeline = await generator.capture(sequence);
|
|
4098
|
+
if (upgradeWarnings.length > 0) {
|
|
4099
|
+
const upgradeEvents = upgradeWarnings.map((message) => ({
|
|
4100
|
+
type: "WARNING",
|
|
4101
|
+
timestamp: 0,
|
|
4102
|
+
target: { role: "generic", accessibleName: "", selector: ":root" },
|
|
4103
|
+
payload: { kind: "warning", message }
|
|
4104
|
+
}));
|
|
4105
|
+
return {
|
|
4106
|
+
...timeline,
|
|
4107
|
+
events: [...upgradeEvents, ...timeline.events],
|
|
4108
|
+
warnings: [...upgradeEvents, ...timeline.warnings]
|
|
4109
|
+
};
|
|
4110
|
+
}
|
|
4111
|
+
return timeline;
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
// ../src/runtime/patterns.ts
|
|
4115
|
+
var MODAL_DIALOG_DEFAULTS = {
|
|
4116
|
+
trigger: "button",
|
|
4117
|
+
container: '[role="dialog"], dialog'
|
|
4118
|
+
};
|
|
4119
|
+
var COMBOBOX_DEFAULTS = {
|
|
4120
|
+
input: '[role="combobox"], input[type="text"]',
|
|
4121
|
+
items: '[role="option"]'
|
|
4122
|
+
};
|
|
4123
|
+
var TABS_DEFAULTS = {
|
|
4124
|
+
container: '[role="tablist"]',
|
|
4125
|
+
items: '[role="tab"]'
|
|
4126
|
+
};
|
|
4127
|
+
var ACCORDION_DEFAULTS = {
|
|
4128
|
+
trigger: "button[aria-expanded]"
|
|
4129
|
+
};
|
|
4130
|
+
function buildModalDialogPattern(selectors) {
|
|
4131
|
+
const trigger = selectors?.trigger ?? MODAL_DIALOG_DEFAULTS.trigger;
|
|
4132
|
+
const actions = [
|
|
4133
|
+
{ type: "click", selector: trigger },
|
|
4134
|
+
{ type: "tab" },
|
|
4135
|
+
{ type: "tab" },
|
|
4136
|
+
{ type: "tab" },
|
|
4137
|
+
{ type: "escape" }
|
|
4138
|
+
];
|
|
4139
|
+
return {
|
|
4140
|
+
description: "Modal dialog: click trigger to open, tab through dialog content, press Escape to close, verify focus returns to trigger",
|
|
4141
|
+
actions
|
|
4142
|
+
};
|
|
4143
|
+
}
|
|
4144
|
+
function buildComboboxPattern(selectors) {
|
|
4145
|
+
const input = selectors?.input ?? COMBOBOX_DEFAULTS.input;
|
|
4146
|
+
const actions = [
|
|
4147
|
+
{ type: "click", selector: input },
|
|
4148
|
+
{ type: "key", combo: "a" },
|
|
4149
|
+
{ type: "arrowDown" },
|
|
4150
|
+
{ type: "arrowDown" },
|
|
4151
|
+
{ type: "enter" }
|
|
4152
|
+
];
|
|
4153
|
+
return {
|
|
4154
|
+
description: "Combobox: focus input, type to filter, navigate options with arrow keys, select with Enter",
|
|
4155
|
+
actions
|
|
4156
|
+
};
|
|
4157
|
+
}
|
|
4158
|
+
function buildTabsPattern(selectors) {
|
|
4159
|
+
const container = selectors?.container ?? TABS_DEFAULTS.container;
|
|
4160
|
+
const actions = [
|
|
4161
|
+
{ type: "click", selector: container },
|
|
4162
|
+
{ type: "arrowRight" },
|
|
4163
|
+
{ type: "arrowRight" },
|
|
4164
|
+
{ type: "enter" },
|
|
4165
|
+
{ type: "arrowLeft" },
|
|
4166
|
+
{ type: "space" }
|
|
4167
|
+
];
|
|
4168
|
+
return {
|
|
4169
|
+
description: "Tabs: focus tablist, navigate between tabs with arrow keys, activate with Enter/Space",
|
|
4170
|
+
actions
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
function buildAccordionPattern(selectors) {
|
|
4174
|
+
const trigger = selectors?.trigger ?? ACCORDION_DEFAULTS.trigger;
|
|
4175
|
+
const actions = [
|
|
4176
|
+
{ type: "click", selector: trigger },
|
|
4177
|
+
{ type: "enter" },
|
|
4178
|
+
{ type: "arrowDown" },
|
|
4179
|
+
{ type: "space" },
|
|
4180
|
+
{ type: "arrowDown" },
|
|
4181
|
+
{ type: "enter" }
|
|
4182
|
+
];
|
|
4183
|
+
return {
|
|
4184
|
+
description: "Accordion: focus header, toggle with Enter/Space, navigate between headers with arrow keys",
|
|
4185
|
+
actions
|
|
4186
|
+
};
|
|
4187
|
+
}
|
|
4188
|
+
function getBuiltinPattern(name, selectors) {
|
|
4189
|
+
switch (name) {
|
|
4190
|
+
case "modal-dialog":
|
|
4191
|
+
return buildModalDialogPattern(selectors);
|
|
4192
|
+
case "combobox":
|
|
4193
|
+
return buildComboboxPattern(selectors);
|
|
4194
|
+
case "tabs":
|
|
4195
|
+
return buildTabsPattern(selectors);
|
|
4196
|
+
case "accordion":
|
|
4197
|
+
return buildAccordionPattern(selectors);
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4201
|
+
// src/sequence.ts
|
|
4202
|
+
var PLAY_PROBE_SEQUENCE = {
|
|
4203
|
+
description: "Keyboard walk: tab through focusable elements",
|
|
4204
|
+
actions: [{ type: "tab" }, { type: "tab" }, { type: "tab" }]
|
|
4205
|
+
};
|
|
4206
|
+
function resolveSequence(context) {
|
|
4207
|
+
const params = context?.parameters?.speakable;
|
|
4208
|
+
if (params && params.timeline === false) {
|
|
4209
|
+
return null;
|
|
4210
|
+
}
|
|
4211
|
+
if (params?.sequence && Array.isArray(params.sequence.actions)) {
|
|
4212
|
+
return params.sequence;
|
|
4213
|
+
}
|
|
4214
|
+
if (params?.pattern) {
|
|
4215
|
+
try {
|
|
4216
|
+
return getBuiltinPattern(params.pattern, params.selectors);
|
|
4217
|
+
} catch {
|
|
4218
|
+
return null;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
if (typeof context?.playFunction === "function" || typeof context?.play === "function") {
|
|
4222
|
+
return PLAY_PROBE_SEQUENCE;
|
|
4223
|
+
}
|
|
4224
|
+
return null;
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
// src/preview.ts
|
|
4228
|
+
var currentObserver = null;
|
|
4229
|
+
var debounceTimer = null;
|
|
4230
|
+
var captureToken = 0;
|
|
4231
|
+
function getChannel() {
|
|
4232
|
+
return window.__STORYBOOK_ADDONS_CHANNEL__;
|
|
4233
|
+
}
|
|
4234
|
+
function findStoryRoot() {
|
|
4235
|
+
return document.getElementById("storybook-root") || document.getElementById("root") || document.body;
|
|
4236
|
+
}
|
|
4237
|
+
function runAnalysis() {
|
|
4238
|
+
const root = findStoryRoot();
|
|
4239
|
+
if (!root) return;
|
|
4240
|
+
const analysis = analyzeDOM(root);
|
|
4241
|
+
const channel = getChannel();
|
|
4242
|
+
if (channel) {
|
|
4243
|
+
channel.emit(EVENT_RESULT, analysis);
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
async function runTimelineCapture(context) {
|
|
4247
|
+
const sequence = resolveSequence(context || {});
|
|
4248
|
+
if (!sequence) return;
|
|
4249
|
+
const token = ++captureToken;
|
|
4250
|
+
const componentName = context?.title || context?.kind || context?.componentId || "Component";
|
|
4251
|
+
const storyName = context?.name || context?.story || "Story";
|
|
4252
|
+
let timeline;
|
|
4253
|
+
try {
|
|
4254
|
+
timeline = await captureTimeline(document, {
|
|
4255
|
+
componentName,
|
|
4256
|
+
storyName,
|
|
4257
|
+
sequence
|
|
4258
|
+
});
|
|
4259
|
+
} catch {
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
if (token !== captureToken) return;
|
|
4263
|
+
const channel = getChannel();
|
|
4264
|
+
if (channel) {
|
|
4265
|
+
channel.emit(EVENT_TIMELINE, timeline);
|
|
2076
4266
|
}
|
|
2077
4267
|
}
|
|
2078
4268
|
function debouncedAnalysis() {
|
|
@@ -2111,13 +4301,15 @@ function startObserving() {
|
|
|
2111
4301
|
]
|
|
2112
4302
|
});
|
|
2113
4303
|
}
|
|
2114
|
-
var withSpeakable = (storyFn,
|
|
4304
|
+
var withSpeakable = (storyFn, context) => {
|
|
2115
4305
|
const result = storyFn();
|
|
4306
|
+
captureToken++;
|
|
2116
4307
|
if (typeof window !== "undefined") {
|
|
2117
4308
|
requestAnimationFrame(() => {
|
|
2118
4309
|
setTimeout(() => {
|
|
2119
4310
|
runAnalysis();
|
|
2120
4311
|
startObserving();
|
|
4312
|
+
void runTimelineCapture(context);
|
|
2121
4313
|
}, 100);
|
|
2122
4314
|
});
|
|
2123
4315
|
}
|