Skip to content
Draft
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
48 changes: 48 additions & 0 deletions lib/commons/dom/get-root-children.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { nodeLookup } from '../../core/utils';
import cache from '../../core/base/cache';

/**
* Return the child virtual nodes of the root node
* @method getRootChildren
* @memberof axe.commons.dom
* @instance
* @param {Element|VirtualNode} node
* @returns {VirtualNode[]|undefined}
*/
export default function getRootChildren(node) {
const { vNode } = nodeLookup(node);
const { shadowId } = vNode;

const childrenMap = cache.get('getRootChildrenMap', () => ({}));
if (childrenMap[shadowId]) {
return childrenMap[shadowId];
}

// top of tree
if (vNode.parent === null) {
childrenMap[shadowId] = [...vNode.children];
return childrenMap[shadowId];
}

// disconnected tree
if (!vNode.parent) {
childrenMap[shadowId] = undefined;
return childrenMap[shadowId];
}

// since the virtual tree does not have a #shadowRoot element the root virtual
// node is the shadow host element. however the shadow host element is not inside
// the shadow DOM tree so we return the children of the shadow host element in
// order to not cross shadow DOM boundaries.
//
// TODO: slotted elements share the shadowId of the shadow tree it is attached to
// but should not be used to find id's inside the shadow tree. throw an error
// until we resolve this
if (vNode.shadowId !== vNode.parent.shadowId) {
throw new Error(
'Getting root children of shadow DOM elements is not supported'
);
}

return getRootChildren(vNode.parent);
}
52 changes: 37 additions & 15 deletions lib/commons/dom/idrefs.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import getRootNode from './get-root-node';
import { tokenList } from '../../core/utils';
import getRootChildren from './get-root-children';
import {
tokenList,
nodeLookup,
querySelectorAll,
getRootNode
} from '../../core/utils';

/**
* Get elements referenced via a space-separated token attribute;
Expand All @@ -18,24 +23,41 @@ import { tokenList } from '../../core/utils';
*
*/
function idrefs(node, attr) {
node = node.actualNode || node;
const { domNode, vNode } = nodeLookup(node);
const results = [];
const attrValue = vNode ? vNode.attr(attr) : node.getAttribute(attr);

if (!attrValue) {
return results;
}

try {
const doc = getRootNode(node);
const result = [];
let attrValue = node.getAttribute(attr);

if (attrValue) {
attrValue = tokenList(attrValue);
for (let index = 0; index < attrValue.length; index++) {
result.push(doc.getElementById(attrValue[index]));
}
const root = getRootNode(domNode);
for (const token of tokenList(attrValue)) {
results.push(root.getElementById(token));
}

return result;
} catch {
throw new TypeError('Cannot resolve id references for non-DOM nodes');
const rootVNodes = getRootChildren(vNode);
if (!rootVNodes) {
throw new TypeError('Cannot resolve id references for non-DOM nodes');
}

for (const token of tokenList(attrValue)) {
let result = null;

for (const root of rootVNodes) {
const foundNode = querySelectorAll(root, `#${token}`)[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For shadow root cases, this is going to essentially bypass QSA's selector-cache behavior that would otherwise make the ID lookup fast; we might want to consider making it smarter about that (findMatchingNodes looks like it's meant to understand how to do it, but getNodesMatchingExpression is going to early exit from the cached path anytime root is not the document root)

Copy link
Contributor Author

@straker straker Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya. We originally did the cache on the root to improve rule node selection. We cached the information on the root node since it was the only part that would have a complete picture of the entire tree. We could work around that limit for just id lookups, but it would require somehow passing the desired shadowId for the getNodesMatchingExpression query instead of grabbing it from the root node. Maybe we create a getElementById like function that allows you do this this? Something like:

function getById(id, shadowId) {
  const root = axe._tree;
  return getNodesMatchingExpression(root, id, () => {}, shadowId)[0]
}


// selector-cache.js
export function getNodesMatchingExpression(domTree, expressions, filter, shadowId) {
  shadowId = shadowId ?? domTree[0].shadowId;
}

if (foundNode) {
result = foundNode;
break;
}
}

results.push(result);
}
}

return results;
}

export default idrefs;
1 change: 1 addition & 0 deletions lib/commons/dom/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { default as getElementCoordinates } from './get-element-coordinates';
export { default as getElementStack } from './get-element-stack';
export { default as getModalDialog } from './get-modal-dialog';
export { default as getOverflowHiddenAncestors } from './get-overflow-hidden-ancestors';
export { default as getRootChildren } from './get-root-children';
export { default as getRootNode } from './get-root-node';
export { default as getScrollOffset } from './get-scroll-offset';
export { default as getTabbableElements } from './get-tabbable-elements';
Expand Down
6 changes: 3 additions & 3 deletions lib/commons/text/accessible-text.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import accessibleTextVirtual from './accessible-text-virtual';
import { getNodeFromTree } from '../../core/utils';
import { nodeLookup } from '../../core/utils';

/**
* Finds virtual node and calls accessibleTextVirtual()
Expand All @@ -12,8 +12,8 @@ import { getNodeFromTree } from '../../core/utils';
* @return {string}
*/
function accessibleText(element, context) {
const virtualNode = getNodeFromTree(element); // throws an exception on purpose if axe._tree not correct
Copy link
Contributor Author

@straker straker Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is not true as getNodeFromTree does not throw, just returns null if the node isn't found.

return accessibleTextVirtual(virtualNode, context);
const { vNode } = nodeLookup(element);
return accessibleTextVirtual(vNode, context);
}

export default accessibleText;
30 changes: 30 additions & 0 deletions test/commons/dom/get-root-children.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
describe('dom.getRootChildren', () => {
const getRootChildren = axe.commons.dom.getRootChildren;
const fixture = document.querySelector('#fixture');
const queryShadowFixture = axe.testUtils.queryShadowFixture;

it('should return the children of the root node of a complete tree', () => {
axe.setup();
const expected = axe.utils.getNodeFromTree(
document.documentElement
).children;
assert.deepEqual(getRootChildren(fixture), expected);
});

it('should return undefined for disconnected tree', () => {
axe.setup();
axe.utils.getNodeFromTree(document.documentElement).parent = undefined;
assert.isUndefined(getRootChildren(fixture));
});

it('should throw for shadow DOM', () => {
const target = queryShadowFixture(
'<div id="shadow"></div>',
'<div id="target">Hello World</div><div id="child1"></div><div id="child2"></div>'
);

assert.throws(() => {
getRootChildren(target);
});
});
});
Loading