@reticular/speakable 1.7.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/dist/browser/index.js +127 -5
- package/dist/browser/index.js.map +1 -1
- package/dist/cli.js +127 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +127 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +127 -5
- package/dist/mcp.js.map +1 -1
- package/dist/speakable-browser.global.js +9 -9
- package/dist/speakable-browser.global.js.map +1 -1
- package/dist/storybook/dist/preview.js +127 -5
- package/package.json +5 -1
|
@@ -67,6 +67,12 @@ var SUPPORTED_ROLES = [
|
|
|
67
67
|
"meter",
|
|
68
68
|
"progressbar",
|
|
69
69
|
"status",
|
|
70
|
+
"switch",
|
|
71
|
+
"slider",
|
|
72
|
+
"spinbutton",
|
|
73
|
+
"tab",
|
|
74
|
+
"tablist",
|
|
75
|
+
"tabpanel",
|
|
70
76
|
"document",
|
|
71
77
|
"application",
|
|
72
78
|
"separator"
|
|
@@ -366,7 +372,10 @@ var INPUT_TYPE_ROLE_MAP = {
|
|
|
366
372
|
"search": "textbox",
|
|
367
373
|
"tel": "textbox",
|
|
368
374
|
"url": "textbox",
|
|
369
|
-
"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"
|
|
370
379
|
};
|
|
371
380
|
function computeRole(element) {
|
|
372
381
|
const warnings = [];
|
|
@@ -399,6 +408,11 @@ function computeImplicitRole(element, _warnings) {
|
|
|
399
408
|
if (tagName === "input") {
|
|
400
409
|
return computeInputRole(element);
|
|
401
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
|
+
}
|
|
402
416
|
if (tagName === "a") {
|
|
403
417
|
return element.hasAttribute("href") ? "link" : null;
|
|
404
418
|
}
|
|
@@ -802,6 +816,7 @@ function processChildNode(node, children, warnings) {
|
|
|
802
816
|
if (node.nodeType === 1) {
|
|
803
817
|
const element = node;
|
|
804
818
|
if (isHidden(element)) return;
|
|
819
|
+
if (isConsumedLabel(element)) return;
|
|
805
820
|
if (!isAccessible(element)) {
|
|
806
821
|
const grandchildren = collectChildrenFromRolelessElement(element, warnings);
|
|
807
822
|
children.push(...grandchildren);
|
|
@@ -811,6 +826,25 @@ function processChildNode(node, children, warnings) {
|
|
|
811
826
|
if (childNode) children.push(childNode);
|
|
812
827
|
}
|
|
813
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
|
+
}
|
|
814
848
|
function collectChildrenFromRolelessElement(element, warnings) {
|
|
815
849
|
return collectAccessibleChildren(element, warnings);
|
|
816
850
|
}
|
|
@@ -891,6 +925,30 @@ function buildAccessibilityTree(rootElement, sourceHash) {
|
|
|
891
925
|
};
|
|
892
926
|
return { model, warnings };
|
|
893
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
|
+
}
|
|
894
952
|
function buildNodeRecursive(element, warnings) {
|
|
895
953
|
if (isHidden(element)) {
|
|
896
954
|
return null;
|
|
@@ -927,7 +985,15 @@ function buildNodeRecursive(element, warnings) {
|
|
|
927
985
|
element: w.element
|
|
928
986
|
})));
|
|
929
987
|
const focusInfo = extractFocusInfo(element);
|
|
930
|
-
|
|
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;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
assignImplicitSetPosition(children);
|
|
931
997
|
const node = {
|
|
932
998
|
role: roleResult.role,
|
|
933
999
|
name: nameResult.name,
|
|
@@ -1424,8 +1490,45 @@ function renderNodeVoiceOver(node, announcements, c) {
|
|
|
1424
1490
|
renderNodeVoiceOver(child, announcements, c);
|
|
1425
1491
|
}
|
|
1426
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
|
+
}
|
|
1427
1504
|
function formatNodeVoiceOver(node, c) {
|
|
1505
|
+
if (isSilentContainerVoiceOver(node)) {
|
|
1506
|
+
return "";
|
|
1507
|
+
}
|
|
1428
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
|
+
}
|
|
1429
1532
|
const roleFirst = shouldAnnounceRoleFirst(node.role);
|
|
1430
1533
|
if (roleFirst) {
|
|
1431
1534
|
const roleText = formatRoleVoiceOver(node);
|
|
@@ -1454,6 +1557,9 @@ function formatNodeVoiceOver(node, c) {
|
|
|
1454
1557
|
parts.push(valueText);
|
|
1455
1558
|
}
|
|
1456
1559
|
}
|
|
1560
|
+
if (node.state.posinset !== void 0 && node.state.setsize !== void 0) {
|
|
1561
|
+
parts.push(`${node.state.posinset} of ${node.state.setsize}`);
|
|
1562
|
+
}
|
|
1457
1563
|
if (node.description) {
|
|
1458
1564
|
parts.push(c.description(node.description));
|
|
1459
1565
|
}
|
|
@@ -1489,7 +1595,7 @@ function formatRoleVoiceOver(node) {
|
|
|
1489
1595
|
case "list":
|
|
1490
1596
|
return "list";
|
|
1491
1597
|
case "listitem":
|
|
1492
|
-
return "
|
|
1598
|
+
return "";
|
|
1493
1599
|
case "navigation":
|
|
1494
1600
|
return "navigation";
|
|
1495
1601
|
case "main":
|
|
@@ -1545,6 +1651,18 @@ function formatRoleVoiceOver(node) {
|
|
|
1545
1651
|
return "progress indicator";
|
|
1546
1652
|
case "status":
|
|
1547
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";
|
|
1548
1666
|
case "group":
|
|
1549
1667
|
return node.name ? "group" : "";
|
|
1550
1668
|
case "document":
|
|
@@ -1565,6 +1683,10 @@ function formatStatesVoiceOver(node) {
|
|
|
1565
1683
|
if (node.state.checked !== void 0) {
|
|
1566
1684
|
if (node.state.checked === "mixed") {
|
|
1567
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");
|
|
1568
1690
|
} else {
|
|
1569
1691
|
states.push(node.state.checked ? "checked" : "unchecked");
|
|
1570
1692
|
}
|
|
@@ -1573,10 +1695,10 @@ function formatStatesVoiceOver(node) {
|
|
|
1573
1695
|
if (node.state.pressed === "mixed") {
|
|
1574
1696
|
states.push("mixed");
|
|
1575
1697
|
} else {
|
|
1576
|
-
states.push(node.state.pressed ? "
|
|
1698
|
+
states.push(node.state.pressed ? "selected" : "unselected");
|
|
1577
1699
|
}
|
|
1578
1700
|
}
|
|
1579
|
-
if (node.state.selected !== void 0) {
|
|
1701
|
+
if (node.state.selected !== void 0 && node.role !== "tab") {
|
|
1580
1702
|
states.push(node.state.selected ? "selected" : "unselected");
|
|
1581
1703
|
}
|
|
1582
1704
|
if (node.state.disabled) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticular/speakable",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "CLI tool for analyzing HTML accessibility announcements",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -59,6 +59,9 @@
|
|
|
59
59
|
"url": "https://github.com/R3TICULAR/AnnounceKit/issues"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
+
"@guidepup/guidepup": "^0.38.0",
|
|
63
|
+
"@guidepup/playwright": "^0.21.0",
|
|
64
|
+
"@playwright/test": "^1.64.1",
|
|
62
65
|
"@storybook/addon-a11y": "^8.6.18",
|
|
63
66
|
"@storybook/addon-essentials": "^8.6.14",
|
|
64
67
|
"@storybook/html": "^8.6.18",
|
|
@@ -70,6 +73,7 @@
|
|
|
70
73
|
"fast-check": "^3.15.0",
|
|
71
74
|
"storybook": "^8.6.18",
|
|
72
75
|
"tsup": "^8.5.0",
|
|
76
|
+
"tsx": "^4.23.15",
|
|
73
77
|
"typescript": "^5.3.0",
|
|
74
78
|
"vitest": "^3.1.0"
|
|
75
79
|
},
|