@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
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ interface ModelVersion {
|
|
|
16
16
|
* Accessible role following ARIA specification.
|
|
17
17
|
* V1 supports common interactive and structural roles.
|
|
18
18
|
*/
|
|
19
|
-
type AccessibleRole = 'button' | 'link' | 'heading' | 'textbox' | 'checkbox' | 'radio' | 'combobox' | 'listbox' | 'option' | 'list' | 'listitem' | 'article' | 'generic' | 'navigation' | 'main' | 'banner' | 'contentinfo' | 'region' | 'img' | 'complementary' | 'form' | 'search' | 'paragraph' | 'blockquote' | 'code' | 'staticText' | 'table' | 'row' | 'cell' | 'columnheader' | 'rowheader' | 'term' | 'definition' | 'figure' | 'caption' | 'group' | 'dialog' | 'meter' | 'progressbar' | 'status' | 'document' | 'application' | 'separator';
|
|
19
|
+
type AccessibleRole = 'button' | 'link' | 'heading' | 'textbox' | 'checkbox' | 'radio' | 'combobox' | 'listbox' | 'option' | 'list' | 'listitem' | 'article' | 'generic' | 'navigation' | 'main' | 'banner' | 'contentinfo' | 'region' | 'img' | 'complementary' | 'form' | 'search' | 'paragraph' | 'blockquote' | 'code' | 'staticText' | 'table' | 'row' | 'cell' | 'columnheader' | 'rowheader' | 'term' | 'definition' | 'figure' | 'caption' | 'group' | 'dialog' | 'meter' | 'progressbar' | 'status' | 'switch' | 'slider' | 'spinbutton' | 'tab' | 'tablist' | 'tabpanel' | 'document' | 'application' | 'separator';
|
|
20
20
|
/**
|
|
21
21
|
* Accessible states and properties.
|
|
22
22
|
* Only includes properties relevant to the element's role.
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,12 @@ var SUPPORTED_ROLES = [
|
|
|
48
48
|
"meter",
|
|
49
49
|
"progressbar",
|
|
50
50
|
"status",
|
|
51
|
+
"switch",
|
|
52
|
+
"slider",
|
|
53
|
+
"spinbutton",
|
|
54
|
+
"tab",
|
|
55
|
+
"tablist",
|
|
56
|
+
"tabpanel",
|
|
51
57
|
"document",
|
|
52
58
|
"application",
|
|
53
59
|
"separator"
|
|
@@ -968,8 +974,45 @@ function renderNodeVoiceOver(node, announcements, c) {
|
|
|
968
974
|
renderNodeVoiceOver(child, announcements, c);
|
|
969
975
|
}
|
|
970
976
|
}
|
|
977
|
+
function isValueFirstWidget(role) {
|
|
978
|
+
return role === "slider" || role === "spinbutton" || role === "combobox";
|
|
979
|
+
}
|
|
980
|
+
function isValueBeforeRoleWidget(role) {
|
|
981
|
+
return role === "progressbar" || role === "meter";
|
|
982
|
+
}
|
|
983
|
+
function isSilentContainerVoiceOver(node) {
|
|
984
|
+
if (node.role !== "list" && node.role !== "status") return false;
|
|
985
|
+
if (node.name) return false;
|
|
986
|
+
return node.children.length > 0;
|
|
987
|
+
}
|
|
971
988
|
function formatNodeVoiceOver(node, c) {
|
|
989
|
+
if (isSilentContainerVoiceOver(node)) {
|
|
990
|
+
return "";
|
|
991
|
+
}
|
|
972
992
|
const parts = [];
|
|
993
|
+
const valueFirst = isValueFirstWidget(node.role) && node.value !== void 0;
|
|
994
|
+
if (valueFirst) {
|
|
995
|
+
const valueText = node.value.text || String(node.value.current);
|
|
996
|
+
if (valueText) parts.push(valueText);
|
|
997
|
+
if (node.name) parts.push(c.elementName(node.name));
|
|
998
|
+
const roleText = formatRoleVoiceOver(node);
|
|
999
|
+
if (roleText) parts.push(c.roleName(roleText));
|
|
1000
|
+
const stateText2 = formatStatesVoiceOver(node);
|
|
1001
|
+
if (stateText2) parts.push(c.stateName(stateText2));
|
|
1002
|
+
if (node.description) parts.push(c.description(node.description));
|
|
1003
|
+
return parts.join(", ");
|
|
1004
|
+
}
|
|
1005
|
+
if (isValueBeforeRoleWidget(node.role) && node.value !== void 0) {
|
|
1006
|
+
if (node.name) parts.push(c.elementName(node.name));
|
|
1007
|
+
const valueText = node.value.text || String(node.value.current);
|
|
1008
|
+
if (valueText) parts.push(valueText);
|
|
1009
|
+
const roleText = formatRoleVoiceOver(node);
|
|
1010
|
+
if (roleText) parts.push(c.roleName(roleText));
|
|
1011
|
+
const stateText2 = formatStatesVoiceOver(node);
|
|
1012
|
+
if (stateText2) parts.push(c.stateName(stateText2));
|
|
1013
|
+
if (node.description) parts.push(c.description(node.description));
|
|
1014
|
+
return parts.join(", ");
|
|
1015
|
+
}
|
|
973
1016
|
const roleFirst = shouldAnnounceRoleFirst(node.role);
|
|
974
1017
|
if (roleFirst) {
|
|
975
1018
|
const roleText = formatRoleVoiceOver(node);
|
|
@@ -998,6 +1041,9 @@ function formatNodeVoiceOver(node, c) {
|
|
|
998
1041
|
parts.push(valueText);
|
|
999
1042
|
}
|
|
1000
1043
|
}
|
|
1044
|
+
if (node.state.posinset !== void 0 && node.state.setsize !== void 0) {
|
|
1045
|
+
parts.push(`${node.state.posinset} of ${node.state.setsize}`);
|
|
1046
|
+
}
|
|
1001
1047
|
if (node.description) {
|
|
1002
1048
|
parts.push(c.description(node.description));
|
|
1003
1049
|
}
|
|
@@ -1033,7 +1079,7 @@ function formatRoleVoiceOver(node) {
|
|
|
1033
1079
|
case "list":
|
|
1034
1080
|
return "list";
|
|
1035
1081
|
case "listitem":
|
|
1036
|
-
return "
|
|
1082
|
+
return "";
|
|
1037
1083
|
case "navigation":
|
|
1038
1084
|
return "navigation";
|
|
1039
1085
|
case "main":
|
|
@@ -1089,6 +1135,18 @@ function formatRoleVoiceOver(node) {
|
|
|
1089
1135
|
return "progress indicator";
|
|
1090
1136
|
case "status":
|
|
1091
1137
|
return "status";
|
|
1138
|
+
case "switch":
|
|
1139
|
+
return "switch";
|
|
1140
|
+
case "slider":
|
|
1141
|
+
return "slider";
|
|
1142
|
+
case "spinbutton":
|
|
1143
|
+
return "stepper";
|
|
1144
|
+
case "tab":
|
|
1145
|
+
return "tab";
|
|
1146
|
+
case "tablist":
|
|
1147
|
+
return node.name ? "tab group" : "tab group";
|
|
1148
|
+
case "tabpanel":
|
|
1149
|
+
return "tab panel";
|
|
1092
1150
|
case "group":
|
|
1093
1151
|
return node.name ? "group" : "";
|
|
1094
1152
|
case "document":
|
|
@@ -1109,6 +1167,10 @@ function formatStatesVoiceOver(node) {
|
|
|
1109
1167
|
if (node.state.checked !== void 0) {
|
|
1110
1168
|
if (node.state.checked === "mixed") {
|
|
1111
1169
|
states.push("mixed");
|
|
1170
|
+
} else if (node.role === "switch") {
|
|
1171
|
+
states.push(node.state.checked ? "on" : "off");
|
|
1172
|
+
} else if (node.role === "radio") {
|
|
1173
|
+
if (node.state.checked) states.push("selected");
|
|
1112
1174
|
} else {
|
|
1113
1175
|
states.push(node.state.checked ? "checked" : "unchecked");
|
|
1114
1176
|
}
|
|
@@ -1117,10 +1179,10 @@ function formatStatesVoiceOver(node) {
|
|
|
1117
1179
|
if (node.state.pressed === "mixed") {
|
|
1118
1180
|
states.push("mixed");
|
|
1119
1181
|
} else {
|
|
1120
|
-
states.push(node.state.pressed ? "
|
|
1182
|
+
states.push(node.state.pressed ? "selected" : "unselected");
|
|
1121
1183
|
}
|
|
1122
1184
|
}
|
|
1123
|
-
if (node.state.selected !== void 0) {
|
|
1185
|
+
if (node.state.selected !== void 0 && node.role !== "tab") {
|
|
1124
1186
|
states.push(node.state.selected ? "selected" : "unselected");
|
|
1125
1187
|
}
|
|
1126
1188
|
if (node.state.disabled) {
|
|
@@ -2069,7 +2131,10 @@ var INPUT_TYPE_ROLE_MAP = {
|
|
|
2069
2131
|
"search": "textbox",
|
|
2070
2132
|
"tel": "textbox",
|
|
2071
2133
|
"url": "textbox",
|
|
2072
|
-
"number"
|
|
2134
|
+
// Per ARIA, <input type="number"> has an implicit role of spinbutton
|
|
2135
|
+
// (VoiceOver announces it as "stepper").
|
|
2136
|
+
"number": "spinbutton",
|
|
2137
|
+
"range": "slider"
|
|
2073
2138
|
};
|
|
2074
2139
|
function computeRole(element) {
|
|
2075
2140
|
const warnings = [];
|
|
@@ -2102,6 +2167,11 @@ function computeImplicitRole(element, _warnings) {
|
|
|
2102
2167
|
if (tagName === "input") {
|
|
2103
2168
|
return computeInputRole(element);
|
|
2104
2169
|
}
|
|
2170
|
+
if (tagName === "select") {
|
|
2171
|
+
const multiple = element.hasAttribute("multiple");
|
|
2172
|
+
const size = parseInt(element.getAttribute("size") || "0", 10);
|
|
2173
|
+
return multiple || size > 1 ? "listbox" : "combobox";
|
|
2174
|
+
}
|
|
2105
2175
|
if (tagName === "a") {
|
|
2106
2176
|
return element.hasAttribute("href") ? "link" : null;
|
|
2107
2177
|
}
|
|
@@ -2511,6 +2581,7 @@ function processChildNode(node, children, warnings) {
|
|
|
2511
2581
|
if (node.nodeType === 1) {
|
|
2512
2582
|
const element = node;
|
|
2513
2583
|
if (isHidden(element)) return;
|
|
2584
|
+
if (isConsumedLabel(element)) return;
|
|
2514
2585
|
if (!isAccessible(element)) {
|
|
2515
2586
|
const grandchildren = collectChildrenFromRolelessElement(element, warnings);
|
|
2516
2587
|
children.push(...grandchildren);
|
|
@@ -2520,6 +2591,25 @@ function processChildNode(node, children, warnings) {
|
|
|
2520
2591
|
if (childNode) children.push(childNode);
|
|
2521
2592
|
}
|
|
2522
2593
|
}
|
|
2594
|
+
function isConsumedLabel(element) {
|
|
2595
|
+
if (element.tagName.toLowerCase() !== "label") return false;
|
|
2596
|
+
const forId = element.getAttribute("for");
|
|
2597
|
+
if (forId) {
|
|
2598
|
+
const doc = element.ownerDocument;
|
|
2599
|
+
const target = doc?.getElementById(forId);
|
|
2600
|
+
if (target && isLabelableControl(target)) return true;
|
|
2601
|
+
}
|
|
2602
|
+
const wrapped = element.querySelector("input, select, textarea, button, meter, progress");
|
|
2603
|
+
if (wrapped && isLabelableControl(wrapped)) return true;
|
|
2604
|
+
return false;
|
|
2605
|
+
}
|
|
2606
|
+
function isLabelableControl(element) {
|
|
2607
|
+
const tag = element.tagName.toLowerCase();
|
|
2608
|
+
if (tag === "input") {
|
|
2609
|
+
return (element.getAttribute("type") || "text").toLowerCase() !== "hidden";
|
|
2610
|
+
}
|
|
2611
|
+
return tag === "select" || tag === "textarea" || tag === "button" || tag === "meter" || tag === "progress";
|
|
2612
|
+
}
|
|
2523
2613
|
function collectChildrenFromRolelessElement(element, warnings) {
|
|
2524
2614
|
return collectAccessibleChildren(element, warnings);
|
|
2525
2615
|
}
|
|
@@ -2625,6 +2715,30 @@ function buildAccessibilityTreeWithSelector(rootElement, selector, sourceHash) {
|
|
|
2625
2715
|
}
|
|
2626
2716
|
return results;
|
|
2627
2717
|
}
|
|
2718
|
+
var SET_MEMBER_ROLES = /* @__PURE__ */ new Set([
|
|
2719
|
+
"radio",
|
|
2720
|
+
"option",
|
|
2721
|
+
"listitem",
|
|
2722
|
+
"tab"
|
|
2723
|
+
]);
|
|
2724
|
+
function assignImplicitSetPosition(children) {
|
|
2725
|
+
const totals = /* @__PURE__ */ new Map();
|
|
2726
|
+
for (const child of children) {
|
|
2727
|
+
if (SET_MEMBER_ROLES.has(child.role)) {
|
|
2728
|
+
totals.set(child.role, (totals.get(child.role) ?? 0) + 1);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2732
|
+
for (const child of children) {
|
|
2733
|
+
if (!SET_MEMBER_ROLES.has(child.role)) continue;
|
|
2734
|
+
const total = totals.get(child.role);
|
|
2735
|
+
if (total < 2) continue;
|
|
2736
|
+
const pos = (seen.get(child.role) ?? 0) + 1;
|
|
2737
|
+
seen.set(child.role, pos);
|
|
2738
|
+
if (child.state.posinset === void 0) child.state.posinset = pos;
|
|
2739
|
+
if (child.state.setsize === void 0) child.state.setsize = total;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2628
2742
|
function buildNodeRecursive(element, warnings) {
|
|
2629
2743
|
if (isHidden(element)) {
|
|
2630
2744
|
return null;
|
|
@@ -2661,7 +2775,15 @@ function buildNodeRecursive(element, warnings) {
|
|
|
2661
2775
|
element: w.element
|
|
2662
2776
|
})));
|
|
2663
2777
|
const focusInfo = extractFocusInfo(element);
|
|
2664
|
-
|
|
2778
|
+
let children = collectAccessibleChildren(element, warnings);
|
|
2779
|
+
const isNativeSelect = element.tagName.toLowerCase() === "select";
|
|
2780
|
+
if (isNativeSelect && roleResult.role === "combobox") {
|
|
2781
|
+
children = [];
|
|
2782
|
+
if (stateResult.state.expanded === void 0) {
|
|
2783
|
+
stateResult.state.expanded = false;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
assignImplicitSetPosition(children);
|
|
2665
2787
|
const node = {
|
|
2666
2788
|
role: roleResult.role,
|
|
2667
2789
|
name: nameResult.name,
|