@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/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,6 +863,25 @@ 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) {
852
886
  return collectAccessibleChildren(element, warnings);
853
887
  }
@@ -953,6 +987,30 @@ function buildAccessibilityTreeWithSelector(rootElement, selector, sourceHash) {
953
987
  }
954
988
  return results;
955
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
+ }
956
1014
  function buildNodeRecursive(element, warnings) {
957
1015
  if (isHidden(element)) {
958
1016
  return null;
@@ -989,7 +1047,15 @@ function buildNodeRecursive(element, warnings) {
989
1047
  element: w.element
990
1048
  })));
991
1049
  const focusInfo = extractFocusInfo(element);
992
- const children = collectAccessibleChildren(element, 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;
1056
+ }
1057
+ }
1058
+ assignImplicitSetPosition(children);
993
1059
  const node = {
994
1060
  role: roleResult.role,
995
1061
  name: nameResult.name,
@@ -1486,8 +1552,45 @@ function renderNodeVoiceOver(node, announcements, c) {
1486
1552
  renderNodeVoiceOver(child, announcements, c);
1487
1553
  }
1488
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
+ }
1489
1566
  function formatNodeVoiceOver(node, c) {
1567
+ if (isSilentContainerVoiceOver(node)) {
1568
+ return "";
1569
+ }
1490
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
+ }
1491
1594
  const roleFirst = shouldAnnounceRoleFirst(node.role);
1492
1595
  if (roleFirst) {
1493
1596
  const roleText = formatRoleVoiceOver(node);
@@ -1516,6 +1619,9 @@ function formatNodeVoiceOver(node, c) {
1516
1619
  parts.push(valueText);
1517
1620
  }
1518
1621
  }
1622
+ if (node.state.posinset !== void 0 && node.state.setsize !== void 0) {
1623
+ parts.push(`${node.state.posinset} of ${node.state.setsize}`);
1624
+ }
1519
1625
  if (node.description) {
1520
1626
  parts.push(c.description(node.description));
1521
1627
  }
@@ -1551,7 +1657,7 @@ function formatRoleVoiceOver(node) {
1551
1657
  case "list":
1552
1658
  return "list";
1553
1659
  case "listitem":
1554
- return "item";
1660
+ return "";
1555
1661
  case "navigation":
1556
1662
  return "navigation";
1557
1663
  case "main":
@@ -1607,6 +1713,18 @@ function formatRoleVoiceOver(node) {
1607
1713
  return "progress indicator";
1608
1714
  case "status":
1609
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";
1610
1728
  case "group":
1611
1729
  return node.name ? "group" : "";
1612
1730
  case "document":
@@ -1627,6 +1745,10 @@ function formatStatesVoiceOver(node) {
1627
1745
  if (node.state.checked !== void 0) {
1628
1746
  if (node.state.checked === "mixed") {
1629
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");
1630
1752
  } else {
1631
1753
  states.push(node.state.checked ? "checked" : "unchecked");
1632
1754
  }
@@ -1635,10 +1757,10 @@ function formatStatesVoiceOver(node) {
1635
1757
  if (node.state.pressed === "mixed") {
1636
1758
  states.push("mixed");
1637
1759
  } else {
1638
- states.push(node.state.pressed ? "pressed" : "not pressed");
1760
+ states.push(node.state.pressed ? "selected" : "unselected");
1639
1761
  }
1640
1762
  }
1641
- if (node.state.selected !== void 0) {
1763
+ if (node.state.selected !== void 0 && node.role !== "tab") {
1642
1764
  states.push(node.state.selected ? "selected" : "unselected");
1643
1765
  }
1644
1766
  if (node.state.disabled) {