@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/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 "item";
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 ? "pressed" : "not 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": "textbox"
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
- const children = collectAccessibleChildren(element, warnings);
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,