@guidepup/virtual-screen-reader
Version:
Virtual Screen Reader driver for unit test automation.
1,746 lines (1,705 loc) • 89.4 kB
JavaScript
// src/getIdRefsByAttribute.ts
function getIdRefsByAttribute({
attributeName,
node
}) {
return (node.getAttribute(attributeName) ?? "").trim().split(" ").filter(Boolean);
}
// src/getNodeAccessibilityData/index.ts
import { roles } from "html-aria";
// src/getNodeAccessibilityData/getRole.ts
import { ALL_ROLES, getRole as getHtmlAriaRole } from "html-aria";
// src/getLocalName.ts
var getLocalName = (element) => element.localName ?? element.tagName.toLowerCase();
// src/isElement.ts
var ELEMENT_NODE = 1;
function isElement(node) {
return node.nodeType === ELEMENT_NODE;
}
// src/getNodeAccessibilityData/getRole.ts
var presentationRoles = /* @__PURE__ */ new Set(["presentation", "none"]);
var synonymRolesMap = {
img: "image",
presentation: "none",
directory: "list"
};
var allowedNonAbstractRoles = new Set(ALL_ROLES);
var rolesRequiringName = /* @__PURE__ */ new Set(["form", "region"]);
var globalStatesAndProperties = [
"aria-atomic",
"aria-braillelabel",
"aria-brailleroledescription",
"aria-busy",
"aria-controls",
"aria-describedby",
"aria-description",
"aria-details",
"aria-dropeffect",
"aria-flowto",
"aria-grabbed",
"aria-hidden",
"aria-keyshortcuts",
"aria-label",
"aria-labelledby",
"aria-live",
"aria-owns",
"aria-relevant",
"aria-roledescription"
];
var FOCUSABLE_SELECTOR = [
"input:not([type=hidden]):not([disabled])",
"button:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
'[contenteditable=""]',
'[contenteditable="true"]',
"a[href]",
"[tabindex]:not([disabled])"
].join(", ");
function isFocusable(node) {
return node.matches(FOCUSABLE_SELECTOR);
}
function hasGlobalStateOrProperty(node) {
return globalStatesAndProperties.some((global) => node.hasAttribute(global));
}
function mapAliasedRoles(role) {
const canonical = synonymRolesMap[role];
return canonical ?? role;
}
function getExplicitRole({
accessibleName,
allowedAccessibilityRoles,
inheritedImplicitPresentational,
node
}) {
const rawRoles = node.getAttribute("role")?.trim().split(" ") ?? [];
const authorErrorFilteredRoles = rawRoles.filter((role) => allowedNonAbstractRoles.has(role)).filter((role) => !!accessibleName || !rolesRequiringName.has(role));
const isExplicitAllowedChildElement = allowedAccessibilityRoles.some(
(allowedExplicitRole) => authorErrorFilteredRoles?.[0] === allowedExplicitRole
);
if (inheritedImplicitPresentational && !isExplicitAllowedChildElement) {
authorErrorFilteredRoles.unshift("none");
}
if (!authorErrorFilteredRoles?.length) {
return "";
}
const filteredRoles = authorErrorFilteredRoles.filter((role) => {
if (!presentationRoles.has(role)) {
return true;
}
if (hasGlobalStateOrProperty(node) || isFocusable(node)) {
return false;
}
return true;
});
return filteredRoles?.[0] ?? "";
}
function getRole({
accessibleName,
allowedAccessibilityRoles,
inheritedImplicitPresentational,
node
}) {
if (!isElement(node)) {
return { explicitRole: "", implicitRole: "", role: "" };
}
const baseExplicitRole = getExplicitRole({
accessibleName,
allowedAccessibilityRoles,
inheritedImplicitPresentational,
node
});
const explicitRole = mapAliasedRoles(baseExplicitRole);
if ("computedRole" in node) {
const role = node.computedRole;
return { explicitRole, implicitRole: role, role };
}
const isBodyElement = getLocalName(node) === "body";
const baseImplicitRole = isBodyElement ? "document" : getHtmlAriaRole(node, { ignoreRoleAttribute: true })?.name ?? "";
const implicitRole = mapAliasedRoles(baseImplicitRole);
if (explicitRole) {
return { explicitRole, implicitRole, role: explicitRole };
}
return {
explicitRole,
implicitRole,
role: implicitRole
};
}
// src/getNodeAccessibilityData/getAccessibleDescription.ts
import { computeAccessibleDescription } from "dom-accessibility-api";
function getAccessibleDescription(node) {
return isElement(node) ? computeAccessibleDescription(node).trim() : "";
}
// src/getNodeAccessibilityData/getAccessibleName.ts
import { computeAccessibleName } from "dom-accessibility-api";
// src/sanitizeString.ts
function sanitizeString(string2) {
return string2.trim().replace(/\s+/g, " ");
}
// src/getNodeAccessibilityData/getAccessibleName.ts
function getAccessibleName(node) {
if ("computedName" in node) {
return node.computedName;
}
return isElement(node) ? computeAccessibleName(node).trim() : (
// `node.textContent` is only `null` for `document` and `doctype`.
sanitizeString(node.textContent)
);
}
// src/getNodeAccessibilityData/getAccessibleValue.ts
var ignoredInputTypes = /* @__PURE__ */ new Set(["checkbox", "radio"]);
var allowedLocalNames = /* @__PURE__ */ new Set([
"button",
"data",
"input",
// "li",
"meter",
"option",
"progress",
"param"
]);
function getSelectValue(node) {
const selectedOptions = [...node.options].filter(
(optionElement) => optionElement.selected
);
if (node.multiple) {
return [...selectedOptions].map((optionElement) => getValue(optionElement)).join("; ");
}
if (selectedOptions.length === 0) {
return "";
}
return getValue(selectedOptions[0]);
}
function getInputValue(node) {
if (ignoredInputTypes.has(node.type)) {
return "";
}
return getValue(node);
}
function getValue(node) {
const localName = getLocalName(node);
if (!allowedLocalNames.has(localName)) {
return "";
}
if (node.getAttribute("aria-valuetext") || node.getAttribute("aria-valuenow")) {
return "";
}
return typeof node.value === "number" ? `${node.value}` : node.value;
}
function getAccessibleValue(node) {
if (!isElement(node)) {
return "";
}
switch (getLocalName(node)) {
case "input": {
return getInputValue(node);
}
case "select": {
return getSelectValue(node);
}
}
return getValue(node);
}
// src/isDialogRole.ts
var dialogRoles = /* @__PURE__ */ new Set(["dialog", "alertdialog"]);
var isDialogRole = (role) => dialogRoles.has(role);
// src/getNodeAccessibilityData/index.ts
var childrenPresentationalRoles = new Set(
Object.entries(roles).filter(([, { childrenPresentational }]) => childrenPresentational).map(([key]) => key)
);
var getSpokenRole = ({
isGeneric,
isPresentational,
node,
role
}) => {
if (isPresentational || isGeneric) {
return "";
}
if (isElement(node)) {
const roledescription = node.getAttribute("aria-roledescription");
if (roledescription) {
return roledescription;
}
}
return role;
};
var getIsInert = ({
inheritedImplicitInert,
node,
role
}) => {
if (!isElement(node)) {
return inheritedImplicitInert;
}
const isNativeModalDialog = getLocalName(node) === "dialog" && node.hasAttribute("open");
const isNonNativeModalDialog = isDialogRole(role) && node.hasAttribute("aria-modal");
const isModalDialog = isNonNativeModalDialog || isNativeModalDialog;
const isExplicitInert = node.hasAttribute("inert");
return isExplicitInert || inheritedImplicitInert && !isModalDialog;
};
function getNodeAccessibilityData({
allowedAccessibilityRoles,
inheritedImplicitInert,
inheritedImplicitPresentational,
node
}) {
const accessibleDescription = getAccessibleDescription(node);
const accessibleName = getAccessibleName(node);
const accessibleValue = getAccessibleValue(node);
const { explicitRole, implicitRole, role } = getRole({
accessibleName,
allowedAccessibilityRoles,
inheritedImplicitPresentational,
node
});
const amendedAccessibleDescription = accessibleDescription === accessibleName ? "" : accessibleDescription;
const isExplicitPresentational = presentationRoles.has(explicitRole);
const isPresentational = presentationRoles.has(role);
const isGeneric = role === "generic";
const spokenRole = getSpokenRole({
isGeneric,
isPresentational,
node,
role
});
const { allowedChildRoles: allowedAccessibilityChildRoles } = roles[role] ?? {
allowedChildRoles: []
};
const { allowedChildRoles: implicitAllowedAccessibilityChildRoles } = roles[implicitRole] ?? {
allowedChildRoles: []
};
const isChildrenPresentationalRole = childrenPresentationalRoles.has(role);
const isExplicitOrInheritedPresentation = isExplicitPresentational || inheritedImplicitPresentational;
const isElementWithImplicitAllowedAccessibilityChildRoles = !!implicitAllowedAccessibilityChildRoles.length;
const childrenInheritPresentationExceptAllowedRoles = isExplicitOrInheritedPresentation && isElementWithImplicitAllowedAccessibilityChildRoles;
const childrenPresentational = isChildrenPresentationalRole || childrenInheritPresentationExceptAllowedRoles;
const isInert = getIsInert({
inheritedImplicitInert,
node,
role
});
return {
accessibleDescription: amendedAccessibleDescription,
accessibleName,
accessibleValue,
allowedAccessibilityChildRoles,
childrenPresentational,
isExplicitPresentational,
isInert,
role,
spokenRole
};
}
// src/getNodeByIdRef.ts
function getNodeByIdRef({
container,
idRef: idRef2
}) {
if (!isElement(container) || !idRef2) {
return null;
}
return container.querySelector(`#${CSS.escape(idRef2)}`);
}
// src/isHiddenFromAccessibilityTree.ts
var TEXT_NODE = 3;
function isHiddenFromAccessibilityTree(node) {
if (!node) {
return true;
}
if (node.nodeType === TEXT_NODE && node.textContent.trim()) {
return false;
}
if (!isElement(node)) {
return true;
}
try {
if (node.hidden === true) {
return true;
}
if (node.getAttribute("aria-hidden") === "true") {
return true;
}
const getComputedStyle = node.ownerDocument.defaultView?.getComputedStyle;
const computedStyle = getComputedStyle?.(node);
if (computedStyle?.visibility === "hidden" || computedStyle?.display === "none") {
return true;
}
} catch {
return true;
}
return false;
}
// src/createAccessibilityTree.ts
function addAlternateReadingOrderNodes(node, alternateReadingOrderMap, container) {
const idRefs2 = getIdRefsByAttribute({
attributeName: "aria-flowto",
node
});
idRefs2.forEach((idRef2) => {
const childNode = getNodeByIdRef({ container, idRef: idRef2 });
if (!childNode) {
return;
}
const currentParentNodes = alternateReadingOrderMap.get(childNode) ?? /* @__PURE__ */ new Set();
currentParentNodes.add(node);
alternateReadingOrderMap.set(childNode, currentParentNodes);
});
}
function mapAlternateReadingOrder(node) {
const alternateReadingOrderMap = /* @__PURE__ */ new Map();
if (!isElement(node)) {
return alternateReadingOrderMap;
}
node.querySelectorAll("[aria-flowto]").forEach(
(parentNode) => addAlternateReadingOrderNodes(parentNode, alternateReadingOrderMap, node)
);
return alternateReadingOrderMap;
}
function addOwnedNodes(node, ownedNodes, container) {
const idRefs2 = getIdRefsByAttribute({
attributeName: "aria-owns",
node
});
idRefs2.forEach((idRef2) => {
const ownedNode = getNodeByIdRef({ container, idRef: idRef2 });
if (!!ownedNode && !ownedNodes.has(ownedNode)) {
ownedNodes.add(ownedNode);
}
});
}
function getAllOwnedNodes(node) {
const ownedNodes = /* @__PURE__ */ new Set();
if (!isElement(node)) {
return ownedNodes;
}
node.querySelectorAll("[aria-owns]").forEach((owningNode) => addOwnedNodes(owningNode, ownedNodes, node));
return ownedNodes;
}
function getOwnedNodes(node, container) {
const ownedNodes = /* @__PURE__ */ new Set();
if (!isElement(node) || !isElement(container)) {
return ownedNodes;
}
addOwnedNodes(node, ownedNodes, container);
return ownedNodes;
}
function growTree(node, tree, {
alternateReadingOrderMap,
container,
ownedNodes,
visitedNodes
}) {
if (visitedNodes.has(node)) {
return tree;
}
visitedNodes.add(node);
const parentDialog = isDialogRole(tree.role) ? tree.node : tree.parentDialog;
if (parentDialog) {
tree.parentDialog = parentDialog;
}
node.childNodes.forEach((childNode) => {
if (isHiddenFromAccessibilityTree(childNode)) {
return;
}
if (ownedNodes.has(childNode)) {
return;
}
const alternateReadingOrderParents = alternateReadingOrderMap.has(childNode) ? (
// `alternateReadingOrderMap.has(childNode)` null guards here.
Array.from(alternateReadingOrderMap.get(childNode))
) : [];
const {
accessibleDescription,
accessibleName,
accessibleValue,
allowedAccessibilityChildRoles,
childrenPresentational,
isExplicitPresentational,
isInert,
role,
spokenRole
} = getNodeAccessibilityData({
allowedAccessibilityRoles: tree.allowedAccessibilityChildRoles,
inheritedImplicitInert: tree.isInert,
inheritedImplicitPresentational: tree.childrenPresentational,
node: childNode
});
const childTree = growTree(
childNode,
{
accessibleDescription,
accessibleName,
accessibleValue,
allowedAccessibilityChildRoles,
alternateReadingOrderParents,
children: [],
childrenPresentational,
isInert,
node: childNode,
parentAccessibilityNodeTree: null,
// Added during flattening
parent: node,
parentDialog,
role,
spokenRole
},
{ alternateReadingOrderMap, container, ownedNodes, visitedNodes }
);
if (isExplicitPresentational) {
tree.children.push(...childTree.children);
} else {
tree.children.push(childTree);
}
});
const ownedChildNodes = getOwnedNodes(node, container);
ownedChildNodes.forEach((childNode) => {
if (isHiddenFromAccessibilityTree(childNode)) {
return;
}
const alternateReadingOrderParents = alternateReadingOrderMap.has(childNode) ? (
// `alternateReadingOrderMap.has(childNode)` null guards here.
Array.from(alternateReadingOrderMap.get(childNode))
) : [];
const {
accessibleDescription,
accessibleName,
accessibleValue,
allowedAccessibilityChildRoles,
childrenPresentational,
isInert,
isExplicitPresentational,
role,
spokenRole
} = getNodeAccessibilityData({
allowedAccessibilityRoles: tree.allowedAccessibilityChildRoles,
inheritedImplicitInert: tree.isInert,
inheritedImplicitPresentational: tree.childrenPresentational,
node: childNode
});
const childTree = growTree(
childNode,
{
accessibleDescription,
accessibleName,
accessibleValue,
allowedAccessibilityChildRoles,
alternateReadingOrderParents,
children: [],
childrenPresentational,
isInert,
node: childNode,
parentAccessibilityNodeTree: null,
// Added during flattening
parent: node,
parentDialog,
role,
spokenRole
},
{ alternateReadingOrderMap, container, ownedNodes, visitedNodes }
);
if (isExplicitPresentational) {
tree.children.push(...childTree.children);
} else {
tree.children.push(childTree);
}
});
return tree;
}
function createAccessibilityTree(node) {
if (isHiddenFromAccessibilityTree(node)) {
return null;
}
const alternateReadingOrderMap = mapAlternateReadingOrder(node);
const ownedNodes = getAllOwnedNodes(node);
const visitedNodes = /* @__PURE__ */ new Set();
const {
accessibleDescription,
accessibleName,
accessibleValue,
allowedAccessibilityChildRoles,
childrenPresentational,
isInert,
role,
spokenRole
} = getNodeAccessibilityData({
allowedAccessibilityRoles: [],
node,
inheritedImplicitPresentational: false,
inheritedImplicitInert: false
});
const tree = growTree(
node,
{
accessibleDescription,
accessibleName,
accessibleValue,
allowedAccessibilityChildRoles,
alternateReadingOrderParents: [],
children: [],
childrenPresentational,
isInert,
node,
parentAccessibilityNodeTree: null,
parent: null,
parentDialog: null,
role,
spokenRole
},
{
alternateReadingOrderMap,
container: node,
ownedNodes,
visitedNodes
}
);
return tree;
}
// src/commands/nodeMatchers.ts
function matchesRoles(node, roles3) {
if (!roles3?.length) {
return true;
}
return roles3.includes(node.role);
}
function matchesAccessibleAttributes(node, ariaAttributes) {
if (!ariaAttributes) {
return true;
}
for (const [name, value] of Object.entries(ariaAttributes)) {
if (node.accessibleAttributeToLabelMap[name]?.value !== value) {
return false;
}
}
return true;
}
// src/getNodeAccessibilityData/getAccessibleAttributeLabels/getAttributesByRole.ts
import { roles as roles2 } from "html-aria";
var ignoreAttributesWithAccessibleValue = /* @__PURE__ */ new Set(["aria-placeholder"]);
var nonSpecCompliantAttributeMap = {
listitem: { "aria-level": null },
option: { "aria-selected": false }
};
var getAttributesByRole = ({
accessibleValue,
role
}) => {
const {
supported: supportedAttributes = [],
defaultAttributeValues = {},
prohibited: prohibitedAttributes = []
} = roles2[role] ?? {};
const implicitRoleAttributes = {
...defaultAttributeValues,
...nonSpecCompliantAttributeMap[role]
};
const uniqueAttributes = Array.from(
/* @__PURE__ */ new Set([
...Object.keys(implicitRoleAttributes),
...supportedAttributes,
...globalStatesAndProperties
])
).filter(
(attribute) => !prohibitedAttributes.includes(attribute)
).filter(
(attribute) => !accessibleValue || !ignoreAttributesWithAccessibleValue.has(attribute)
);
return uniqueAttributes.map((attribute) => [
attribute,
attribute in implicitRoleAttributes && implicitRoleAttributes[attribute] !== null ? implicitRoleAttributes[attribute].toString() : null
]);
};
// src/getItemText.ts
var getItemText = (accessibilityNode) => {
const { accessibleName, accessibleValue } = accessibilityNode;
const announcedValue = accessibleName === accessibleValue ? "" : accessibleValue;
return [accessibleName, announcedValue].filter(Boolean).join(", ");
};
// src/getNodeAccessibilityData/getAccessibleAttributeLabels/mapAttributeNameAndValueToLabel.ts
var STATE = {
BUSY: "busy",
CHECKED: "checked",
CURRENT: "current item",
DISABLED: "disabled",
EXPANDED: "expanded",
INVALID: "invalid",
MODAL: "modal",
MULTI_SELECTABLE: "multi-selectable",
PARTIALLY_CHECKED: "partially checked",
PARTIALLY_PRESSED: "partially pressed",
PRESSED: "pressed",
READ_ONLY: "read only",
REQUIRED: "required",
SELECTED: "selected"
};
var ariaPropertyToVirtualLabelMap = {
"aria-activedescendant": idRef("active descendant"),
"aria-atomic": null,
// Handled by live region logic
"aria-autocomplete": token({
inline: "autocomplete inlined",
list: "autocomplete in list",
both: "autocomplete inlined and in list",
none: "no autocomplete"
}),
"aria-braillelabel": null,
// Currently won't do - not implementing a braille screen reader
"aria-brailleroledescription": null,
// Currently won't do - not implementing a braille screen reader
"aria-busy": state(STATE.BUSY),
"aria-checked": tristate(STATE.CHECKED, STATE.PARTIALLY_CHECKED),
"aria-colcount": integer("column count"),
"aria-colindex": integer("column index"),
"aria-colindextext": string("column index"),
"aria-colspan": integer("column span"),
"aria-controls": idRefs("control", "controls"),
// Handled by virtual.perform()
"aria-current": token({
page: "current page",
step: "current step",
location: "current location",
date: "current date",
time: "current time",
true: STATE.CURRENT,
false: `not ${STATE.CURRENT}`
}),
"aria-describedby": null,
// Handled by accessible description
"aria-description": null,
// Handled by accessible description
"aria-details": idRefs("linked details", "linked details", false),
"aria-disabled": state(STATE.DISABLED),
"aria-dropeffect": null,
// Deprecated in WAI-ARIA 1.1
"aria-errormessage": errorMessageIdRefs("error message", "error messages"),
"aria-expanded": state(STATE.EXPANDED),
"aria-flowto": idRefs("alternate reading order", "alternate reading orders"),
// Handled by virtual.perform()
"aria-grabbed": null,
// Deprecated in WAI-ARIA 1.1
"aria-haspopup": token({
/**
* Assistive technologies SHOULD NOT expose the aria-haspopup property if
* it has a value of false.
*
* REF: // https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup
*/
false: null,
true: "has popup menu",
menu: "has popup menu",
listbox: "has popup listbox",
tree: "has popup tree",
grid: "has popup grid",
dialog: "has popup dialog"
}),
"aria-hidden": null,
// Excluded from accessibility tree
"aria-invalid": token({
grammar: "grammatical error detected",
false: `not ${STATE.INVALID}`,
spelling: "spelling error detected",
true: STATE.INVALID
}),
"aria-keyshortcuts": string("key shortcuts"),
"aria-label": null,
// Handled by accessible name
"aria-labelledby": null,
// Handled by accessible name
"aria-level": integer("level"),
"aria-live": null,
// Handled by live region logic
"aria-modal": state(STATE.MODAL),
"aria-multiselectable": state(STATE.MULTI_SELECTABLE),
"aria-orientation": token({
horizontal: "orientated horizontally",
vertical: "orientated vertically"
}),
"aria-owns": null,
// Handled by accessibility tree construction
"aria-placeholder": string("placeholder"),
"aria-posinset": integer("position"),
"aria-pressed": tristate(STATE.PRESSED, STATE.PARTIALLY_PRESSED),
"aria-readonly": state(STATE.READ_ONLY),
"aria-relevant": null,
// Handled by live region logic
"aria-required": state(STATE.REQUIRED),
"aria-roledescription": null,
// Handled by accessible description
"aria-rowcount": integer("row count"),
"aria-rowindex": integer("row index"),
"aria-rowindextext": string("row index"),
"aria-rowspan": integer("row span"),
"aria-selected": state(STATE.SELECTED),
"aria-setsize": integer("set size"),
"aria-sort": token({
ascending: "sorted in ascending order",
descending: "sorted in descending order",
none: "no defined sort order",
other: "non ascending / descending sort order applied"
}),
"aria-valuemax": number("max value"),
"aria-valuemin": number("min value"),
"aria-valuenow": number("current value"),
"aria-valuetext": string("current value")
};
function state(stateValue) {
return function stateMapper({ attributeValue, negative }) {
if (negative) {
return attributeValue !== "false" ? `not ${stateValue}` : stateValue;
}
return attributeValue !== "false" ? stateValue : `not ${stateValue}`;
};
}
function errorMessageIdRefs(propertyDescriptionSuffixSingular, propertyDescriptionSuffixPlural, printCount = true) {
return function mapper({ attributeValue, container, node }) {
if (node?.getAttribute("aria-invalid") === "false") {
return "";
}
return idRefs(
propertyDescriptionSuffixSingular,
propertyDescriptionSuffixPlural,
printCount
)({ attributeValue, container });
};
}
function idRefs(propertyDescriptionSuffixSingular, propertyDescriptionSuffixPlural, printCount = true) {
return function mapper({ attributeValue, container }) {
const idRefsCount = attributeValue.trim().split(" ").filter(
(idRef2) => !!container && !!getNodeByIdRef({ container, idRef: idRef2 })
).length;
if (idRefsCount === 0) {
return "";
}
return `${printCount ? `${idRefsCount} ` : ""}${idRefsCount === 1 ? propertyDescriptionSuffixSingular : propertyDescriptionSuffixPlural}`;
};
}
function idRef(propertyName) {
return function mapper({ attributeValue: idRef2, container }) {
const node = getNodeByIdRef({ container, idRef: idRef2 });
if (!node) {
return "";
}
const accessibleName = getAccessibleName(node);
const accessibleValue = getAccessibleValue(node);
const itemText = getItemText({ accessibleName, accessibleValue });
return concat(propertyName)({ attributeValue: itemText, container });
};
}
function tristate(stateValue, mixedValue) {
return function stateMapper({ attributeValue }) {
if (attributeValue === "mixed") {
return mixedValue;
}
return attributeValue !== "false" ? stateValue : `not ${stateValue}`;
};
}
function token(tokenMap) {
return function tokenMapper({ attributeValue }) {
return tokenMap[attributeValue];
};
}
function concat(propertyName) {
return function mapper({ attributeValue }) {
return attributeValue ? `${propertyName} ${attributeValue}` : "";
};
}
function integer(propertyName) {
return concat(propertyName);
}
function number(propertyName) {
return concat(propertyName);
}
function string(propertyName) {
return concat(propertyName);
}
var mapAttributeNameAndValueToLabel = ({
attributeName,
attributeValue,
container,
negative = false,
node
}) => {
if (typeof attributeValue !== "string") {
return null;
}
const mapper = ariaPropertyToVirtualLabelMap[attributeName];
return mapper?.({ attributeValue, container, negative, node }) ?? null;
};
// src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromAriaAttribute.ts
var getLabelFromAriaAttribute = ({
attributeName,
container,
node
}) => {
const attributeValue = node.getAttribute(attributeName);
return {
label: mapAttributeNameAndValueToLabel({
attributeName,
attributeValue,
container,
node
}) ?? "",
value: attributeValue ?? ""
};
};
// src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromHtmlEquivalentAttribute.ts
var isNotMatchingElement = ({
elements,
node
}) => elements.length && !elements.includes(getLocalName(node));
var isNotMatchingProperties = ({
node,
properties
}) => properties.length && !properties.some(({ key, value }) => node.getAttribute(key) === value);
var ariaToHTMLAttributeMapping = {
"aria-autocomplete": [
{ elements: ["form"], name: "autocomplete" },
{ elements: ["input", "select", "textarea"], name: "autocomplete" }
],
"aria-checked": [
{
elements: ["input"],
implicitMissingValue: "false",
name: "checked",
properties: [
{ key: "type", value: "checkbox" },
{ key: "type", value: "radio" }
]
},
{
value: "mixed",
name: "indeterminate"
}
],
"aria-colspan": [{ elements: ["td", "th"], name: "colspan" }],
"aria-controls": [
{
elements: ["input"],
name: "list"
}
],
"aria-disabled": [
{
elements: ["button", "input", "optgroup", "option", "select", "textarea"],
name: "disabled"
},
{
// TODO: Form controls within a valid legend child element of a fieldset
// with a disabled attribute do not become disabled.
elements: ["fieldset"],
name: "disabled"
}
],
// TODO: Set properties on the summary element.
// REF: https://www.w3.org/TR/html-aam-1.0/#att-open-details
// "aria-expanded": [{ elements: ["details"], name: "open" }],
// TODO: Set properties on the dialog element.
// REF: https://www.w3.org/TR/html-aam-1.0/#att-open-dialog
// Not announced, indeed it will be hidden from the accessibility tree.
// "aria-hidden": [{ name: "hidden" }],
"aria-invalid": [
// TODO: If the value doesn't match the pattern: aria-invalid="true";
// Otherwise, aria-invalid="false"
// REF: https://www.w3.org/TR/html-aam-1.0/#att-pattern
// { elements: ["input"], name: "pattern" },
// TODO: aria-invalid="spelling" or grammar
// REF: https://www.w3.org/TR/html-aam-1.0/#att-spellcheck
// { elements: ["input"], name: "spellcheck" },
],
"aria-multiselectable": [{ elements: ["select"], name: "multiple" }],
"aria-placeholder": [
{ elements: ["input", "textarea"], name: "placeholder" }
],
"aria-valuemax": [
{ elements: ["input"], name: "max" },
{ elements: ["meter", "progress"], name: "max" }
],
"aria-valuemin": [
{ elements: ["input"], name: "min" },
{ elements: ["meter", "progress"], name: "min" }
],
"aria-valuenow": [{ elements: ["meter", "progress"], name: "value" }],
"aria-readonly": [
{ elements: ["input", "textarea"], name: "readonly" },
{ name: "contenteditable", negative: true }
],
"aria-required": [
{ elements: ["input", "select", "textarea"], name: "required" }
],
"aria-rowspan": [{ elements: ["td", "th"], name: "rowspan" }],
"aria-selected": [{ elements: ["option"], name: "selected" }]
};
var getLabelFromHtmlEquivalentAttribute = ({
attributeName,
container,
node
}) => {
const htmlAttribute = ariaToHTMLAttributeMapping[attributeName];
if (!htmlAttribute?.length) {
return { label: "", value: "" };
}
for (const {
elements = [],
implicitMissingValue,
name,
negative = false,
properties = [],
value
} of htmlAttribute) {
if (isNotMatchingElement({ elements, node })) {
continue;
}
if (isNotMatchingProperties({ node, properties })) {
continue;
}
const attributeValue = node.hasAttribute(name) ? value ?? node.getAttribute(name) : node.hasAttribute(attributeName) ? null : implicitMissingValue ?? null;
const label = mapAttributeNameAndValueToLabel({
attributeName,
attributeValue,
container,
negative,
node
});
if (label) {
return { label, value: attributeValue ?? "" };
}
}
return { label: "", value: "" };
};
// src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromImplicitHtmlElementValue/getLevelFromDocumentStructure.ts
var getLevelFromDocumentStructure = ({
level = 1,
role,
tree
}) => {
if (!tree) {
return `${level}`;
}
if (tree.role === role) {
level++;
}
const parentTree = tree.parentAccessibilityNodeTree;
if (!parentTree) {
return `${level}`;
}
return getLevelFromDocumentStructure({
role,
tree: parentTree,
level
});
};
// src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromImplicitHtmlElementValue/getSet.ts
var getFirstNestedChildrenByRole = ({
role,
tree
}) => tree.children.flatMap((child) => {
if (child.role === role) {
return child;
}
return getFirstNestedChildrenByRole({ role, tree: child });
});
var getParentByRole = ({
role,
tree
}) => {
let parentTree = tree;
while (parentTree.role !== role && parentTree.parentAccessibilityNodeTree) {
parentTree = parentTree.parentAccessibilityNodeTree;
}
return parentTree;
};
var getSiblingsByRoleAndLevel = ({
role,
parentRole = role,
tree
}) => {
const parentTree = getParentByRole({ role: parentRole, tree });
return getFirstNestedChildrenByRole({ role, tree: parentTree });
};
var getFormOwnerTree = ({ tree }) => getParentByRole({ role: "form", tree });
var getRadioInputsByName = ({
name,
tree
}) => tree.children.flatMap((child) => {
if (isElement(child.node) && child.node.getAttribute("name") === name) {
return child;
}
return getRadioInputsByName({ name, tree: child });
});
var getRadioGroup = ({
node,
tree
}) => {
if (node.localName !== "input") {
return getSiblingsByRoleAndLevel({
role: "radio",
parentRole: "radiogroup",
tree
});
}
if (!node.hasAttribute("name")) {
return [];
}
const name = node.getAttribute("name");
if (!name) {
return [];
}
const formOwnerTree = getFormOwnerTree({ tree });
return getRadioInputsByName({ name, tree: formOwnerTree });
};
var getChildrenByRole = ({
role,
tree
}) => tree.children.filter((child) => child.role === role);
var getSet = ({
node,
role,
tree
}) => {
if (role === "treeitem") {
return getSiblingsByRoleAndLevel({ role, tree });
}
if (role === "radio") {
return getRadioGroup({ node, tree });
}
return getChildrenByRole({
role,
tree
});
};
// src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromImplicitHtmlElementValue/hasTreegridAncestor.ts
var hasTreegridAncestor = (tree) => {
if (!tree) {
return false;
}
if (tree.role === "treegrid") {
return true;
}
if (!tree.parentAccessibilityNodeTree) {
return false;
}
return hasTreegridAncestor(tree.parentAccessibilityNodeTree);
};
// src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromImplicitHtmlElementValue/index.ts
var headingLocalNameToLevelMap = {
h1: "1",
h2: "2",
h3: "3",
h4: "4",
h5: "5",
h6: "6"
};
var getNodeSet = ({
node,
role,
tree
}) => {
if (!tree) {
return null;
}
if (role === "article") {
return null;
}
if (role === "row" && !hasTreegridAncestor(tree)) {
return null;
}
return getSet({
node,
role,
tree
});
};
var levelItemRoles = /* @__PURE__ */ new Set(["listitem", "treeitem"]);
var mapHtmlElementAriaToImplicitValue = {
/**
* Used in Roles:
*
* - heading
* - listitem
* - row
*
* Inherits into Roles:
*
* - treeitem
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#aria-level
*/
"aria-level": ({ role, tree, node }) => {
if (role === "heading") {
const localName = getLocalName(node);
return headingLocalNameToLevelMap[localName];
}
if (role === "row" && hasTreegridAncestor(tree)) {
return getLevelFromDocumentStructure({
role,
tree
});
}
if (levelItemRoles.has(role)) {
return getLevelFromDocumentStructure({
role,
tree
});
}
return "";
},
/**
* Used in Roles:
*
* - article
* - listitem
* - menuitem
* - option
* - radio
* - row
* - tab
*
* Inherits into Roles:
*
* - menuitemcheckbox
* - menuitemradio
* - treeitem
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#aria-posinset
*/
"aria-posinset": ({ node, tree, role }) => {
const nodeSet = getNodeSet({ node, role, tree });
if (!nodeSet?.length) {
return "";
}
const index = nodeSet.findIndex((child) => child.node === node);
return `${index + 1}`;
},
/**
* Used in Roles:
*
* - article
* - listitem
* - menuitem
* - option
* - radio
* - row
* - tab
*
* Inherits into Roles:
*
* - menuitemcheckbox
* - menuitemradio
* - treeitem
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#aria-setsize
*/
"aria-setsize": ({ node, tree, role }) => {
const nodeSet = getNodeSet({ node, role, tree });
if (!nodeSet?.length) {
return "";
}
return `${nodeSet.length}`;
}
};
var getLabelFromImplicitHtmlElementValue = ({
attributeName,
container,
node,
parentAccessibilityNodeTree,
role
}) => {
const implicitValue = mapHtmlElementAriaToImplicitValue[attributeName]?.({
node,
tree: parentAccessibilityNodeTree,
role
});
return {
label: mapAttributeNameAndValueToLabel({
attributeName,
attributeValue: implicitValue,
container,
node
}) ?? "",
value: implicitValue ?? ""
};
};
// src/getNodeAccessibilityData/getAccessibleAttributeLabels/postProcessAriaValueNow.ts
var percentageBasedValueRoles = /* @__PURE__ */ new Set(["progressbar", "scrollbar"]);
var isNumberLike = (value) => {
return !isNaN(parseFloat(value));
};
var toNumber = (value) => parseFloat(value);
var toPercentageLabel = (value) => `current value ${value}%`;
var postProcessAriaValueNow = ({
max,
min,
role,
value
}) => {
if (!percentageBasedValueRoles.has(role)) {
return value;
}
if (!isNumberLike(value)) {
return value;
}
if (isNumberLike(max) && isNumberLike(min)) {
const percentage = +((toNumber(value) - toNumber(min)) / (toNumber(max) - toNumber(min)) * 100).toFixed(2);
return toPercentageLabel(percentage);
}
return toPercentageLabel(value);
};
// src/getNodeAccessibilityData/getAccessibleAttributeLabels/postProcessLabels.ts
var priorityReplacementMap = [
["aria-colindextext", "aria-colindex"],
["aria-rowindextext", "aria-rowindex"],
/**
* If aria-valuetext is specified, assistive technologies SHOULD render that
* value instead of the value of aria-valuenow.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#aria-valuetext
*/
["aria-valuetext", "aria-valuenow"]
];
var postProcessLabels = ({
labels,
role
}) => {
for (const [preferred, dropped] of priorityReplacementMap) {
if (labels[preferred] && labels[dropped]) {
labels[dropped].value = "";
}
}
if (labels["aria-valuenow"]) {
labels["aria-valuenow"].label = postProcessAriaValueNow({
value: labels["aria-valuenow"].value,
min: labels["aria-valuemin"]?.value,
max: labels["aria-valuemax"]?.value,
role
});
}
if (labels["aria-setsize"]?.value === "-1") {
labels["aria-setsize"].label = "set size unknown";
}
return labels;
};
// src/getNodeAccessibilityData/getAccessibleAttributeLabels/index.ts
var getAccessibleAttributeLabels = ({
accessibleValue,
alternateReadingOrderParents,
container,
node,
parentAccessibilityNodeTree,
role
}) => {
if (!isElement(node)) {
return {
accessibleAttributeLabels: [],
accessibleAttributeToLabelMap: {}
};
}
const labels = {};
const attributes = getAttributesByRole({ accessibleValue, role });
attributes.forEach(([attributeName, implicitAttributeValue]) => {
const {
label: labelFromHtmlEquivalentAttribute,
value: valueFromHtmlEquivalentAttribute
} = getLabelFromHtmlEquivalentAttribute({
attributeName,
container,
node
});
if (labelFromHtmlEquivalentAttribute) {
labels[attributeName] = {
label: labelFromHtmlEquivalentAttribute,
value: valueFromHtmlEquivalentAttribute
};
return;
}
const { label: labelFromAriaAttribute, value: valueFromAriaAttribute } = getLabelFromAriaAttribute({
attributeName,
container,
node
});
if (labelFromAriaAttribute) {
labels[attributeName] = {
label: labelFromAriaAttribute,
value: valueFromAriaAttribute
};
return;
}
const {
label: labelFromImplicitHtmlElementValue,
value: valueFromImplicitHtmlElementValue
} = getLabelFromImplicitHtmlElementValue({
attributeName,
container,
node,
parentAccessibilityNodeTree,
role
});
if (labelFromImplicitHtmlElementValue) {
labels[attributeName] = {
label: labelFromImplicitHtmlElementValue,
value: valueFromImplicitHtmlElementValue
};
return;
}
const labelFromImplicitAriaAttributeValue = mapAttributeNameAndValueToLabel(
{
attributeName,
attributeValue: implicitAttributeValue,
container,
node
}
);
if (labelFromImplicitAriaAttributeValue) {
labels[attributeName] = {
label: labelFromImplicitAriaAttributeValue,
value: implicitAttributeValue ?? ""
};
return;
}
});
const accessibleAttributeToLabelMap = postProcessLabels({ labels, role });
const accessibleAttributeLabels = Object.values(accessibleAttributeToLabelMap).map(({ label }) => label).filter(Boolean);
if (alternateReadingOrderParents.length > 0) {
accessibleAttributeLabels.push(
`${alternateReadingOrderParents.length} previous alternate reading ${alternateReadingOrderParents.length === 1 ? "order" : "orders"}`
);
}
return { accessibleAttributeLabels, accessibleAttributeToLabelMap };
};
// src/flattenTree.ts
var END_OF_ROLE_PREFIX = "end of";
var END_OF_NO_ROLE_PREFIX = "end";
var TEXT_NODE2 = 3;
function shouldIgnoreChildren(tree) {
const { accessibleName, children, node } = tree;
if (!accessibleName) {
return false;
}
if (children.length > 1) {
return false;
}
if (children.every((child) => child.node.nodeType !== TEXT_NODE2)) {
return false;
}
return accessibleName === (node.textContent || `${node.value}` || "")?.trim();
}
function flattenTree(container, tree, parentAccessibilityNodeTree) {
const { children, ...treeNode } = tree;
treeNode.parentAccessibilityNodeTree = parentAccessibilityNodeTree;
const { accessibleAttributeLabels, accessibleAttributeToLabelMap } = getAccessibleAttributeLabels({
...treeNode,
container
});
const treeNodeWithAttributeLabels = {
...treeNode,
accessibleAttributeLabels,
accessibleAttributeToLabelMap
};
const isAnnounced = !treeNodeWithAttributeLabels.isInert && (!!treeNodeWithAttributeLabels.accessibleName || !!treeNodeWithAttributeLabels.accessibleDescription || treeNodeWithAttributeLabels.accessibleAttributeLabels.length > 0 || !!treeNodeWithAttributeLabels.spokenRole);
const ignoreChildren = shouldIgnoreChildren(tree);
const flattenedTree = ignoreChildren ? [] : children.flatMap(
(child) => flattenTree(container, child, {
...treeNodeWithAttributeLabels,
children
})
);
const isRoleContainer = !!flattenedTree.length && !ignoreChildren && isAnnounced;
if (isAnnounced) {
flattenedTree.unshift(treeNodeWithAttributeLabels);
}
if (isRoleContainer) {
flattenedTree.push({
...treeNodeWithAttributeLabels,
spokenRole: treeNodeWithAttributeLabels.spokenRole ? `${END_OF_ROLE_PREFIX} ${treeNodeWithAttributeLabels.spokenRole}` : END_OF_NO_ROLE_PREFIX
});
}
return flattenedTree;
}
// src/commands/getIndexByRoleAndAttributes.ts
function getIndexByRoleAndAttributes({
filters,
reorderedTree,
tree
}) {
const accessibilityNode = reorderedTree.find(
(node) => !node.spokenRole.startsWith(END_OF_ROLE_PREFIX) && matchesRoles(node, filters.roles) && matchesAccessibleAttributes(node, filters.ariaAttributes)
);
if (!accessibilityNode) {
return null;
}
return tree.findIndex((node) => node === accessibilityNode);
}
// src/commands/getNextIndexByRoleAndAttributes.ts
function getNextIndexByRoleAndAttributes(filters) {
return function getNextIndexByRoleAndAttributesInner({
currentIndex,
tree
}) {
const reorderedTree = tree.slice(currentIndex + 1).concat(tree.slice(0, currentIndex + 1));
return getIndexByRoleAndAttributes({ filters, reorderedTree, tree });
};
}
// src/commands/getPreviousIndexByRoleAndAttributes.ts
function getPreviousIndexByRoleAndAttributes(filters) {
return function getPreviousIndexInner({
currentIndex,
tree
}) {
const reorderedTree = tree.slice(0, currentIndex).reverse().concat(tree.slice(currentIndex).reverse());
return getIndexByRoleAndAttributes({ filters, reorderedTree, tree });
};
}
// src/getElementFromNode.ts
var getElementFromNode = (node) => {
return isElement(node) ? node : node.parentElement;
};
// src/commands/getElementNode.ts
function getElementNode(accessibilityNode) {
const { node } = accessibilityNode;
return getElementFromNode(node);
}
// src/commands/getNextIndexByIdRefsAttribute.ts
function getNextIndexByIdRefsAttribute({
attributeName,
index = 0,
container,
currentIndex,
tree
}) {
if (!isElement(container)) {
return;
}
const currentAccessibilityNode = tree.at(currentIndex);
const currentNode = getElementNode(currentAccessibilityNode);
const idRefs2 = getIdRefsByAttribute({
attributeName,
node: currentNode
});
const idRef2 = idRefs2[index];
const targetNode = getNodeByIdRef({ container, idRef: idRef2 });
if (!targetNode) {
return;
}
const nodeIndex = tree.findIndex(({ node }) => node === targetNode);
if (nodeIndex !== -1) {
return nodeIndex;
}
const nodeIndexByParent = tree.findIndex(
({ parent }) => parent === targetNode
);
if (nodeIndexByParent !== -1) {
return nodeIndexByParent;
}
return;
}
// src/commands/jumpToControlledElement.ts
function jumpToControlledElement({
index = 0,
container,
currentIndex,
tree
}) {
return getNextIndexByIdRefsAttribute({
attributeName: "aria-controls",
index,
container,
currentIndex,
tree
});
}
// src/commands/jumpToDetailsElement.ts
function jumpToDetailsElement({
container,
currentIndex,
tree
}) {
return getNextIndexByIdRefsAttribute({
attributeName: "aria-details",
index: 0,
container,
currentIndex,
tree
});
}
// src/commands/jumpToErrorMessageElement.ts
function jumpToErrorMessageElement({
index = 0,
container,
currentIndex,
tree
}) {
return getNextIndexByIdRefsAttribute({
attributeName: "aria-errormessage",
index,
container,
currentIndex,
tree
});
}
// src/commands/moveToNextAlternateReadingOrderElement.ts
function moveToNextAlternateReadingOrderElement({
index,
container,
currentIndex,
tree
}) {
return getNextIndexByIdRefsAttribute({
attributeName: "aria-flowto",
index,
container,
currentIndex,
tree
});
}
// src/commands/moveToPreviousAlternateReadingOrderElement.ts
function moveToPreviousAlternateReadingOrderElement({
index = 0,
container,
currentIndex,
tree
}) {
if (!isElement(container)) {
return;
}
const { alternateReadingOrderParents } = tree.at(currentIndex);
const targetNode = alternateReadingOrderParents[index];
if (!targetNode) {
return;
}
return tree.findIndex(({ node }) => node === targetNode);
}
// src/commands/index.ts
var quickLandmarkNavigationRoles = [
/**
* Assistive technologies SHOULD enable users to quickly navigate to
* elements with role banner.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#banner
*/
"banner",
/**
* Assistive technologies SHOULD enable users to quickly navigate to
* elements with role complementary.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#complementary
*/
"complementary",
/**
* Assistive technologies SHOULD enable users to quickly navigate to
* elements with role contentinfo.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#contentinfo
*/
"contentinfo",
/**
* Assistive technologies SHOULD enable users to quickly navigate to
* figures.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#figure
*/
"figure",
/**
* Assistive technologies SHOULD enable users to quickly navigate to
* elements with role form.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#form
*/
"form",
/**
* Assistive technologies SHOULD enable users to quickly navigate to
* elements with role main.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#main
*/
"main",
/**
* Assistive technologies SHOULD enable users to quickly navigate to
* elements with role navigation.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#navigation
*/
"navigation",
/**
* Assistive technologies SHOULD enable users to quickly navigate to
* elements with role region.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#region
*/
"region",
/**
* Assistive technologies SHOULD enable users to quickly navigate to
* elements with role search.
*
* REF: https://www.w3.org/TR/wai-aria-1.2/#search
*/
"search"
];
var quickAriaRoleNavigationRoles = [
...quickLandmarkNavigationRoles,
/**
* WAI-ARIA doesn't specify that assistive technologies should enable users
* to quickly navigate to elements with role heading. However, it is very
* common for assistive technology users to navigate between headings.
*
* REF:
* - https://www.w3.org/TR/wai-aria-1.2/#heading
* - https://webaim.org/projects/screenreadersurvey10/#heading
* - https://webaim.org/projects/screenreadersurvey10/#finding
*
* MUST requirements:
*
* Headings provide an outline of the page and users need to be able to
* quickly navigate to different sections of the page.
*
* REF: https://a11ysupport.io/tech/aria/heading_role
*/
"heading",
/**
* WAI-ARIA doesn't specify that assistive technologies should enable users
* to quickly navigate to elements with role link. However, it is very
* common for assistive technology users to navigate between links.
*
* REF:
* - https://www.w3.org/TR/wai-aria-1.2/#link
* - https://webaim.org/projects/screenreadersurvey10/#finding
*/
"link"
];
var quickAriaRoleNavigationCommands = quickAriaRoleNavigationRoles.reduce(
(accumulatedCommands, role) => {
const moveToNextCommand = `moveToNext${role.at(0).toUpperCase()}${role.slice(1)}`;
const moveToPreviousCommand = `moveToPrevious${role.at(0).toUpperCase()}${role.slice(1)}`;
return {
...accumulatedCommands,
[moveToNextCommand]: getNextIndexByRoleAndAttributes({ roles: [role] }),
[moveToPreviousCommand]: getPreviousIndexByRoleAndAttributes({
roles: [role]
})
};
},
{}
);
var headingLevels = ["1", "2", "3", "4", "5", "6"];
var headingLevelNavigationCommands = headingLevels.reduce((accumulatedCommands, headingLevel) => {
const moveToNextCommand = `moveToNextHeadingLevel${headingLevel}`;
const moveToPreviousCommand = `moveToPreviousHeadingLevel${headingLevel}`;
return {
...accumulatedCommands,
[moveToNextCommand]: getNextIndexByRoleAndAttributes({
ariaAttributes: { "aria-level": headingLevel }
}),
[moveToPreviousCommand]: getPreviousIndexByRoleAndAttributes({
ariaAttributes: { "aria-level": headingLevel }
})
};
}, {});
var commands = {
/**
* Jump to an element controlled by the current element in the Virtual Screen
* Reader focus. See [aria-controls](https://www.w3.org/TR/wai-aria-1.2/#aria-controls).
*
* When using with `virtual.perform()`, pass an index option to select which
* controlled element is jumped to when there are more than one:
*
* ```ts
* import { virtual } from "@guidepup/virtual-scre