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