UNPKG

@guidepup/virtual-screen-reader

Version:

Virtual Screen Reader driver for unit test automation.

1,746 lines (1,705 loc) 89.4 kB
// 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