Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/Virtual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,12 @@ export class Virtual {
this.#treeCache = null;
}

async #handleFocusChange({ target }: Event) {
async #handleFocusChange(event: Event) {
const target =
typeof event.composedPath === "function" && event.composedPath().length > 0
? (event.composedPath()[0] as Node)
: (event.target as Node);

await tick();

this.#invalidateTreeCache();
Expand All @@ -314,7 +319,9 @@ export class Virtual {

// We've covered the tree having no length so there should be at least one
// matching node, but if not we will not update the state
const newActiveNode = tree.find(({ node }) => node === target);
const newActiveNode =
tree.find(({ node }) => node === target) ??
tree.find(({ node }) => node === event.target);

if (!newActiveNode) {
return;
Expand Down
101 changes: 79 additions & 22 deletions src/createAccessibilityTree.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AccessibleAttributeToLabelMap } from "./getNodeAccessibilityData/getAccessibleAttributeLabels/index";
import { getIdRefsByAttribute } from "./getIdRefsByAttribute";
import { getLocalName } from "./getLocalName";
import { getNodeAccessibilityData } from "./getNodeAccessibilityData/index";
import { getNodeByIdRef } from "./getNodeByIdRef";
import { isDialogRole } from "./isDialogRole";
Expand Down Expand Up @@ -39,10 +40,46 @@ interface AccessibilityContext {
visitedNodes: Set<Node>;
}

function querySelectorAllDeep(root: Node, selector: string): Element[] {
const results: Element[] = [];

if (!isElement(root) && root.nodeType !== 9 && root.nodeType !== 11) {
return results;
}

const container = root as Element | Document | DocumentFragment;

if (
isElement(container) &&
typeof container.matches === "function" &&
container.matches(selector)
) {
results.push(container);
}

if (typeof container.querySelectorAll === "function") {
results.push(...Array.from(container.querySelectorAll(selector)));

const allDescendants = container.querySelectorAll("*");

for (const el of allDescendants) {
if (el.shadowRoot) {
results.push(...querySelectorAllDeep(el.shadowRoot, selector));
}
}
}

if (isElement(container) && container.shadowRoot) {
results.push(...querySelectorAllDeep(container.shadowRoot, selector));
}

return Array.from(new Set(results));
}

function addAlternateReadingOrderNodes(
node: Element,
alternateReadingOrderMap: Map<Node, Set<Node>>,
container: Element
container: Node
) {
const idRefs = getIdRefsByAttribute({
attributeName: "aria-flowto",
Expand All @@ -68,23 +105,17 @@ function addAlternateReadingOrderNodes(
function mapAlternateReadingOrder(node: Node) {
const alternateReadingOrderMap = new Map<Node, Set<Node>>();

if (!isElement(node)) {
return alternateReadingOrderMap;
}

node
.querySelectorAll("[aria-flowto]")
.forEach((parentNode) =>
addAlternateReadingOrderNodes(parentNode, alternateReadingOrderMap, node)
);
querySelectorAllDeep(node, "[aria-flowto]").forEach((parentNode) =>
addAlternateReadingOrderNodes(parentNode, alternateReadingOrderMap, node)
);

return alternateReadingOrderMap;
}

function addOwnedNodes(
node: Element,
ownedNodes: Set<Node>,
container: Element
container: Node
) {
const idRefs = getIdRefsByAttribute({
attributeName: "aria-owns",
Expand All @@ -103,21 +134,17 @@ function addOwnedNodes(
function getAllOwnedNodes(node: Node) {
const ownedNodes = new Set<Node>();

if (!isElement(node)) {
return ownedNodes;
}

node
.querySelectorAll("[aria-owns]")
.forEach((owningNode) => addOwnedNodes(owningNode, ownedNodes, node));
querySelectorAllDeep(node, "[aria-owns]").forEach((owningNode) =>
addOwnedNodes(owningNode, ownedNodes, node)
);

return ownedNodes;
}

function getOwnedNodes(node: Node, container: Node) {
const ownedNodes = new Set<Node>();

if (!isElement(node) || !isElement(container)) {
if (!isElement(node)) {
return ownedNodes;
}

Expand All @@ -126,6 +153,30 @@ function getOwnedNodes(node: Node, container: Node) {
return ownedNodes;
}

function getChildNodes(node: Node): Node[] {
if (isElement(node)) {
if (node.shadowRoot) {
return Array.from(node.shadowRoot.childNodes);
}

if (getLocalName(node) === "slot") {
const slot = node as HTMLSlotElement;

if (typeof slot.assignedNodes === "function") {
const assignedNodes = slot.assignedNodes({ flatten: true });

if (assignedNodes.length > 0 || !node.childNodes.length) {
return assignedNodes;
}
}

return Array.from(node.childNodes);
}
}

return Array.from(node.childNodes);
}

function growTree(
node: Node,
tree: Omit<
Expand Down Expand Up @@ -160,7 +211,7 @@ function growTree(
tree.parentDialog = parentDialog;
}

node.childNodes.forEach((childNode) => {
getChildNodes(node).forEach((childNode) => {
if (isHiddenFromAccessibilityTree(childNode)) {
return;
}
Expand Down Expand Up @@ -214,7 +265,10 @@ function growTree(
{ alternateReadingOrderMap, container, ownedNodes, visitedNodes }
);

if (isExplicitPresentational) {
if (
isExplicitPresentational ||
(isElement(childNode) && getLocalName(childNode) === "slot")
) {
tree.children.push(...childTree.children);
} else {
tree.children.push(childTree);
Expand Down Expand Up @@ -283,7 +337,10 @@ function growTree(
{ alternateReadingOrderMap, container, ownedNodes, visitedNodes }
);

if (isExplicitPresentational) {
if (
isExplicitPresentational ||
(isElement(childNode) && getLocalName(childNode) === "slot")
) {
tree.children.push(...childTree.children);
} else {
tree.children.push(childTree);
Expand Down
7 changes: 6 additions & 1 deletion src/getElementFromNode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { isElement } from "./isElement";

export const getElementFromNode = (node: Node): HTMLElement => {
return isElement(node) ? node : node.parentElement!;
return isElement(node)
? node
: (node.parentElement ??
(((node.parentNode as (Node & { host?: HTMLElement }) | null)
?.host as HTMLElement) ||
(node.parentElement as HTMLElement)));
};
7 changes: 6 additions & 1 deletion src/getLiveSpokenPhrase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,12 @@ function getLiveRegionAttributes(
};
}

const targetAncestor = target.parentElement;
const targetAncestor =
target.parentElement ??
((target.parentNode as (Node & { host?: HTMLElement }) | null)?.host as
| HTMLElement
| undefined) ??
null;

if (target === container || targetAncestor === null) {
return {
Expand Down
64 changes: 59 additions & 5 deletions src/getNodeByIdRef.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,69 @@
import { isElement } from "./isElement";

function isContainer(
node: Node
): node is Element | Document | DocumentFragment {
return (
isElement(node) ||
node.nodeType === 9 || // DOCUMENT_NODE
node.nodeType === 11 // DOCUMENT_FRAGMENT_NODE
);
}

function findNodeByIdDeep(root: Node, idRef: string): Element | null {
if (!isContainer(root) || !idRef) {
return null;
}

const escaped = `#${CSS.escape(idRef)}`;

if (typeof root.querySelector === "function") {
try {
const found = root.querySelector(escaped);

if (found) {
return found;
}
} catch {
// ignore invalid selector error
}
}

if (isElement(root) && root.id === idRef) {
return root;
}

if (isElement(root) && root.shadowRoot) {
const found = findNodeByIdDeep(root.shadowRoot, idRef);

if (found) {
return found;
}
}

if (typeof root.querySelectorAll === "function") {
const allDescendants = root.querySelectorAll("*");

for (const el of allDescendants) {
if (el.shadowRoot) {
const found = findNodeByIdDeep(el.shadowRoot, idRef);

if (found) {
return found;
}
}
}
}

return null;
}

export function getNodeByIdRef({
container,
idRef,
}: {
container: Node;
idRef: string;
}) {
if (!isElement(container) || !idRef) {
return null;
}

return container.querySelector(`#${CSS.escape(idRef)}`);
return findNodeByIdDeep(container, idRef);
}
4 changes: 2 additions & 2 deletions src/isElement.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const ELEMENT_NODE = 1;

export function isElement(node: Node): node is HTMLElement {
return node.nodeType === ELEMENT_NODE;
export function isElement(node: Node | null | undefined): node is HTMLElement {
return !!node && node.nodeType === ELEMENT_NODE;
}
66 changes: 59 additions & 7 deletions src/observeDOM.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,48 @@
import { isElement } from "./isElement";
import type { Root } from "./Virtual";

const OBSERVER_CONFIG: MutationObserverInit = {
attributes: true,
characterData: true,
childList: true,
subtree: true,
};

function observeSubtree(
observer: MutationObserver,
node: Node,
observedNodes: Set<Node>
) {
if (observedNodes.has(node)) {
return;
}

observer.observe(node, OBSERVER_CONFIG);
observedNodes.add(node);

if (isElement(node) && node.shadowRoot) {
observeSubtree(observer, node.shadowRoot, observedNodes);
}

if (
isElement(node) ||
node.nodeType === 9 || // DOCUMENT_NODE
node.nodeType === 11 // DOCUMENT_FRAGMENT_NODE
) {
const root = node as Element | Document | DocumentFragment;

if (typeof root.querySelectorAll === "function") {
const descendants = root.querySelectorAll("*");

for (const el of descendants) {
if (el.shadowRoot) {
observeSubtree(observer, el.shadowRoot, observedNodes);
}
}
}
}
}

export function observeDOM(
root: Root | undefined,
node: Node,
Expand All @@ -14,17 +56,27 @@ export function observeDOM(
typeof root !== "undefined" ? root?.MutationObserver : null;

if (MutationObserver) {
const mutationObserver = new MutationObserver(onChange);
const observedNodes = new Set<Node>();

const mutationObserver = new MutationObserver(
(mutations: MutationRecord[], observer: MutationObserver) => {
for (const mutation of mutations) {
if (mutation.type === "childList") {
mutation.addedNodes.forEach((addedNode) => {
observeSubtree(observer, addedNode, observedNodes);
});
}
}

onChange(mutations, observer);
}
);

mutationObserver.observe(node, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
});
observeSubtree(mutationObserver, node, observedNodes);

return () => {
mutationObserver.disconnect();
observedNodes.clear();
};
}

Expand Down
Loading
Loading