Skip to content

fix(idref): fallback to qsa #4844

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
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
38 changes: 38 additions & 0 deletions lib/commons/dom/get-root-vnodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { nodeLookup } from '../../core/utils';

/**
* Return the vNode(s)
* @method getRootVNodes
* @memberof axe.commons.dom
* @instance
* @param {Element|VirtualNode} node
* @returns {VirtualNode[]}
*/
export default function getRootVNodes(node) {
const { vNode } = nodeLookup(node);

if (vNode._rootNodes) {
return vNode._rootNodes;
}

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

// disconnected tree
if (!vNode.parent) {
return undefined;
}

// 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
if (vNode.shadowId !== vNode.parent.shadowId) {
return [...vNode.parent.children];
}

vNode._rootNodes = getRootVNodes(vNode.parent);
return vNode._rootNodes;
}
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 getRootVNodes from './get-root-vnodes';
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 = getRootVNodes(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];
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 @@ -15,6 +15,7 @@ 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 getRootNode } from './get-root-node';
export { default as getRootVNodes } from './get-root-vnodes';
export { default as getScrollOffset } from './get-scroll-offset';
export { default as getTabbableElements } from './get-tabbable-elements';
export { default as getTargetRects } from './get-target-rects';
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;
27 changes: 27 additions & 0 deletions test/commons/dom/get-root-vnodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
describe('dom.getRootVNodes', () => {
const getRootVNodes = axe.commons.dom.getRootVNodes;
const fixture = document.querySelector('#fixture');
const queryShadowFixture = axe.testUtils.queryShadowFixture;

it('should return the root vNode of complete tree', () => {
axe.setup();
const expected = [axe.utils.getNodeFromTree(document.documentElement)];
assert.deepEqual(getRootVNodes(fixture), expected);
});

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

it('should return each child of a shadow DOM host', () => {
const target = queryShadowFixture(
'<div id="shadow"></div>',
'<div id="target">Hello World</div><div id="child1"></div><div id="child2"></div>'
);

const expected = target.parent.children;
assert.deepEqual(getRootVNodes(target), expected);
});
});
Loading