UNPKG

@guidepup/virtual-screen-reader

Version:

Virtual Screen Reader driver for unit test automation.

1 lines 210 kB
{"version":3,"sources":["../../src/index.ts","../../src/getIdRefsByAttribute.ts","../../src/getNodeAccessibilityData/index.ts","../../src/getNodeAccessibilityData/getRole.ts","../../src/getLocalName.ts","../../src/isElement.ts","../../src/getNodeAccessibilityData/getAccessibleDescription.ts","../../src/getNodeAccessibilityData/getAccessibleName.ts","../../src/sanitizeString.ts","../../src/getNodeAccessibilityData/getAccessibleValue.ts","../../src/isDialogRole.ts","../../src/getNodeByIdRef.ts","../../src/isHiddenFromAccessibilityTree.ts","../../src/createAccessibilityTree.ts","../../src/commands/nodeMatchers.ts","../../src/getNodeAccessibilityData/getAccessibleAttributeLabels/getAttributesByRole.ts","../../src/getItemText.ts","../../src/getNodeAccessibilityData/getAccessibleAttributeLabels/mapAttributeNameAndValueToLabel.ts","../../src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromAriaAttribute.ts","../../src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromHtmlEquivalentAttribute.ts","../../src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromImplicitHtmlElementValue/getLevelFromDocumentStructure.ts","../../src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromImplicitHtmlElementValue/getSet.ts","../../src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromImplicitHtmlElementValue/hasTreegridAncestor.ts","../../src/getNodeAccessibilityData/getAccessibleAttributeLabels/getLabelFromImplicitHtmlElementValue/index.ts","../../src/getNodeAccessibilityData/getAccessibleAttributeLabels/postProcessAriaValueNow.ts","../../src/getNodeAccessibilityData/getAccessibleAttributeLabels/postProcessLabels.ts","../../src/getNodeAccessibilityData/getAccessibleAttributeLabels/index.ts","../../src/flattenTree.ts","../../src/commands/getIndexByRoleAndAttributes.ts","../../src/commands/getNextIndexByRoleAndAttributes.ts","../../src/commands/getPreviousIndexByRoleAndAttributes.ts","../../src/getElementFromNode.ts","../../src/commands/getElementNode.ts","../../src/commands/getNextIndexByIdRefsAttribute.ts","../../src/commands/jumpToControlledElement.ts","../../src/commands/jumpToDetailsElement.ts","../../src/commands/jumpToErrorMessageElement.ts","../../src/commands/moveToNextAlternateReadingOrderElement.ts","../../src/commands/moveToPreviousAlternateReadingOrderElement.ts","../../src/commands/index.ts","../../src/errors.ts","../../src/getLiveSpokenPhrase.ts","../../src/getSpokenPhrase.ts","../../src/observeDOM.ts","../../src/tick.ts","../../src/Virtual.ts"],"sourcesContent":["import { type StartOptions, Virtual } from \"./Virtual\";\n\n/**\n * [API Reference](https://www.guidepup.dev/docs/api/class-virtual)\n *\n * A Virtual Screen Reader instance that can be used to launch and control a\n * headless JavaScript screen reader which is compatible with any specification\n * compliant DOM implementation, e.g. jsdom, Jest, or any modern browser.\n *\n * Here's a typical example:\n *\n * ```ts\n * import { virtual } from \"@guidepup/virtual-screen-reader\";\n *\n * function setupBasicPage() {\n * document.body.innerHTML = `\n * <nav>Nav Text</nav>\n * <section>\n * <h1>Section Heading</h1>\n * <p>Section Text</p>\n * <article>\n * <header>\n * <h1>Article Header Heading</h1>\n * <p>Article Header Text</p>\n * </header>\n * <p>Article Text</p>\n * </article>\n * </section>\n * <footer>Footer</footer>\n * `;\n * }\n *\n * describe(\"Screen Reader Tests\", () => {\n * test(\"should traverse the page announcing the expected roles and content\", async () => {\n * // Setup a page using a framework and testing library of your choice\n * setupBasicPage();\n *\n * // Start your Virtual Screen Reader instance\n * await virtual.start({ container: document.body });\n *\n * // Navigate your environment with the Virtual Screen Reader just as your users would\n * while ((await virtual.lastSpokenPhrase()) !== \"end of document\") {\n * await virtual.next();\n * }\n *\n * // Assert on what your users would really see and hear when using screen readers\n * expect(await virtual.spokenPhraseLog()).toEqual([\n * \"document\",\n * \"navigation\",\n * \"Nav Text\",\n * \"end of navigation\",\n * \"region\",\n * \"heading, Section Heading, level 1\",\n * \"Section Text\",\n * \"article\",\n * \"heading, Article Header Heading, level 1\",\n * \"Article Header Text\",\n * \"Article Text\",\n * \"end of article\",\n * \"end of region\",\n * \"contentinfo\",\n * \"Footer\",\n * \"end of contentinfo\",\n * \"end of document\",\n * ]);\n *\n * // Stop your Virtual Screen Reader instance\n * await virtual.stop();\n * });\n * });\n * ```\n */\nexport const virtual = new Virtual();\n\nexport { type StartOptions, Virtual };\n","export function getIdRefsByAttribute({\n attributeName,\n node,\n}: {\n attributeName: string;\n node: Element;\n}) {\n return (node.getAttribute(attributeName) ?? \"\")\n .trim()\n .split(\" \")\n .filter(Boolean);\n}\n","import { ARIARole, roles } from \"html-aria\";\nimport { getRole, presentationRoles } from \"./getRole\";\nimport { getAccessibleDescription } from \"./getAccessibleDescription\";\nimport { getAccessibleName } from \"./getAccessibleName\";\nimport { getAccessibleValue } from \"./getAccessibleValue\";\nimport { getLocalName } from \"../getLocalName\";\nimport { isDialogRole } from \"../isDialogRole\";\nimport { isElement } from \"../isElement\";\n\nconst childrenPresentationalRoles = new Set(\n Object.entries(roles)\n .filter(([, { childrenPresentational }]) => childrenPresentational)\n .map(([key]) => key) as string[]\n);\n\nconst getSpokenRole = ({\n isGeneric,\n isPresentational,\n node,\n role,\n}: {\n isGeneric: boolean;\n isPresentational: boolean;\n node: Node;\n role: string;\n}) => {\n if (isPresentational || isGeneric) {\n return \"\";\n }\n\n if (isElement(node)) {\n /**\n * Assistive technologies SHOULD use the value of aria-roledescription when\n * presenting the role of an element, but SHOULD NOT change other\n * functionality based on the role of an element that has a value for\n * aria-roledescription. For example, an assistive technology that provides\n * functions for navigating to the next region or button SHOULD allow those\n * functions to navigate to regions and buttons that have an\n * aria-roledescription.\n *\n * REF: https://www.w3.org/TR/wai-aria-1.2/#aria-roledescription\n */\n const roledescription = node.getAttribute(\"aria-roledescription\");\n\n if (roledescription) {\n return roledescription;\n }\n }\n\n return role;\n};\n\n/**\n * Nodes that are [inert](https://html.spec.whatwg.org/multipage/interaction.html#inert)\n * are not exposed to an accessibility API.\n *\n * Note: an inert node can have descendants that are not inert. For example,\n * a [modal dialog](https://html.spec.whatwg.org/multipage/interaction.html#modal-dialogs-and-inert-subtrees)\n * can escape an inert subtree.\n *\n * REF: https://www.w3.org/TR/html-aam-1.0/#att-inert\n */\nconst getIsInert = ({\n inheritedImplicitInert,\n node,\n role,\n}: {\n inheritedImplicitInert: boolean;\n node: Node;\n role: string;\n}) => {\n if (!isElement(node)) {\n return inheritedImplicitInert;\n }\n\n // TODO: this doesn't cater to `<dialog>` elements which are model if opened\n // by `show()` vs `showModal()`.\n // REF: https://html.spec.whatwg.org/multipage/interaction.html#modal-dialogs-and-inert-subtrees\n const isNativeModalDialog =\n getLocalName(node) === \"dialog\" && node.hasAttribute(\"open\");\n\n const isNonNativeModalDialog =\n isDialogRole(role) && node.hasAttribute(\"aria-modal\");\n\n const isModalDialog = isNonNativeModalDialog || isNativeModalDialog;\n const isExplicitInert = node.hasAttribute(\"inert\");\n\n return isExplicitInert || (inheritedImplicitInert && !isModalDialog);\n};\n\nexport function getNodeAccessibilityData({\n allowedAccessibilityRoles,\n inheritedImplicitInert,\n inheritedImplicitPresentational,\n node,\n}: {\n allowedAccessibilityRoles: string[];\n inheritedImplicitInert: boolean;\n inheritedImplicitPresentational: boolean;\n node: Node;\n}) {\n const accessibleDescription = getAccessibleDescription(node);\n const accessibleName = getAccessibleName(node);\n const accessibleValue = getAccessibleValue(node);\n\n const { explicitRole, implicitRole, role } = getRole({\n accessibleName,\n allowedAccessibilityRoles,\n inheritedImplicitPresentational,\n node,\n });\n\n const amendedAccessibleDescription =\n accessibleDescription === accessibleName ? \"\" : accessibleDescription;\n\n const isExplicitPresentational = presentationRoles.has(explicitRole);\n const isPresentational = presentationRoles.has(role);\n const isGeneric = role === \"generic\";\n\n const spokenRole = getSpokenRole({\n isGeneric,\n isPresentational,\n node,\n role,\n });\n\n const { allowedChildRoles: allowedAccessibilityChildRoles } = roles[\n role as ARIARole\n ] ?? {\n allowedChildRoles: [],\n };\n\n const { allowedChildRoles: implicitAllowedAccessibilityChildRoles } = roles[\n implicitRole as ARIARole\n ] ?? {\n allowedChildRoles: [],\n };\n\n /**\n * Any descendants of elements that have the characteristic \"Children\n * Presentational: True\" unless the descendant is not allowed to be\n * presentational because it meets one of the conditions for exception\n * described in Presentational Roles Conflict Resolution. However, the text\n * content of any excluded descendants is included.\n *\n * REF: https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion\n */\n const isChildrenPresentationalRole = childrenPresentationalRoles.has(role);\n\n /**\n * When an explicit or inherited role of presentation is applied to an\n * element with the implicit semantic of a WAI-ARIA role that has Allowed\n * Accessibility Child Roles, in addition to the element with the explicit\n * role of presentation, the user agent MUST apply an inherited role of\n * presentation to any owned elements that do not have an explicit role\n * defined. Also, when an explicit or inherited role of presentation is\n * applied to a host language element which has specifically allowed children\n * as defined by the host language specification, in addition to the element\n * with the explicit role of presentation, the user agent MUST apply an\n * inherited role of presentation to any specifically allowed children that\n * do not have an explicit role defined.\n *\n * REF: https://www.w3.org/TR/wai-aria-1.2/#presentational-role-inheritance\n */\n const isExplicitOrInheritedPresentation =\n isExplicitPresentational || inheritedImplicitPresentational;\n const isElementWithImplicitAllowedAccessibilityChildRoles =\n !!implicitAllowedAccessibilityChildRoles.length;\n const childrenInheritPresentationExceptAllowedRoles =\n isExplicitOrInheritedPresentation &&\n isElementWithImplicitAllowedAccessibilityChildRoles;\n\n const childrenPresentational =\n isChildrenPresentationalRole ||\n childrenInheritPresentationExceptAllowedRoles;\n\n const isInert = getIsInert({\n inheritedImplicitInert,\n node,\n role,\n });\n\n return {\n accessibleDescription: amendedAccessibleDescription,\n accessibleName,\n accessibleValue,\n allowedAccessibilityChildRoles,\n childrenPresentational,\n isExplicitPresentational,\n isInert,\n role,\n spokenRole,\n };\n}\n","import { ALL_ROLES, ARIARole, getRole as getHtmlAriaRole } from \"html-aria\";\nimport { getLocalName } from \"../getLocalName\";\nimport { isElement } from \"../isElement\";\n\nexport const presentationRoles = new Set([\"presentation\", \"none\"]);\n\nexport const synonymRolesMap: Record<string, string> = {\n img: \"image\",\n presentation: \"none\",\n directory: \"list\",\n};\n\nconst allowedNonAbstractRoles = new Set(ALL_ROLES);\n\nconst rolesRequiringName = new Set([\"form\", \"region\"]);\n\nexport const globalStatesAndProperties = [\n \"aria-atomic\",\n \"aria-braillelabel\",\n \"aria-brailleroledescription\",\n \"aria-busy\",\n \"aria-controls\",\n \"aria-describedby\",\n \"aria-description\",\n \"aria-details\",\n \"aria-dropeffect\",\n \"aria-flowto\",\n \"aria-grabbed\",\n \"aria-hidden\",\n \"aria-keyshortcuts\",\n \"aria-label\",\n \"aria-labelledby\",\n \"aria-live\",\n \"aria-owns\",\n \"aria-relevant\",\n \"aria-roledescription\",\n];\n\nconst FOCUSABLE_SELECTOR = [\n \"input:not([type=hidden]):not([disabled])\",\n \"button:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n '[contenteditable=\"\"]',\n '[contenteditable=\"true\"]',\n \"a[href]\",\n \"[tabindex]:not([disabled])\",\n].join(\", \");\n\nfunction isFocusable(node: HTMLElement) {\n return node.matches(FOCUSABLE_SELECTOR);\n}\n\nfunction hasGlobalStateOrProperty(node: HTMLElement) {\n return globalStatesAndProperties.some((global) => node.hasAttribute(global));\n}\n\nfunction mapAliasedRoles(role: string) {\n const canonical = synonymRolesMap[role];\n\n return canonical ?? role;\n}\n\nfunction getExplicitRole({\n accessibleName,\n allowedAccessibilityRoles,\n inheritedImplicitPresentational,\n node,\n}: {\n accessibleName: string;\n allowedAccessibilityRoles: string[];\n inheritedImplicitPresentational: boolean;\n node: HTMLElement;\n}) {\n const rawRoles = node.getAttribute(\"role\")?.trim().split(\" \") ?? [];\n\n const authorErrorFilteredRoles = rawRoles\n /**\n * As stated in the Definition of Roles section, it is considered an\n * authoring error to use abstract roles in content.\n * User agents MUST NOT map abstract roles via the standard role mechanism\n * of the accessibility API.\n *\n * REF: https://www.w3.org/TR/wai-aria-1.2/#document-handling_author-errors_roles\n */\n .filter((role) => allowedNonAbstractRoles.has(role as ARIARole))\n /**\n * Certain landmark roles require names from authors. In situations where\n * an author has not specified names for these landmarks, it is\n * considered an authoring error. The user agent MUST treat such elements\n * as if no role had been provided. If a valid fallback role had been\n * specified, or if the element had an implicit ARIA role, then user\n * agents would continue to expose that role, instead. Instances of such\n * roles are as follows:\n *\n * - form\n * - region\n *\n * REF: https://www.w3.org/TR/wai-aria-1.2/#document-handling_author-errors_roles\n */\n .filter((role) => !!accessibleName || !rolesRequiringName.has(role));\n\n /**\n * If an allowed child element has an explicit non-presentational role, user\n * agents MUST ignore an inherited presentational role and expose the element\n * with its explicit role. If the action of exposing the explicit role causes\n * the accessibility tree to be malformed, the expected results are\n * undefined.\n *\n * See also \"Children Presentational: True\".\n *\n * REF:\n *\n * - https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none\n * - https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion\n * - https://www.w3.org/TR/wai-aria-1.2/#mustContain\n */\n\n const isExplicitAllowedChildElement = allowedAccessibilityRoles.some(\n (allowedExplicitRole) =>\n authorErrorFilteredRoles?.[0] === allowedExplicitRole\n );\n\n if (inheritedImplicitPresentational && !isExplicitAllowedChildElement) {\n authorErrorFilteredRoles.unshift(\"none\");\n }\n\n if (!authorErrorFilteredRoles?.length) {\n return \"\";\n }\n\n const filteredRoles = authorErrorFilteredRoles\n /**\n * If an element is focusable, user agents MUST ignore the\n * presentation/none role and expose the element with its implicit role, in\n * order to ensure that the element is operable.\n *\n * If an element has global WAI-ARIA states or properties, user agents MUST\n * ignore the presentation role and instead expose the element's implicit\n * role. However, if an element has only non-global, role-specific WAI-ARIA\n * states or properties, the element MUST NOT be exposed unless the\n * presentational role is inherited and an explicit non-presentational role\n * is applied.\n *\n * REF: https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none\n */\n .filter((role) => {\n if (!presentationRoles.has(role)) {\n return true;\n }\n\n if (hasGlobalStateOrProperty(node) || isFocusable(node)) {\n return false;\n }\n\n return true;\n });\n\n return filteredRoles?.[0] ?? \"\";\n}\n\nexport function getRole({\n accessibleName,\n allowedAccessibilityRoles,\n inheritedImplicitPresentational,\n node,\n}: {\n accessibleName: string;\n allowedAccessibilityRoles: string[];\n inheritedImplicitPresentational: boolean;\n node: Node;\n}): { explicitRole: string; implicitRole: string; role: string } {\n if (!isElement(node)) {\n return { explicitRole: \"\", implicitRole: \"\", role: \"\" };\n }\n\n const baseExplicitRole = getExplicitRole({\n accessibleName,\n allowedAccessibilityRoles,\n inheritedImplicitPresentational,\n node,\n });\n const explicitRole = mapAliasedRoles(baseExplicitRole);\n\n // Feature detect AOM support\n // TODO: this isn't quite right, computed role might not be the implicit\n // role, it might be the explicit one as `computedRole` does not\n // distinguish between them.\n if (\"computedRole\" in node) {\n const role = node.computedRole as string;\n\n return { explicitRole, implicitRole: role, role };\n }\n\n // Backwards compatibility\n const isBodyElement = getLocalName(node) === \"body\";\n\n const baseImplicitRole = isBodyElement\n ? \"document\"\n : getHtmlAriaRole(node, { ignoreRoleAttribute: true })?.name ?? \"\";\n\n const implicitRole = mapAliasedRoles(baseImplicitRole);\n\n if (explicitRole) {\n return { explicitRole, implicitRole, role: explicitRole };\n }\n\n return {\n explicitRole,\n implicitRole,\n role: implicitRole,\n };\n}\n","export const getLocalName = (element: Element) =>\n element.localName ?? element.tagName.toLowerCase();\n","const ELEMENT_NODE = 1;\n\nexport function isElement(node: Node): node is HTMLElement {\n return node.nodeType === ELEMENT_NODE;\n}\n","import { computeAccessibleDescription } from \"dom-accessibility-api\";\nimport { isElement } from \"../isElement\";\n\nexport function getAccessibleDescription(node: Node) {\n return isElement(node) ? computeAccessibleDescription(node).trim() : \"\";\n}\n","import { computeAccessibleName } from \"dom-accessibility-api\";\nimport { isElement } from \"../isElement\";\nimport { sanitizeString } from \"../sanitizeString\";\n\nexport function getAccessibleName(node: Node): string {\n // Feature detect AOM support\n if (\"computedName\" in node) {\n return node.computedName as string;\n }\n\n return isElement(node)\n ? computeAccessibleName(node).trim()\n : // `node.textContent` is only `null` for `document` and `doctype`.\n \n sanitizeString(node.textContent!);\n}\n","export function sanitizeString(string: string) {\n return string.trim().replace(/\\s+/g, \" \");\n}\n","import { getLocalName } from \"../getLocalName\";\nimport { isElement } from \"../isElement\";\n\nexport type HTMLElementWithValue =\n | HTMLButtonElement\n | HTMLDataElement\n | HTMLInputElement\n | HTMLLIElement\n | HTMLMeterElement\n | HTMLOptionElement\n | HTMLProgressElement\n | HTMLParamElement;\n\nconst ignoredInputTypes = new Set([\"checkbox\", \"radio\"]);\nconst allowedLocalNames = new Set([\n \"button\",\n \"data\",\n \"input\",\n // \"li\",\n \"meter\",\n \"option\",\n \"progress\",\n \"param\",\n]);\n\nfunction getSelectValue(node: HTMLSelectElement) {\n const selectedOptions = [...node.options].filter(\n (optionElement) => optionElement.selected\n );\n\n if (node.multiple) {\n return [...selectedOptions]\n .map((optionElement) => getValue(optionElement))\n .join(\"; \");\n }\n\n if (selectedOptions.length === 0) {\n return \"\";\n }\n\n return getValue(selectedOptions[0]);\n}\n\nfunction getInputValue(node: HTMLInputElement) {\n if (ignoredInputTypes.has(node.type)) {\n return \"\";\n }\n\n return getValue(node);\n}\n\nfunction getValue(node: HTMLElementWithValue) {\n const localName = getLocalName(node);\n\n // TODO: handle use of explicit roles where a value taken from content is\n // expected, e.g. combobox.\n // See core-aam/combobox-value-calculation-manual.html\n if (!allowedLocalNames.has(localName)) {\n return \"\";\n }\n\n if (\n node.getAttribute(\"aria-valuetext\") ||\n node.getAttribute(\"aria-valuenow\")\n ) {\n return \"\";\n }\n\n return typeof node.value === \"number\" ? `${node.value}` : node.value;\n}\n\nexport function getAccessibleValue(node: Node) {\n if (!isElement(node)) {\n return \"\";\n }\n\n switch (getLocalName(node)) {\n case \"input\": {\n return getInputValue(node as HTMLInputElement);\n }\n case \"select\": {\n return getSelectValue(node as HTMLSelectElement);\n }\n }\n\n return getValue(node as HTMLElementWithValue);\n}\n","const dialogRoles = new Set([\"dialog\", \"alertdialog\"]);\n\nexport const isDialogRole = (role: string) => dialogRoles.has(role);\n","import { isElement } from \"./isElement\";\n\nexport function getNodeByIdRef({\n container,\n idRef,\n}: {\n container: Node;\n idRef: string;\n}) {\n if (!isElement(container) || !idRef) {\n return null;\n }\n\n return container.querySelector(`#${CSS.escape(idRef)}`);\n}\n","import { isElement } from \"./isElement\";\n\nconst TEXT_NODE = 3;\n\nexport function isHiddenFromAccessibilityTree(node: Node | null): node is null {\n if (!node) {\n return true;\n }\n\n // `node.textContent` is only `null` for `document` and `doctype`.\n\n if (node.nodeType === TEXT_NODE && node.textContent!.trim()) {\n return false;\n }\n\n if (!isElement(node)) {\n return true;\n }\n\n try {\n if (node.hidden === true) {\n return true;\n }\n\n if (node.getAttribute(\"aria-hidden\") === \"true\") {\n return true;\n }\n\n const getComputedStyle = node.ownerDocument.defaultView?.getComputedStyle;\n const computedStyle = getComputedStyle?.(node);\n\n if (\n computedStyle?.visibility === \"hidden\" ||\n computedStyle?.display === \"none\"\n ) {\n return true;\n }\n } catch {\n // Some elements aren't supported by DOM implementations such as JSDOM.\n // E.g. `<math>`, see https://github.com/jsdom/jsdom/issues/3515\n // We ignore these nodes at the moment as we can't support them.\n return true;\n }\n\n return false;\n}\n","import { AccessibleAttributeToLabelMap } from \"./getNodeAccessibilityData/getAccessibleAttributeLabels/index\";\nimport { getIdRefsByAttribute } from \"./getIdRefsByAttribute\";\nimport { getNodeAccessibilityData } from \"./getNodeAccessibilityData/index\";\nimport { getNodeByIdRef } from \"./getNodeByIdRef\";\nimport { isDialogRole } from \"./isDialogRole\";\nimport { isElement } from \"./isElement\";\nimport { isHiddenFromAccessibilityTree } from \"./isHiddenFromAccessibilityTree\";\n\nexport interface AccessibilityNode {\n accessibleAttributeLabels: string[];\n accessibleAttributeToLabelMap: AccessibleAttributeToLabelMap;\n accessibleDescription: string;\n accessibleName: string;\n accessibleValue: string;\n allowedAccessibilityChildRoles: string[];\n alternateReadingOrderParents: Node[];\n childrenPresentational: boolean;\n isInert: boolean;\n node: Node;\n parentAccessibilityNodeTree: AccessibilityNodeTree | null;\n parent: Node | null;\n parentDialog: HTMLElement | null;\n role: string;\n spokenRole: string;\n}\n\nexport interface AccessibilityNodeTree\n extends Omit<\n AccessibilityNode,\n \"accessibleAttributeLabels\" | \"accessibleAttributeToLabelMap\"\n > {\n children: AccessibilityNodeTree[];\n}\n\ninterface AccessibilityContext {\n alternateReadingOrderMap: Map<Node, Set<Node>>;\n container: Node;\n ownedNodes: Set<Node>;\n visitedNodes: Set<Node>;\n}\n\nfunction addAlternateReadingOrderNodes(\n node: Element,\n alternateReadingOrderMap: Map<Node, Set<Node>>,\n container: Element\n) {\n const idRefs = getIdRefsByAttribute({\n attributeName: \"aria-flowto\",\n node,\n });\n\n idRefs.forEach((idRef) => {\n const childNode = getNodeByIdRef({ container, idRef });\n\n if (!childNode) {\n return;\n }\n\n const currentParentNodes =\n alternateReadingOrderMap.get(childNode) ?? new Set<Node>();\n\n currentParentNodes.add(node);\n\n alternateReadingOrderMap.set(childNode, currentParentNodes);\n });\n}\n\nfunction mapAlternateReadingOrder(node: Node) {\n const alternateReadingOrderMap = new Map<Node, Set<Node>>();\n\n if (!isElement(node)) {\n return alternateReadingOrderMap;\n }\n\n node\n .querySelectorAll(\"[aria-flowto]\")\n .forEach((parentNode) =>\n addAlternateReadingOrderNodes(parentNode, alternateReadingOrderMap, node)\n );\n\n return alternateReadingOrderMap;\n}\n\nfunction addOwnedNodes(\n node: Element,\n ownedNodes: Set<Node>,\n container: Element\n) {\n const idRefs = getIdRefsByAttribute({\n attributeName: \"aria-owns\",\n node,\n });\n\n idRefs.forEach((idRef) => {\n const ownedNode = getNodeByIdRef({ container, idRef });\n\n if (!!ownedNode && !ownedNodes.has(ownedNode)) {\n ownedNodes.add(ownedNode);\n }\n });\n}\n\nfunction getAllOwnedNodes(node: Node) {\n const ownedNodes = new Set<Node>();\n\n if (!isElement(node)) {\n return ownedNodes;\n }\n\n node\n .querySelectorAll(\"[aria-owns]\")\n .forEach((owningNode) => addOwnedNodes(owningNode, ownedNodes, node));\n\n return ownedNodes;\n}\n\nfunction getOwnedNodes(node: Node, container: Node) {\n const ownedNodes = new Set<Node>();\n\n if (!isElement(node) || !isElement(container)) {\n return ownedNodes;\n }\n\n addOwnedNodes(node, ownedNodes, container);\n\n return ownedNodes;\n}\n\nfunction growTree(\n node: Node,\n tree: Omit<\n AccessibilityNodeTree,\n \"accessibleAttributeLabels\" | \"accessibleAttributeToLabelMap\"\n >,\n {\n alternateReadingOrderMap,\n container,\n ownedNodes,\n visitedNodes,\n }: AccessibilityContext\n): AccessibilityNodeTree {\n /**\n * Authors MUST NOT create circular references with aria-owns. In the case of\n * authoring error with aria-owns, the user agent MAY ignore some aria-owns\n * element references in order to build a consistent model of the content.\n *\n * REF: https://www.w3.org/TR/wai-aria-1.2/#aria-owns\n */\n if (visitedNodes.has(node)) {\n return tree;\n }\n\n visitedNodes.add(node);\n\n const parentDialog = isDialogRole(tree.role)\n ? (tree.node as HTMLElement)\n : tree.parentDialog;\n\n if (parentDialog) {\n tree.parentDialog = parentDialog;\n }\n\n node.childNodes.forEach((childNode) => {\n if (isHiddenFromAccessibilityTree(childNode)) {\n return;\n }\n\n // REF: https://github.com/w3c/aria/issues/1817#issuecomment-1261602357\n if (ownedNodes.has(childNode)) {\n return;\n }\n\n const alternateReadingOrderParents = alternateReadingOrderMap.has(childNode)\n ? // `alternateReadingOrderMap.has(childNode)` null guards here.\n\n Array.from(alternateReadingOrderMap.get(childNode)!)\n : [];\n\n const {\n accessibleDescription,\n accessibleName,\n accessibleValue,\n allowedAccessibilityChildRoles,\n childrenPresentational,\n isExplicitPresentational,\n isInert,\n role,\n spokenRole,\n } = getNodeAccessibilityData({\n allowedAccessibilityRoles: tree.allowedAccessibilityChildRoles,\n inheritedImplicitInert: tree.isInert,\n inheritedImplicitPresentational: tree.childrenPresentational,\n node: childNode,\n });\n\n const childTree = growTree(\n childNode,\n {\n accessibleDescription,\n accessibleName,\n accessibleValue,\n allowedAccessibilityChildRoles,\n alternateReadingOrderParents,\n children: [],\n childrenPresentational,\n isInert,\n node: childNode,\n parentAccessibilityNodeTree: null, // Added during flattening\n parent: node,\n parentDialog,\n role,\n spokenRole,\n },\n { alternateReadingOrderMap, container, ownedNodes, visitedNodes }\n );\n\n if (isExplicitPresentational) {\n tree.children.push(...childTree.children);\n } else {\n tree.children.push(childTree);\n }\n });\n\n /**\n * If an element has both aria-owns and DOM children then the order of the\n * child elements with respect to the parent/child relationship is the DOM\n * children first, then the elements referenced in aria-owns. If the author\n * intends that the DOM children are not first, then list the DOM children in\n * aria-owns in the desired order. Authors SHOULD NOT use aria-owns as a\n * replacement for the DOM hierarchy. If the relationship is represented in\n * the DOM, do not use aria-owns.\n *\n * REF: https://www.w3.org/TR/wai-aria-1.2/#aria-owns\n */\n const ownedChildNodes = getOwnedNodes(node, container);\n\n ownedChildNodes.forEach((childNode) => {\n if (isHiddenFromAccessibilityTree(childNode)) {\n return;\n }\n\n const alternateReadingOrderParents = alternateReadingOrderMap.has(childNode)\n ? // `alternateReadingOrderMap.has(childNode)` null guards here.\n\n Array.from(alternateReadingOrderMap.get(childNode)!)\n : [];\n\n const {\n accessibleDescription,\n accessibleName,\n accessibleValue,\n allowedAccessibilityChildRoles,\n childrenPresentational,\n isInert,\n isExplicitPresentational,\n role,\n spokenRole,\n } = getNodeAccessibilityData({\n allowedAccessibilityRoles: tree.allowedAccessibilityChildRoles,\n inheritedImplicitInert: tree.isInert,\n inheritedImplicitPresentational: tree.childrenPresentational,\n node: childNode,\n });\n\n const childTree = growTree(\n childNode,\n {\n accessibleDescription,\n accessibleName,\n accessibleValue,\n allowedAccessibilityChildRoles,\n alternateReadingOrderParents,\n children: [],\n childrenPresentational,\n isInert,\n node: childNode,\n parentAccessibilityNodeTree: null, // Added during flattening\n parent: node,\n parentDialog,\n role,\n spokenRole,\n },\n { alternateReadingOrderMap, container, ownedNodes, visitedNodes }\n );\n\n if (isExplicitPresentational) {\n tree.children.push(...childTree.children);\n } else {\n tree.children.push(childTree);\n }\n });\n\n return tree;\n}\n\nexport function createAccessibilityTree(\n node: Node | null\n): AccessibilityNodeTree | null {\n if (isHiddenFromAccessibilityTree(node)) {\n return null;\n }\n\n const alternateReadingOrderMap = mapAlternateReadingOrder(node);\n const ownedNodes = getAllOwnedNodes(node);\n const visitedNodes = new Set<Node>();\n\n const {\n accessibleDescription,\n accessibleName,\n accessibleValue,\n allowedAccessibilityChildRoles,\n childrenPresentational,\n isInert,\n role,\n spokenRole,\n } = getNodeAccessibilityData({\n allowedAccessibilityRoles: [],\n node,\n inheritedImplicitPresentational: false,\n inheritedImplicitInert: false,\n });\n\n const tree = growTree(\n node,\n {\n accessibleDescription,\n accessibleName,\n accessibleValue,\n allowedAccessibilityChildRoles,\n alternateReadingOrderParents: [],\n children: [],\n childrenPresentational,\n isInert,\n node,\n parentAccessibilityNodeTree: null,\n parent: null,\n parentDialog: null,\n role,\n spokenRole,\n },\n {\n alternateReadingOrderMap,\n container: node,\n ownedNodes,\n visitedNodes,\n }\n );\n\n return tree;\n}\n","import { AccessibilityNode } from \"../createAccessibilityTree\";\nimport { AriaAttributes } from \"./types\";\n\nexport function matchesRoles(\n node: AccessibilityNode,\n roles?: Readonly<string[]>\n) {\n if (!roles?.length) {\n return true;\n }\n\n return roles.includes(node.role);\n}\n\nexport function matchesAccessibleAttributes(\n node: AccessibilityNode,\n ariaAttributes?: Readonly<AriaAttributes>\n) {\n if (!ariaAttributes) {\n return true;\n }\n\n for (const [name, value] of Object.entries(ariaAttributes)) {\n if (node.accessibleAttributeToLabelMap[name]?.value !== value) {\n return false;\n }\n }\n\n return true;\n}\n","import { ARIAAttribute, ARIARole, roles } from \"html-aria\";\nimport { globalStatesAndProperties } from \"../getRole\";\n\nconst ignoreAttributesWithAccessibleValue = new Set([\"aria-placeholder\"]);\n\nconst nonSpecCompliantAttributeMap: Record<\n string,\n Record<string, boolean | number | string | null>\n> = {\n listitem: { \"aria-level\": null },\n option: { \"aria-selected\": false },\n};\n\nexport const getAttributesByRole = ({\n accessibleValue,\n role,\n}: {\n accessibleValue: string;\n role: string;\n}): [string, string | null][] => {\n const {\n supported: supportedAttributes = [],\n defaultAttributeValues = {},\n prohibited: prohibitedAttributes = [],\n } = roles[role as ARIARole] ?? {};\n\n const implicitRoleAttributes = {\n ...defaultAttributeValues,\n ...nonSpecCompliantAttributeMap[role],\n };\n\n const uniqueAttributes = Array.from(\n new Set([\n ...Object.keys(implicitRoleAttributes),\n ...supportedAttributes,\n ...globalStatesAndProperties,\n ])\n )\n .filter(\n (attribute) => !prohibitedAttributes.includes(attribute as ARIAAttribute)\n )\n .filter(\n (attribute) =>\n !accessibleValue || !ignoreAttributesWithAccessibleValue.has(attribute)\n );\n\n return uniqueAttributes.map((attribute) => [\n attribute,\n attribute in implicitRoleAttributes &&\n implicitRoleAttributes[attribute] !== null\n ? implicitRoleAttributes[attribute].toString()\n : null,\n ]);\n};\n","import { AccessibilityNode } from \"./createAccessibilityTree\";\n\nexport const getItemText = (\n accessibilityNode: Pick<\n AccessibilityNode,\n \"accessibleName\" | \"accessibleValue\"\n >\n) => {\n const { accessibleName, accessibleValue } = accessibilityNode;\n const announcedValue =\n accessibleName === accessibleValue ? \"\" : accessibleValue;\n\n return [accessibleName, announcedValue].filter(Boolean).join(\", \");\n};\n","import { getAccessibleName } from \"../getAccessibleName\";\nimport { getAccessibleValue } from \"../getAccessibleValue\";\nimport { getItemText } from \"../../getItemText\";\nimport { getNodeByIdRef } from \"../../getNodeByIdRef\";\n\ntype ValueOf<T> = T[keyof T];\n\nconst STATE = {\n BUSY: \"busy\",\n CHECKED: \"checked\",\n CURRENT: \"current item\",\n DISABLED: \"disabled\",\n EXPANDED: \"expanded\",\n INVALID: \"invalid\",\n MODAL: \"modal\",\n MULTI_SELECTABLE: \"multi-selectable\",\n PARTIALLY_CHECKED: \"partially checked\",\n PARTIALLY_PRESSED: \"partially pressed\",\n PRESSED: \"pressed\",\n READ_ONLY: \"read only\",\n REQUIRED: \"required\",\n SELECTED: \"selected\",\n};\n\n// https://www.w3.org/TR/wai-aria-1.2/#state_prop_def\nconst ariaPropertyToVirtualLabelMap: Record<\n string,\n ((mapperArgs: MapperArgs) => string | null) | null\n> = {\n \"aria-activedescendant\": idRef(\"active descendant\"),\n \"aria-atomic\": null, // Handled by live region logic\n \"aria-autocomplete\": token({\n inline: \"autocomplete inlined\",\n list: \"autocomplete in list\",\n both: \"autocomplete inlined and in list\",\n none: \"no autocomplete\",\n }),\n \"aria-braillelabel\": null, // Currently won't do - not implementing a braille screen reader\n \"aria-brailleroledescription\": null, // Currently won't do - not implementing a braille screen reader\n \"aria-busy\": state(STATE.BUSY),\n \"aria-checked\": tristate(STATE.CHECKED, STATE.PARTIALLY_CHECKED),\n \"aria-colcount\": integer(\"column count\"),\n \"aria-colindex\": integer(\"column index\"),\n \"aria-colindextext\": string(\"column index\"),\n \"aria-colspan\": integer(\"column span\"),\n \"aria-controls\": idRefs(\"control\", \"controls\"), // Handled by virtual.perform()\n \"aria-current\": token({\n page: \"current page\",\n step: \"current step\",\n location: \"current location\",\n date: \"current date\",\n time: \"current time\",\n true: STATE.CURRENT,\n false: `not ${STATE.CURRENT}`,\n }),\n \"aria-describedby\": null, // Handled by accessible description\n \"aria-description\": null, // Handled by accessible description\n \"aria-details\": idRefs(\"linked details\", \"linked details\", false),\n \"aria-disabled\": state(STATE.DISABLED),\n \"aria-dropeffect\": null, // Deprecated in WAI-ARIA 1.1\n \"aria-errormessage\": errorMessageIdRefs(\"error message\", \"error messages\"),\n \"aria-expanded\": state(STATE.EXPANDED),\n \"aria-flowto\": idRefs(\"alternate reading order\", \"alternate reading orders\"), // Handled by virtual.perform()\n \"aria-grabbed\": null, // Deprecated in WAI-ARIA 1.1\n \"aria-haspopup\": token({\n /**\n * Assistive technologies SHOULD NOT expose the aria-haspopup property if\n * it has a value of false.\n *\n * REF: // https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup\n */\n false: null,\n true: \"has popup menu\",\n menu: \"has popup menu\",\n listbox: \"has popup listbox\",\n tree: \"has popup tree\",\n grid: \"has popup grid\",\n dialog: \"has popup dialog\",\n }),\n \"aria-hidden\": null, // Excluded from accessibility tree\n \"aria-invalid\": token({\n grammar: \"grammatical error detected\",\n false: `not ${STATE.INVALID}`,\n spelling: \"spelling error detected\",\n true: STATE.INVALID,\n }),\n \"aria-keyshortcuts\": string(\"key shortcuts\"),\n \"aria-label\": null, // Handled by accessible name\n \"aria-labelledby\": null, // Handled by accessible name\n \"aria-level\": integer(\"level\"),\n \"aria-live\": null, // Handled by live region logic\n \"aria-modal\": state(STATE.MODAL),\n \"aria-multiselectable\": state(STATE.MULTI_SELECTABLE),\n \"aria-orientation\": token({\n horizontal: \"orientated horizontally\",\n vertical: \"orientated vertically\",\n }),\n \"aria-owns\": null, // Handled by accessibility tree construction\n \"aria-placeholder\": string(\"placeholder\"),\n \"aria-posinset\": integer(\"position\"),\n \"aria-pressed\": tristate(STATE.PRESSED, STATE.PARTIALLY_PRESSED),\n \"aria-readonly\": state(STATE.READ_ONLY),\n \"aria-relevant\": null, // Handled by live region logic\n \"aria-required\": state(STATE.REQUIRED),\n \"aria-roledescription\": null, // Handled by accessible description\n \"aria-rowcount\": integer(\"row count\"),\n \"aria-rowindex\": integer(\"row index\"),\n \"aria-rowindextext\": string(\"row index\"),\n \"aria-rowspan\": integer(\"row span\"),\n \"aria-selected\": state(STATE.SELECTED),\n \"aria-setsize\": integer(\"set size\"),\n \"aria-sort\": token({\n ascending: \"sorted in ascending order\",\n descending: \"sorted in descending order\",\n none: \"no defined sort order\",\n other: \"non ascending / descending sort order applied\",\n }),\n \"aria-valuemax\": number(\"max value\"),\n \"aria-valuemin\": number(\"min value\"),\n \"aria-valuenow\": number(\"current value\"),\n \"aria-valuetext\": string(\"current value\"),\n};\n\ninterface MapperArgs {\n attributeValue: string;\n container: Node;\n negative?: boolean;\n node?: HTMLElement;\n}\n\nfunction state(stateValue: ValueOf<typeof STATE>) {\n return function stateMapper({ attributeValue, negative }: MapperArgs) {\n if (negative) {\n return attributeValue !== \"false\" ? `not ${stateValue}` : stateValue;\n }\n\n return attributeValue !== \"false\" ? stateValue : `not ${stateValue}`;\n };\n}\n\nfunction errorMessageIdRefs(\n propertyDescriptionSuffixSingular: string,\n propertyDescriptionSuffixPlural: string,\n printCount = true\n) {\n return function mapper({ attributeValue, container, node }: MapperArgs) {\n // TODO: use implicit values for aria-invalid:\n // - spellcheck\n // - pattern\n if (node?.getAttribute(\"aria-invalid\") === \"false\") {\n return \"\";\n }\n\n return idRefs(\n propertyDescriptionSuffixSingular,\n propertyDescriptionSuffixPlural,\n printCount\n )({ attributeValue, container });\n };\n}\n\nfunction idRefs(\n propertyDescriptionSuffixSingular: string,\n propertyDescriptionSuffixPlural: string,\n printCount = true\n) {\n return function mapper({ attributeValue, container }: MapperArgs) {\n const idRefsCount = attributeValue\n .trim()\n .split(\" \")\n .filter(\n (idRef) => !!container && !!getNodeByIdRef({ container, idRef })\n ).length;\n\n if (idRefsCount === 0) {\n return \"\";\n }\n\n return `${printCount ? `${idRefsCount} ` : \"\"}${\n idRefsCount === 1\n ? propertyDescriptionSuffixSingular\n : propertyDescriptionSuffixPlural\n }`;\n };\n}\n\nfunction idRef(propertyName: string) {\n return function mapper({ attributeValue: idRef, container }: MapperArgs) {\n const node = getNodeByIdRef({ container, idRef });\n\n if (!node) {\n return \"\";\n }\n\n const accessibleName = getAccessibleName(node);\n const accessibleValue = getAccessibleValue(node);\n const itemText = getItemText({ accessibleName, accessibleValue });\n\n return concat(propertyName)({ attributeValue: itemText, container });\n };\n}\n\nfunction tristate(\n stateValue: ValueOf<typeof STATE>,\n mixedValue: ValueOf<typeof STATE>\n) {\n return function stateMapper({ attributeValue }: MapperArgs) {\n if (attributeValue === \"mixed\") {\n return mixedValue;\n }\n\n return attributeValue !== \"false\" ? stateValue : `not ${stateValue}`;\n };\n}\n\nfunction token(tokenMap: Record<string, string | null>) {\n return function tokenMapper({ attributeValue }: MapperArgs) {\n return tokenMap[attributeValue];\n };\n}\n\nfunction concat(propertyName: string) {\n return function mapper({ attributeValue }: MapperArgs) {\n return attributeValue ? `${propertyName} ${attributeValue}` : \"\";\n };\n}\n\nfunction integer(propertyName: string) {\n return concat(propertyName);\n}\n\nfunction number(propertyName: string) {\n return concat(propertyName);\n}\n\nfunction string(propertyName: string) {\n return concat(propertyName);\n}\n\nexport const mapAttributeNameAndValueToLabel = ({\n attributeName,\n attributeValue,\n container,\n negative = false,\n node,\n}: {\n attributeName: string;\n attributeValue: string | null;\n container: Node;\n negative?: boolean;\n node: HTMLElement;\n}) => {\n if (typeof attributeValue !== \"string\") {\n return null;\n }\n\n const mapper = ariaPropertyToVirtualLabelMap[attributeName];\n\n return mapper?.({ attributeValue, container, negative, node }) ?? null;\n};\n","import { mapAttributeNameAndValueToLabel } from \"./mapAttributeNameAndValueToLabel\";\n\nexport const getLabelFromAriaAttribute = ({\n attributeName,\n container,\n node,\n}: {\n attributeName: string;\n container: Node;\n node: HTMLElement;\n}): { label: string; value: string } => {\n const attributeValue = node.getAttribute(attributeName);\n\n return {\n label:\n mapAttributeNameAndValueToLabel({\n attributeName,\n attributeValue,\n container,\n node,\n }) ?? \"\",\n value: attributeValue ?? \"\",\n };\n};\n","import { getLocalName } from \"../../getLocalName\";\nimport { mapAttributeNameAndValueToLabel } from \"./mapAttributeNameAndValueToLabel\";\n\nconst isNotMatchingElement = ({\n elements,\n node,\n}: {\n elements: string[];\n node: HTMLElement;\n}) => elements.length && !elements.includes(getLocalName(node));\n\nconst isNotMatchingProperties = ({\n node,\n properties,\n}: {\n node: HTMLElement;\n properties: { key: string; value: string }[];\n}) =>\n properties.length &&\n !properties.some(({ key, value }) => node.getAttribute(key) === value);\n\n// REFs:\n// - https://www.w3.org/TR/html-aria/#docconformance-attr\n// - https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings\nconst ariaToHTMLAttributeMapping: Record<\n string,\n Array<{\n elements?: string[];\n implicitMissingValue?: string;\n name: string;\n negative?: boolean;\n properties?: { key: string; value: string }[];\n value?: string;\n }>\n> = {\n \"aria-autocomplete\": [\n { elements: [\"form\"], name: \"autocomplete\" },\n { elements: [\"input\", \"select\", \"textarea\"], name: \"autocomplete\" },\n ],\n \"aria-checked\": [\n {\n elements: [\"input\"],\n implicitMissingValue: \"false\",\n name: \"checked\",\n properties: [\n { key: \"type\", value: \"checkbox\" },\n { key: \"type\", value: \"radio\" },\n ],\n },\n {\n value: \"mixed\",\n name: \"indeterminate\",\n },\n ],\n \"aria-colspan\": [{ elements: [\"td\", \"th\"], name: \"colspan\" }],\n \"aria-controls\": [\n {\n elements: [\"input\"],\n name: \"list\",\n },\n ],\n \"aria-disabled\": [\n {\n elements: [\"button\", \"input\", \"optgroup\", \"option\", \"select\", \"textarea\"],\n name: \"disabled\",\n },\n {\n // TODO: Form controls within a valid legend child element of a fieldset\n // with a disabled attribute do not become disabled.\n elements: [\"fieldset\"],\n name: \"disabled\",\n },\n ],\n\n // TODO: Set properties on the summary element.\n // REF: https://www.w3.org/TR/html-aam-1.0/#att-open-details\n // \"aria-expanded\": [{ elements: [\"details\"], name: \"open\" }],\n\n // TODO: Set properties on the dialog element.\n // REF: https://www.w3.org/TR/html-aam-1.0/#att-open-dialog\n\n // Not announced, indeed it will be hidden from the accessibility tree.\n // \"aria-hidden\": [{ name: \"hidden\" }],\n\n \"aria-invalid\": [\n // TODO: If the value doesn't match the pattern: aria-invalid=\"true\";\n // Otherwise, aria-invalid=\"false\"\n // REF: https://www.w3.org/TR/html-aam-1.0/#att-pattern\n // { elements: [\"input\"], name: \"pattern\" },\n // TODO: aria-invalid=\"spelling\" or grammar\n // REF: https://www.w3.org/TR/html-aam-1.0/#att-spellcheck\n // { elements: [\"input\"], name: \"spellcheck\" },\n ],\n\n \"aria-multiselectable\": [{ elements: [\"select\"], name: \"multiple\" }],\n \"aria-placeholder\": [\n { elements: [\"input\", \"textarea\"], name: \"placeholder\" },\n ],\n \"aria-valuemax\": [\n { elements: [\"input\"], name: \"max\" },\n { elements: [\"meter\", \"progress\"], name: \"max\" },\n ],\n \"aria-valuemin\": [\n { elements: [\"input\"], name: \"min\" },\n { elements: [\"meter\", \"progress\"], name: \"min\" },\n ],\n \"aria-valuenow\": [{ elements: [\"meter\", \"progress\"], name: \"value\" }],\n \"aria-readonly\": [\n { elements: [\"input\", \"textarea\"], name: \"readonly\" },\n { name: \"contenteditable\", negative: true },\n ],\n \"aria-required\": [\n { elements: [\"input\", \"select\", \"textarea\"], name: \"required\" },\n ],\n \"aria-rowspan\": [{ elements: [\"td\", \"th\"], name: \"rowspan\" }],\n \"aria-selected\": [{ elements: [\"option\"], name: \"selected\" }],\n};\n\nexport const getLabelFromHtmlEquivalentAttribute = ({\n attributeName,\n container,\n node,\n}: {\n attributeName: string;\n container: Node;\n node: HTMLElement;\n}): { label: string; value: string } => {\n const htmlAttribute = ariaToHTMLAttributeMapping[attributeName];\n\n if (!htmlAttribute?.length) {\n return { label: \"\", value: \"\" };\n }\n\n for (const {\n elements = [],\n implicitMissingValue,\n name,\n negative = false,\n properties = [],\n value,\n } of htmlAttribute) {\n if (isNotMatchingElement({ elements, node })) {\n continue;\n }\n\n if (isNotMatchingProperties({ node, properties })) {\n continue;\n }\n\n const attributeValue = node.hasAttribute(name)\n ? value ?? node.getAttribute(name)!\n : node.hasAttribute(attributeName)\n ? null\n : implicitMissingValue ?? null;\n\n const label = mapAttributeNameAndValueToLabel({\n attributeName,\n attributeValue,\n container,\n negative,\n node,\n });\n\n if (label) {\n return { label, value: attributeValue ?? \"\" };\n }\n }\n\n return { label: \"\", value: \"\" };\n};\n","import type { AccessibilityNodeTree } from \"../../../createAccessibilityTree\";\n\n/**\n * If the DOM ancestry accurately represents the level, the user agent can\n * calculate the level of an item from the document structure. This attribute\n * can be used to provide an explicit indication of the level when that is\n * not possible to calculate from the document structure or the aria-owns\n * attribute. User agent support for automatic calculation of level may vary;\n * authors SHOULD test with user agents and assistive technologies to\n * determine whether this attribute is needed. If the author int