@guidepup/virtual-screen-reader
Advanced tools
Comparing version 0.9.1 to 0.10.0
@@ -7,4 +7,6 @@ export interface AccessibilityNode { | ||
allowedAccessibilityChildRoles: string[][]; | ||
alternateReadingOrderParents: Node[]; | ||
childrenPresentational: boolean; | ||
node: Node; | ||
parent: Node | null; | ||
role: string; | ||
@@ -11,0 +13,0 @@ spokenRole: string; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createAccessibilityTree = void 0; | ||
const getIdRefsByAttribute_1 = require("./getIdRefsByAttribute"); | ||
const getNodeAccessibilityData_1 = require("./getNodeAccessibilityData"); | ||
const getNodeByIdRef_1 = require("./getNodeByIdRef"); | ||
const isElement_1 = require("./isElement"); | ||
const dom_accessibility_api_1 = require("dom-accessibility-api"); | ||
function addOwnedNodes(owningNode, ownedNodes) { | ||
const ownedNodesIdRefs = (owningNode.getAttribute("aria-owns") ?? "") | ||
.trim() | ||
.split(" "); | ||
ownedNodesIdRefs.filter(Boolean).forEach((id) => { | ||
const ownedNode = document.querySelector(`#${id}`); | ||
function addAlternateReadingOrderNodes(node, alternateReadingOrderMap, container) { | ||
const idRefs = (0, getIdRefsByAttribute_1.getIdRefsByAttribute)({ | ||
attributeName: "aria-flowto", | ||
node, | ||
}); | ||
idRefs.forEach((idRef) => { | ||
const childNode = (0, getNodeByIdRef_1.getNodeByIdRef)({ container, idRef }); | ||
if (!childNode) { | ||
return; | ||
} | ||
const currentParentNodes = alternateReadingOrderMap.get(childNode) ?? new Set(); | ||
currentParentNodes.add(node); | ||
alternateReadingOrderMap.set(childNode, currentParentNodes); | ||
}); | ||
} | ||
function mapAlternateReadingOrder(node) { | ||
const alternateReadingOrderMap = new Map(); | ||
if (!(0, isElement_1.isElement)(node)) { | ||
return alternateReadingOrderMap; | ||
} | ||
node | ||
.querySelectorAll("[aria-flowto]") | ||
.forEach((parentNode) => addAlternateReadingOrderNodes(parentNode, alternateReadingOrderMap, node)); | ||
return alternateReadingOrderMap; | ||
} | ||
function addOwnedNodes(node, ownedNodes, container) { | ||
const idRefs = (0, getIdRefsByAttribute_1.getIdRefsByAttribute)({ | ||
attributeName: "aria-owns", | ||
node, | ||
}); | ||
idRefs.forEach((idRef) => { | ||
const ownedNode = (0, getNodeByIdRef_1.getNodeByIdRef)({ container, idRef }); | ||
if (!!ownedNode && !ownedNodes.has(ownedNode)) { | ||
@@ -25,11 +53,11 @@ ownedNodes.add(ownedNode); | ||
.querySelectorAll("[aria-owns]") | ||
.forEach((owningNode) => addOwnedNodes(owningNode, ownedNodes)); | ||
.forEach((owningNode) => addOwnedNodes(owningNode, ownedNodes, node)); | ||
return ownedNodes; | ||
} | ||
function getOwnedNodes(node) { | ||
function getOwnedNodes(node, container) { | ||
const ownedNodes = new Set(); | ||
if (!(0, isElement_1.isElement)(node)) { | ||
if (!(0, isElement_1.isElement)(node) || !(0, isElement_1.isElement)(container)) { | ||
return ownedNodes; | ||
} | ||
addOwnedNodes(node, ownedNodes); | ||
addOwnedNodes(node, ownedNodes, container); | ||
return ownedNodes; | ||
@@ -61,5 +89,6 @@ } | ||
const { children, ...treeNode } = tree; | ||
const isAnnounced = treeNode.accessibleName || | ||
treeNode.accessibleDescription || | ||
treeNode.spokenRole; | ||
const isAnnounced = !!treeNode.accessibleName || | ||
!!treeNode.accessibleDescription || | ||
treeNode.accessibleAttributeLabels.length > 0 || | ||
!!treeNode.spokenRole; | ||
const ignoreChildren = shouldIgnoreChildren(tree); | ||
@@ -80,4 +109,6 @@ const flattenedTree = ignoreChildren | ||
allowedAccessibilityChildRoles: treeNode.allowedAccessibilityChildRoles, | ||
alternateReadingOrderParents: treeNode.alternateReadingOrderParents, | ||
childrenPresentational: treeNode.childrenPresentational, | ||
node: treeNode.node, | ||
parent: treeNode.parent, | ||
role: treeNode.role, | ||
@@ -89,3 +120,3 @@ spokenRole: `end of ${treeNode.spokenRole}`, | ||
} | ||
function growTree(node, tree, { container, ownedNodes, visitedNodes }) { | ||
function growTree(node, tree, { alternateReadingOrderMap, container, ownedNodes, visitedNodes, }) { | ||
/** | ||
@@ -110,4 +141,8 @@ * Authors MUST NOT create circular references with aria-owns. In the case of | ||
} | ||
const alternateReadingOrderParents = alternateReadingOrderMap.has(childNode) | ||
? Array.from(alternateReadingOrderMap.get(childNode)) | ||
: []; | ||
const { accessibleAttributeLabels, accessibleDescription, accessibleName, accessibleValue, allowedAccessibilityChildRoles, childrenPresentational, role, spokenRole, } = (0, getNodeAccessibilityData_1.getNodeAccessibilityData)({ | ||
allowedAccessibilityRoles: tree.allowedAccessibilityChildRoles, | ||
alternateReadingOrderParents, | ||
container, | ||
@@ -123,8 +158,10 @@ node: childNode, | ||
allowedAccessibilityChildRoles, | ||
alternateReadingOrderParents, | ||
children: [], | ||
childrenPresentational, | ||
node: childNode, | ||
parent: node, | ||
role, | ||
spokenRole, | ||
}, { container, ownedNodes, visitedNodes })); | ||
}, { alternateReadingOrderMap, container, ownedNodes, visitedNodes })); | ||
}); | ||
@@ -142,3 +179,3 @@ /** | ||
*/ | ||
const ownedChildNodes = getOwnedNodes(node); | ||
const ownedChildNodes = getOwnedNodes(node, container); | ||
ownedChildNodes.forEach((childNode) => { | ||
@@ -148,4 +185,8 @@ if (isHiddenFromAccessibilityTree(childNode)) { | ||
} | ||
const alternateReadingOrderParents = alternateReadingOrderMap.has(childNode) | ||
? Array.from(alternateReadingOrderMap.get(childNode)) | ||
: []; | ||
const { accessibleAttributeLabels, accessibleDescription, accessibleName, accessibleValue, allowedAccessibilityChildRoles, childrenPresentational, role, spokenRole, } = (0, getNodeAccessibilityData_1.getNodeAccessibilityData)({ | ||
allowedAccessibilityRoles: tree.allowedAccessibilityChildRoles, | ||
alternateReadingOrderParents, | ||
container, | ||
@@ -161,8 +202,10 @@ node: childNode, | ||
allowedAccessibilityChildRoles, | ||
alternateReadingOrderParents, | ||
children: [], | ||
childrenPresentational, | ||
node: childNode, | ||
parent: node, | ||
role, | ||
spokenRole, | ||
}, { container, ownedNodes, visitedNodes })); | ||
}, { alternateReadingOrderMap, container, ownedNodes, visitedNodes })); | ||
}); | ||
@@ -175,2 +218,3 @@ return tree; | ||
} | ||
const alternateReadingOrderMap = mapAlternateReadingOrder(node); | ||
const ownedNodes = getAllOwnedNodes(node); | ||
@@ -180,2 +224,3 @@ const visitedNodes = new Set(); | ||
allowedAccessibilityRoles: [], | ||
alternateReadingOrderParents: [], | ||
container: node, | ||
@@ -191,8 +236,11 @@ node, | ||
allowedAccessibilityChildRoles, | ||
alternateReadingOrderParents: [], | ||
children: [], | ||
childrenPresentational, | ||
node, | ||
parent: null, | ||
role, | ||
spokenRole, | ||
}, { | ||
alternateReadingOrderMap, | ||
container: node, | ||
@@ -199,0 +247,0 @@ ownedNodes, |
@@ -1,3 +0,4 @@ | ||
export declare const getAccessibleAttributeLabels: ({ accessibleValue, container, node, role, }: { | ||
export declare const getAccessibleAttributeLabels: ({ accessibleValue, alternateReadingOrderParents, container, node, role, }: { | ||
accessibleValue: string; | ||
alternateReadingOrderParents: Node[]; | ||
container: Node; | ||
@@ -4,0 +5,0 @@ node: Node; |
@@ -11,3 +11,3 @@ "use strict"; | ||
const postProcessLabels_1 = require("./postProcessLabels"); | ||
const getAccessibleAttributeLabels = ({ accessibleValue, container, node, role, }) => { | ||
const getAccessibleAttributeLabels = ({ accessibleValue, alternateReadingOrderParents, container, node, role, }) => { | ||
if (!(0, isElement_1.isElement)(node)) { | ||
@@ -68,4 +68,19 @@ return []; | ||
}); | ||
return (0, postProcessLabels_1.postProcessLabels)({ labels, role }); | ||
const processedLabels = (0, postProcessLabels_1.postProcessLabels)({ labels, role }).filter(Boolean); | ||
/** | ||
* aria-flowto MUST requirements: | ||
* | ||
* The reading order goes both directions, and a user needs to be aware of the | ||
* alternate reading order so that they can invoke the functionality. | ||
* | ||
* The reading order goes both directions, and a user needs to be able to | ||
* travel backwards through their chosen reading order. | ||
* | ||
* REF: https://a11ysupport.io/tech/aria/aria-flowto_attribute | ||
*/ | ||
if (alternateReadingOrderParents.length > 0) { | ||
processedLabels.push(`${alternateReadingOrderParents.length} previous alternate reading ${alternateReadingOrderParents.length === 1 ? "order" : "orders"}`); | ||
} | ||
return processedLabels; | ||
}; | ||
exports.getAccessibleAttributeLabels = getAccessibleAttributeLabels; |
@@ -7,3 +7,3 @@ "use strict"; | ||
const getItemText_1 = require("../../getItemText"); | ||
const isElement_1 = require("../../isElement"); | ||
const getNodeByIdRef_1 = require("../../getNodeByIdRef"); | ||
var State; | ||
@@ -28,3 +28,3 @@ (function (State) { | ||
const ariaPropertyToVirtualLabelMap = { | ||
"aria-activedescendant": idref("active descendant"), | ||
"aria-activedescendant": idRef("active descendant"), | ||
"aria-atomic": null, | ||
@@ -45,3 +45,3 @@ "aria-autocomplete": token({ | ||
"aria-colspan": integer("column span"), | ||
"aria-controls": null, | ||
"aria-controls": idRefs("control", "controls"), | ||
"aria-current": token({ | ||
@@ -63,3 +63,3 @@ page: "current page", | ||
"aria-expanded": state(State.EXPANDED), | ||
"aria-flowto": null, | ||
"aria-flowto": idRefs("alternate reading order", "alternate reading orders"), | ||
"aria-grabbed": null, | ||
@@ -132,8 +132,19 @@ "aria-haspopup": token({ | ||
} | ||
function idref(propertyName) { | ||
return function mapper({ attributeValue: idref, container }) { | ||
if (!(0, isElement_1.isElement)(container) || !idref) { | ||
function idRefs(propertyDescriptionSuffixSingular, propertyDescriptionSuffixPlural) { | ||
return function mapper({ attributeValue, container }) { | ||
const idRefsCount = attributeValue | ||
.trim() | ||
.split(" ") | ||
.filter((idRef) => !!(0, getNodeByIdRef_1.getNodeByIdRef)({ container, idRef })).length; | ||
if (idRefsCount === 0) { | ||
return ""; | ||
} | ||
const node = container.querySelector(`#${idref}`); | ||
return `${idRefsCount} ${idRefsCount === 1 | ||
? propertyDescriptionSuffixSingular | ||
: propertyDescriptionSuffixPlural}`; | ||
}; | ||
} | ||
function idRef(propertyName) { | ||
return function mapper({ attributeValue: idRef, container }) { | ||
const node = (0, getNodeByIdRef_1.getNodeByIdRef)({ container, idRef }); | ||
if (!node) { | ||
@@ -140,0 +151,0 @@ return ""; |
@@ -1,3 +0,4 @@ | ||
export declare function getNodeAccessibilityData({ allowedAccessibilityRoles, container, inheritedImplicitPresentational, node, }: { | ||
export declare function getNodeAccessibilityData({ allowedAccessibilityRoles, alternateReadingOrderParents, container, inheritedImplicitPresentational, node, }: { | ||
allowedAccessibilityRoles: string[][]; | ||
alternateReadingOrderParents: Node[]; | ||
container: Node; | ||
@@ -4,0 +5,0 @@ inheritedImplicitPresentational: boolean; |
@@ -38,3 +38,3 @@ "use strict"; | ||
}; | ||
function getNodeAccessibilityData({ allowedAccessibilityRoles, container, inheritedImplicitPresentational, node, }) { | ||
function getNodeAccessibilityData({ allowedAccessibilityRoles, alternateReadingOrderParents, container, inheritedImplicitPresentational, node, }) { | ||
const accessibleDescription = (0, getAccessibleDescription_1.getAccessibleDescription)(node); | ||
@@ -51,2 +51,3 @@ const accessibleName = (0, getAccessibleName_1.getAccessibleName)(node); | ||
accessibleValue, | ||
alternateReadingOrderParents, | ||
container, | ||
@@ -60,3 +61,8 @@ node, | ||
const isGeneric = role === "generic"; | ||
const spokenRole = getSpokenRole({ isGeneric, isPresentational, node, role }); | ||
const spokenRole = getSpokenRole({ | ||
isGeneric, | ||
isPresentational, | ||
node, | ||
role, | ||
}); | ||
const { requiredOwnedElements: allowedAccessibilityChildRoles } = aria_query_1.roles.get(role) ?? { requiredOwnedElements: [] }; | ||
@@ -63,0 +69,0 @@ const { requiredOwnedElements: implicitAllowedAccessibilityChildRoles } = aria_query_1.roles.get(implicitRole) ?? { requiredOwnedElements: [] }; |
import { CommandOptions, ScreenReader } from "@guidepup/guidepup"; | ||
import { VirtualCommandKey, VirtualCommands } from "./commands"; | ||
import { VirtualCommandArgs } from "./commands/types"; | ||
export interface StartOptions extends CommandOptions { | ||
@@ -8,3 +10,3 @@ /** | ||
*/ | ||
container: HTMLElement; | ||
container: Node; | ||
} | ||
@@ -23,2 +25,12 @@ /** | ||
/** | ||
* Getter for screen reader commands. | ||
* | ||
* Use with `await virtual.perform(command)`. | ||
*/ | ||
get commands(): { | ||
jumpToControlledElement: "jumpToControlledElement"; | ||
moveToNextAlternateReadingOrderElement: "moveToNextAlternateReadingOrderElement"; | ||
moveToPreviousAlternateReadingOrderElement: "moveToPreviousAlternateReadingOrderElement"; | ||
}; | ||
/** | ||
* Detect whether the screen reader is supported for the current OS. | ||
@@ -108,5 +120,8 @@ * | ||
* | ||
* Currently not implemented. | ||
* @param {string} command Screen reader command. | ||
* @param {object} [options] Command options. | ||
*/ | ||
perform(): Promise<void>; | ||
perform<T extends VirtualCommandKey, K extends Omit<Parameters<VirtualCommands[T]>[0], keyof VirtualCommandArgs>>(command: T, options?: { | ||
[L in keyof K]: K[L]; | ||
} & CommandOptions): Promise<void>; | ||
/** | ||
@@ -113,0 +128,0 @@ * Click the mouse. |
@@ -6,2 +6,3 @@ "use strict"; | ||
const guidepup_1 = require("@guidepup/guidepup"); | ||
const commands_1 = require("./commands"); | ||
const errors_1 = require("./errors"); | ||
@@ -11,3 +12,2 @@ const getItemText_1 = require("./getItemText"); | ||
const isElement_1 = require("./isElement"); | ||
const notImplemented_1 = require("./notImplemented"); | ||
const user_event_1 = require("@testing-library/user-event"); | ||
@@ -171,2 +171,13 @@ const defaultUserEventOptions = { | ||
/** | ||
* Getter for screen reader commands. | ||
* | ||
* Use with `await virtual.perform(command)`. | ||
*/ | ||
get commands() { | ||
return Object.fromEntries(Object.keys(commands_1.commands).map((command) => [ | ||
command, | ||
command, | ||
])); | ||
} | ||
/** | ||
* Detect whether the screen reader is supported for the current OS. | ||
@@ -359,81 +370,25 @@ * | ||
* | ||
* Currently not implemented. | ||
* @param {string} command Screen reader command. | ||
* @param {object} [options] Command options. | ||
*/ | ||
async perform() { | ||
async perform(command, options) { | ||
this.#checkContainer(); | ||
await tick(); | ||
/** | ||
* TODO: Assistive technologies SHOULD enable users to quickly navigate to | ||
* elements with role banner. | ||
* | ||
* REF: https://w3c.github.io/aria/#banner | ||
*/ | ||
/** | ||
* TODO: Assistive technologies SHOULD enable users to quickly navigate to | ||
* elements with role complementary. | ||
* | ||
* REF: https://w3c.github.io/aria/#complementary | ||
*/ | ||
/** | ||
* TODO: Assistive technologies SHOULD enable users to quickly navigate to | ||
* elements with role contentinfo. | ||
* | ||
* REF: https://w3c.github.io/aria/#contentinfo | ||
*/ | ||
/** | ||
* TODO: Assistive technologies SHOULD enable users to quickly navigate to | ||
* figures. | ||
* | ||
* REF: https://w3c.github.io/aria/#figure | ||
*/ | ||
/** | ||
* TODO: Assistive technologies SHOULD enable users to quickly navigate to | ||
* elements with role form. | ||
* | ||
* REF: https://w3c.github.io/aria/#form | ||
*/ | ||
/** | ||
* TODO: Assistive technologies SHOULD enable users to quickly navigate to | ||
* landmark regions. | ||
* | ||
* REF: https://w3c.github.io/aria/#landmark | ||
*/ | ||
/** | ||
* TODO: Assistive technologies SHOULD enable users to quickly navigate to | ||
* elements with role main. | ||
* | ||
* REF: https://w3c.github.io/aria/#main | ||
*/ | ||
/** | ||
* TODO: Assistive technologies SHOULD enable users to quickly navigate to | ||
* elements with role navigation. | ||
* | ||
* REF: https://w3c.github.io/aria/#navigation | ||
*/ | ||
/** | ||
* TODO: Assistive technologies SHOULD enable users to quickly navigate to | ||
* elements with role region. | ||
* | ||
* REF: https://w3c.github.io/aria/#region | ||
*/ | ||
/** | ||
* TODO: Assistive technologies SHOULD enable users to quickly navigate to | ||
* elements with role search. | ||
* | ||
* REF: https://w3c.github.io/aria/#search | ||
*/ | ||
/** | ||
* TODO: However, when aria-flowto is provided with multiple ID | ||
* references, assistive technologies SHOULD present the referenced | ||
* elements as path choices. | ||
* | ||
* In the case of one or more ID references, user agents or assistive | ||
* technologies SHOULD give the user the option of navigating to any of the | ||
* targeted elements. The name of the path can be determined by the name of | ||
* the target element of the aria-flowto attribute. Accessibility APIs can | ||
* provide named path relationships. | ||
* | ||
* REF: https://w3c.github.io/aria/#aria-flowto | ||
*/ | ||
(0, notImplemented_1.notImplemented)(); | ||
const tree = this.#getAccessibilityTree(); | ||
if (!tree.length) { | ||
return; | ||
} | ||
const currentIndex = this.#getCurrentIndex(tree); | ||
const nextIndex = commands_1.commands[command]?.({ | ||
...options, | ||
container: this.#container, | ||
currentIndex, | ||
tree, | ||
}); | ||
if (typeof nextIndex !== "number") { | ||
return; | ||
} | ||
const newActiveNode = tree.at(nextIndex); | ||
this.#updateState(newActiveNode); | ||
return; | ||
} | ||
@@ -440,0 +395,0 @@ /** |
{ | ||
"name": "@guidepup/virtual-screen-reader", | ||
"version": "0.9.1", | ||
"version": "0.10.0", | ||
"description": "Virtual screen reader driver for unit test automation.", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
90926
61
2215