@reticular/speakable 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -301,6 +301,12 @@ var SUPPORTED_ROLES = [
301
301
  "meter",
302
302
  "progressbar",
303
303
  "status",
304
+ "switch",
305
+ "slider",
306
+ "spinbutton",
307
+ "tab",
308
+ "tablist",
309
+ "tabpanel",
304
310
  "document",
305
311
  "application",
306
312
  "separator"
@@ -397,7 +403,10 @@ var INPUT_TYPE_ROLE_MAP = {
397
403
  "search": "textbox",
398
404
  "tel": "textbox",
399
405
  "url": "textbox",
400
- "number": "textbox"
406
+ // Per ARIA, <input type="number"> has an implicit role of spinbutton
407
+ // (VoiceOver announces it as "stepper").
408
+ "number": "spinbutton",
409
+ "range": "slider"
401
410
  };
402
411
  function computeRole(element) {
403
412
  const warnings = [];
@@ -430,6 +439,11 @@ function computeImplicitRole(element, _warnings) {
430
439
  if (tagName === "input") {
431
440
  return computeInputRole(element);
432
441
  }
442
+ if (tagName === "select") {
443
+ const multiple = element.hasAttribute("multiple");
444
+ const size = parseInt(element.getAttribute("size") || "0", 10);
445
+ return multiple || size > 1 ? "listbox" : "combobox";
446
+ }
433
447
  if (tagName === "a") {
434
448
  return element.hasAttribute("href") ? "link" : null;
435
449
  }
@@ -839,6 +853,7 @@ function processChildNode(node, children, warnings) {
839
853
  if (node.nodeType === 1) {
840
854
  const element = node;
841
855
  if (isHidden(element)) return;
856
+ if (isConsumedLabel(element)) return;
842
857
  if (!isAccessible(element)) {
843
858
  const grandchildren = collectChildrenFromRolelessElement(element, warnings);
844
859
  children.push(...grandchildren);
@@ -848,18 +863,74 @@ function processChildNode(node, children, warnings) {
848
863
  if (childNode) children.push(childNode);
849
864
  }
850
865
  }
866
+ function isConsumedLabel(element) {
867
+ if (element.tagName.toLowerCase() !== "label") return false;
868
+ const forId = element.getAttribute("for");
869
+ if (forId) {
870
+ const doc = element.ownerDocument;
871
+ const target = doc?.getElementById(forId);
872
+ if (target && isLabelableControl(target)) return true;
873
+ }
874
+ const wrapped = element.querySelector("input, select, textarea, button, meter, progress");
875
+ if (wrapped && isLabelableControl(wrapped)) return true;
876
+ return false;
877
+ }
878
+ function isLabelableControl(element) {
879
+ const tag = element.tagName.toLowerCase();
880
+ if (tag === "input") {
881
+ return (element.getAttribute("type") || "text").toLowerCase() !== "hidden";
882
+ }
883
+ return tag === "select" || tag === "textarea" || tag === "button" || tag === "meter" || tag === "progress";
884
+ }
851
885
  function collectChildrenFromRolelessElement(element, warnings) {
886
+ return collectAccessibleChildren(element, warnings);
887
+ }
888
+ function collectAccessibleChildren(element, warnings) {
852
889
  const children = [];
853
890
  if (element.shadowRoot) {
854
891
  for (const child of Array.from(element.shadowRoot.childNodes)) {
855
- processChildNode(child, children, warnings);
892
+ processShadowNode(child, children, warnings);
856
893
  }
894
+ return children;
895
+ }
896
+ if (element.localName && element.localName.includes("-") && element.childNodes.length === 0) {
897
+ warnings.push({
898
+ message: `Custom element <${element.localName}> has no accessible children and may have a closed shadow root; its content could not be analyzed.`,
899
+ element
900
+ });
857
901
  }
858
902
  for (const child of Array.from(element.childNodes)) {
859
903
  processChildNode(child, children, warnings);
860
904
  }
861
905
  return children;
862
906
  }
907
+ function processShadowNode(node, children, warnings) {
908
+ if (node.nodeType === 1) {
909
+ const element = node;
910
+ if (element.localName === "slot") {
911
+ const slot = element;
912
+ let assigned = [];
913
+ if (typeof slot.assignedNodes === "function") {
914
+ try {
915
+ assigned = Array.from(slot.assignedNodes({ flatten: true }));
916
+ } catch {
917
+ assigned = [];
918
+ }
919
+ }
920
+ if (assigned.length > 0) {
921
+ for (const assignedNode of assigned) {
922
+ processChildNode(assignedNode, children, warnings);
923
+ }
924
+ } else {
925
+ for (const child of Array.from(slot.childNodes)) {
926
+ processShadowNode(child, children, warnings);
927
+ }
928
+ }
929
+ return;
930
+ }
931
+ }
932
+ processChildNode(node, children, warnings);
933
+ }
863
934
  function buildAccessibilityTree(rootElement, sourceHash) {
864
935
  const warnings = [];
865
936
  if (isHidden(rootElement)) {
@@ -916,6 +987,30 @@ function buildAccessibilityTreeWithSelector(rootElement, selector, sourceHash) {
916
987
  }
917
988
  return results;
918
989
  }
990
+ var SET_MEMBER_ROLES = /* @__PURE__ */ new Set([
991
+ "radio",
992
+ "option",
993
+ "listitem",
994
+ "tab"
995
+ ]);
996
+ function assignImplicitSetPosition(children) {
997
+ const totals = /* @__PURE__ */ new Map();
998
+ for (const child of children) {
999
+ if (SET_MEMBER_ROLES.has(child.role)) {
1000
+ totals.set(child.role, (totals.get(child.role) ?? 0) + 1);
1001
+ }
1002
+ }
1003
+ const seen = /* @__PURE__ */ new Map();
1004
+ for (const child of children) {
1005
+ if (!SET_MEMBER_ROLES.has(child.role)) continue;
1006
+ const total = totals.get(child.role);
1007
+ if (total < 2) continue;
1008
+ const pos = (seen.get(child.role) ?? 0) + 1;
1009
+ seen.set(child.role, pos);
1010
+ if (child.state.posinset === void 0) child.state.posinset = pos;
1011
+ if (child.state.setsize === void 0) child.state.setsize = total;
1012
+ }
1013
+ }
919
1014
  function buildNodeRecursive(element, warnings) {
920
1015
  if (isHidden(element)) {
921
1016
  return null;
@@ -952,15 +1047,15 @@ function buildNodeRecursive(element, warnings) {
952
1047
  element: w.element
953
1048
  })));
954
1049
  const focusInfo = extractFocusInfo(element);
955
- const children = [];
956
- if (element.shadowRoot) {
957
- for (const child of Array.from(element.shadowRoot.childNodes)) {
958
- processChildNode(child, children, warnings);
1050
+ let children = collectAccessibleChildren(element, warnings);
1051
+ const isNativeSelect = element.tagName.toLowerCase() === "select";
1052
+ if (isNativeSelect && roleResult.role === "combobox") {
1053
+ children = [];
1054
+ if (stateResult.state.expanded === void 0) {
1055
+ stateResult.state.expanded = false;
959
1056
  }
960
1057
  }
961
- for (const child of Array.from(element.childNodes)) {
962
- processChildNode(child, children, warnings);
963
- }
1058
+ assignImplicitSetPosition(children);
964
1059
  const node = {
965
1060
  role: roleResult.role,
966
1061
  name: nameResult.name,
@@ -973,15 +1068,7 @@ function buildNodeRecursive(element, warnings) {
973
1068
  return node;
974
1069
  }
975
1070
  function createGenericContainer(rootElement, warnings) {
976
- const children = [];
977
- if (rootElement.shadowRoot) {
978
- for (const child of Array.from(rootElement.shadowRoot.childNodes)) {
979
- processChildNode(child, children, warnings);
980
- }
981
- }
982
- for (const child of Array.from(rootElement.childNodes)) {
983
- processChildNode(child, children, warnings);
984
- }
1071
+ const children = collectAccessibleChildren(rootElement, warnings);
985
1072
  return {
986
1073
  role: "generic",
987
1074
  name: "",
@@ -1465,8 +1552,45 @@ function renderNodeVoiceOver(node, announcements, c) {
1465
1552
  renderNodeVoiceOver(child, announcements, c);
1466
1553
  }
1467
1554
  }
1555
+ function isValueFirstWidget(role) {
1556
+ return role === "slider" || role === "spinbutton" || role === "combobox";
1557
+ }
1558
+ function isValueBeforeRoleWidget(role) {
1559
+ return role === "progressbar" || role === "meter";
1560
+ }
1561
+ function isSilentContainerVoiceOver(node) {
1562
+ if (node.role !== "list" && node.role !== "status") return false;
1563
+ if (node.name) return false;
1564
+ return node.children.length > 0;
1565
+ }
1468
1566
  function formatNodeVoiceOver(node, c) {
1567
+ if (isSilentContainerVoiceOver(node)) {
1568
+ return "";
1569
+ }
1469
1570
  const parts = [];
1571
+ const valueFirst = isValueFirstWidget(node.role) && node.value !== void 0;
1572
+ if (valueFirst) {
1573
+ const valueText = node.value.text || String(node.value.current);
1574
+ if (valueText) parts.push(valueText);
1575
+ if (node.name) parts.push(c.elementName(node.name));
1576
+ const roleText = formatRoleVoiceOver(node);
1577
+ if (roleText) parts.push(c.roleName(roleText));
1578
+ const stateText2 = formatStatesVoiceOver(node);
1579
+ if (stateText2) parts.push(c.stateName(stateText2));
1580
+ if (node.description) parts.push(c.description(node.description));
1581
+ return parts.join(", ");
1582
+ }
1583
+ if (isValueBeforeRoleWidget(node.role) && node.value !== void 0) {
1584
+ if (node.name) parts.push(c.elementName(node.name));
1585
+ const valueText = node.value.text || String(node.value.current);
1586
+ if (valueText) parts.push(valueText);
1587
+ const roleText = formatRoleVoiceOver(node);
1588
+ if (roleText) parts.push(c.roleName(roleText));
1589
+ const stateText2 = formatStatesVoiceOver(node);
1590
+ if (stateText2) parts.push(c.stateName(stateText2));
1591
+ if (node.description) parts.push(c.description(node.description));
1592
+ return parts.join(", ");
1593
+ }
1470
1594
  const roleFirst = shouldAnnounceRoleFirst(node.role);
1471
1595
  if (roleFirst) {
1472
1596
  const roleText = formatRoleVoiceOver(node);
@@ -1495,6 +1619,9 @@ function formatNodeVoiceOver(node, c) {
1495
1619
  parts.push(valueText);
1496
1620
  }
1497
1621
  }
1622
+ if (node.state.posinset !== void 0 && node.state.setsize !== void 0) {
1623
+ parts.push(`${node.state.posinset} of ${node.state.setsize}`);
1624
+ }
1498
1625
  if (node.description) {
1499
1626
  parts.push(c.description(node.description));
1500
1627
  }
@@ -1530,7 +1657,7 @@ function formatRoleVoiceOver(node) {
1530
1657
  case "list":
1531
1658
  return "list";
1532
1659
  case "listitem":
1533
- return "item";
1660
+ return "";
1534
1661
  case "navigation":
1535
1662
  return "navigation";
1536
1663
  case "main":
@@ -1586,6 +1713,18 @@ function formatRoleVoiceOver(node) {
1586
1713
  return "progress indicator";
1587
1714
  case "status":
1588
1715
  return "status";
1716
+ case "switch":
1717
+ return "switch";
1718
+ case "slider":
1719
+ return "slider";
1720
+ case "spinbutton":
1721
+ return "stepper";
1722
+ case "tab":
1723
+ return "tab";
1724
+ case "tablist":
1725
+ return node.name ? "tab group" : "tab group";
1726
+ case "tabpanel":
1727
+ return "tab panel";
1589
1728
  case "group":
1590
1729
  return node.name ? "group" : "";
1591
1730
  case "document":
@@ -1606,6 +1745,10 @@ function formatStatesVoiceOver(node) {
1606
1745
  if (node.state.checked !== void 0) {
1607
1746
  if (node.state.checked === "mixed") {
1608
1747
  states.push("mixed");
1748
+ } else if (node.role === "switch") {
1749
+ states.push(node.state.checked ? "on" : "off");
1750
+ } else if (node.role === "radio") {
1751
+ if (node.state.checked) states.push("selected");
1609
1752
  } else {
1610
1753
  states.push(node.state.checked ? "checked" : "unchecked");
1611
1754
  }
@@ -1614,10 +1757,10 @@ function formatStatesVoiceOver(node) {
1614
1757
  if (node.state.pressed === "mixed") {
1615
1758
  states.push("mixed");
1616
1759
  } else {
1617
- states.push(node.state.pressed ? "pressed" : "not pressed");
1760
+ states.push(node.state.pressed ? "selected" : "unselected");
1618
1761
  }
1619
1762
  }
1620
- if (node.state.selected !== void 0) {
1763
+ if (node.state.selected !== void 0 && node.role !== "tab") {
1621
1764
  states.push(node.state.selected ? "selected" : "unselected");
1622
1765
  }
1623
1766
  if (node.state.disabled) {