From c551c9e9f3ca5ed6bc1e2c25b17d70b878ec7a0e Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:11:19 -0600 Subject: [PATCH 1/4] chore: support elements not in tree for get/hasAriaValue --- lib/commons/aria/get-aria-value.js | 28 +++++++++++++++++-------- lib/commons/aria/has-aria-value.js | 32 +++++++++++++++++------------ test/commons/aria/get-aria-value.js | 25 ++++++++++++++++++++++ test/commons/aria/has-aria-value.js | 19 ++++++++++++++++- 4 files changed, 81 insertions(+), 23 deletions(-) diff --git a/lib/commons/aria/get-aria-value.js b/lib/commons/aria/get-aria-value.js index 6a3fa0c692..5dcf280634 100644 --- a/lib/commons/aria/get-aria-value.js +++ b/lib/commons/aria/get-aria-value.js @@ -1,4 +1,4 @@ -import { nodeLookup } from '../../core/utils'; +import { nodeLookup, getElementInternals } from '../../core/utils'; import standards from '../../standards'; const idrefTypes = ['idref', 'idrefs']; @@ -36,11 +36,10 @@ export default function getAriaValue(node, attrName, options = {}) { } const { type } = attrStandard; - const { vNode } = nodeLookup(node); const { lowercase } = options; for (const { source, getValue } of sources) { - let value = getValue(vNode, attrStandard, attrName); + let value = getValue(node, attrStandard, attrName); if (value === null || value === undefined) { continue; } @@ -69,9 +68,10 @@ export default function getAriaValue(node, attrName, options = {}) { return null; } -function getAttributeValue(vNode, attrStandard, attrName) { +function getAttributeValue(node, attrStandard, attrName) { + const { vNode, domNode } = nodeLookup(node); const { type } = attrStandard; - const value = vNode.attr(attrName); + const value = vNode ? vNode.attr(attrName) : domNode.getAttribute(attrName); // setting an ARIA idref(s) prop value can result in empty attribute values, so we'll need extra processing for idref(s) // e.g. el.ariaLabelledByElements = [label]; el.getAttribute('aria-labelledby') === '' @@ -90,12 +90,22 @@ function getAttributeValue(vNode, attrStandard, attrName) { return propEmpty ? null : value; } -function getPropertyValue(vNode, attrStandard) { +function getPropertyValue(node, attrStandard) { + const { domNode } = nodeLookup(node); const { prop } = attrStandard; - return prop && vNode.actualNode ? vNode.actualNode[prop] : null; + return prop && domNode ? domNode[prop] : null; } -function getInternalValue(vNode, attrStandard) { +function getInternalValue(node, attrStandard) { + const { vNode, domNode } = nodeLookup(node); const { prop } = attrStandard; - return prop && vNode.elementInternals ? vNode.elementInternals[prop] : null; + if (!prop) { + return null; + } + + const internals = vNode + ? vNode.elementInternals + : getElementInternals(domNode); + + return internals ? internals[prop] : null; } diff --git a/lib/commons/aria/has-aria-value.js b/lib/commons/aria/has-aria-value.js index 8db4056633..c68f5cb69d 100644 --- a/lib/commons/aria/has-aria-value.js +++ b/lib/commons/aria/has-aria-value.js @@ -1,4 +1,4 @@ -import { nodeLookup } from '../../core/utils'; +import { nodeLookup, getElementInternals } from '../../core/utils'; import standards from '../../standards'; /** @@ -16,31 +16,37 @@ export default function hasAriaValue(node, attrName) { throw new TypeError(`Attribute ${attrName} is not an ARIA attribute`); } - const { vNode } = nodeLookup(node); return ( - hasAttributeValue(vNode, attrName) || - hasPropertyValue(vNode, attrStandard) || - hasInternalValue(vNode, attrStandard) + hasAttributeValue(node, attrName) || + hasPropertyValue(node, attrStandard) || + hasInternalValue(node, attrStandard) ); } -function hasAttributeValue(vNode, attrName) { - return vNode.hasAttr(attrName); +function hasAttributeValue(node, attrName) { + const { vNode, domNode } = nodeLookup(node); + return vNode ? vNode.hasAttr(attrName) : domNode.hasAttribute(attrName); } -function hasPropertyValue(vNode, attrStandard) { +function hasPropertyValue(node, attrStandard) { const { prop } = attrStandard; - if (prop && vNode.actualNode) { - const propValue = vNode.actualNode[prop]; + const { domNode } = nodeLookup(node); + if (prop && domNode) { + const propValue = domNode[prop]; return propValue !== null && propValue !== undefined; } return false; } -function hasInternalValue(vNode, attrStandard) { +function hasInternalValue(node, attrStandard) { + const { vNode, domNode } = nodeLookup(node); const { prop } = attrStandard; - if (prop && vNode.elementInternals) { - const internalsValue = vNode.elementInternals[prop]; + const internals = vNode + ? vNode.elementInternals + : getElementInternals(domNode); + + if (prop && internals) { + const internalsValue = internals[prop]; return internalsValue !== null && internalsValue !== undefined; } return false; diff --git a/test/commons/aria/get-aria-value.js b/test/commons/aria/get-aria-value.js index b05d36f88d..7ee060e5d5 100644 --- a/test/commons/aria/get-aria-value.js +++ b/test/commons/aria/get-aria-value.js @@ -161,6 +161,31 @@ describe('aria.getAriaValue', () => { assert.isNull(getAriaValue(vNode, 'aria-label')); }); + it('returns attribute from an element not in the tree', () => { + fixture.innerHTML = html`
`; + const node = fixture.querySelector('#target'); + + const result = getAriaValue(node, 'aria-label'); + assert.deepEqual(result, { + value: 'hello', + source: 'attribute' + }); + }); + + it('returns element internals from an element not in the tree', () => { + fixture.innerHTML = html``; + const node = fixture.querySelector('#target'); + + const result = getAriaValue(node, 'aria-label'); + assert.deepEqual(result, { + value: 'hello', + source: 'internals' + }); + }); + describe('idref', () => { it('returns the attribute value over the property value', () => { const vNode = queryFixture( diff --git a/test/commons/aria/has-aria-value.js b/test/commons/aria/has-aria-value.js index b1eb6ac0d9..e0e563ae73 100644 --- a/test/commons/aria/has-aria-value.js +++ b/test/commons/aria/has-aria-value.js @@ -1,5 +1,5 @@ describe('aria.hasAriaValue', () => { - const { queryFixture, html } = axe.testUtils; + const { queryFixture, fixture, html } = axe.testUtils; const hasAriaValue = axe.commons.aria.hasAriaValue; const SerialVirtualNode = axe.SerialVirtualNode; @@ -78,6 +78,23 @@ describe('aria.hasAriaValue', () => { assert.isFalse(hasAriaValue(vNode, 'aria-label')); }); + it('returns true if element not in the tree has attribute', () => { + fixture.innerHTML = html`
`; + const node = fixture.querySelector('#target'); + + assert.isTrue(hasAriaValue(node, 'aria-label')); + }); + + it('returns true if element has elementInternals', () => { + fixture.innerHTML = html``; + const node = fixture.querySelector('#target'); + + assert.isTrue(hasAriaValue(node, 'aria-label')); + }); + describe('SerialVirtualNode', () => { it('returns true if element has attribute', () => { // SerialVirtualNode will not support `props` or `elementInternals` so everything must be part of the `attributes` property From 57b364f89cd27c1b1562c394b976fd609fffd975 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:18:30 -0600 Subject: [PATCH 2/4] typo --- test/commons/aria/has-aria-value.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commons/aria/has-aria-value.js b/test/commons/aria/has-aria-value.js index e0e563ae73..ec712a926c 100644 --- a/test/commons/aria/has-aria-value.js +++ b/test/commons/aria/has-aria-value.js @@ -85,7 +85,7 @@ describe('aria.hasAriaValue', () => { assert.isTrue(hasAriaValue(node, 'aria-label')); }); - it('returns true if element has elementInternals', () => { + it('returns true if element not in the tree has elementInternals', () => { fixture.innerHTML = html` Date: Wed, 17 Jun 2026 08:11:39 -0600 Subject: [PATCH 3/4] fix --- lib/commons/aria/get-aria-value.js | 2 +- test/commons/aria/get-aria-value.js | 31 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/commons/aria/get-aria-value.js b/lib/commons/aria/get-aria-value.js index 5dcf280634..a642784955 100644 --- a/lib/commons/aria/get-aria-value.js +++ b/lib/commons/aria/get-aria-value.js @@ -85,7 +85,7 @@ function getAttributeValue(node, attrStandard, attrName) { } // return null for the attribute if the value is empty but the idref(s) prop is not - const propValue = getPropertyValue(vNode, attrStandard); + const propValue = getPropertyValue(node, attrStandard); const propEmpty = Array.isArray(propValue) ? propValue.length : !!propValue; return propEmpty ? null : value; } diff --git a/test/commons/aria/get-aria-value.js b/test/commons/aria/get-aria-value.js index 7ee060e5d5..0a111e29e6 100644 --- a/test/commons/aria/get-aria-value.js +++ b/test/commons/aria/get-aria-value.js @@ -229,6 +229,21 @@ describe('aria.getAriaValue', () => { source: 'attribute' }); }); + + it('returns stringified value from an element not in the tree', () => { + fixture.innerHTML = html`
+
+
`; + const node = fixture.querySelector('#target'); + const child = fixture.querySelector('#child'); + node.ariaActiveDescendantElement = child; + + const result = getAriaValue(node, 'aria-activedescendant'); + assert.deepEqual(result, { + value: 'DIV', + source: 'property' + }); + }); }); describe('idrefs', () => { @@ -274,6 +289,22 @@ describe('aria.getAriaValue', () => { source: 'attribute' }); }); + + it('returns stringified value from an element not in the tree', () => { + fixture.innerHTML = html`
+
+
`; + const node = fixture.querySelector('#target'); + const label1 = fixture.querySelector('#label1'); + const label2 = fixture.querySelector('#label2'); + node.ariaLabelledByElements = [label1, label2]; + + const result = getAriaValue(node, 'aria-labelledby'); + assert.deepEqual(result, { + value: '[DIV,DIV]', + source: 'property' + }); + }); }); describe('options', () => { From 70533732f27dfd6e352b279d4dfd07570fc34cf6 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:20:46 -0600 Subject: [PATCH 4/4] suggestions --- lib/commons/aria/get-aria-value.js | 20 ++++++++++++-------- lib/commons/aria/has-aria-value.js | 24 +++++++++++++++--------- test/commons/aria/get-aria-value.js | 16 ++++++++++++++++ test/commons/aria/has-aria-value.js | 15 +++++++++++++++ 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/lib/commons/aria/get-aria-value.js b/lib/commons/aria/get-aria-value.js index a642784955..48453bf221 100644 --- a/lib/commons/aria/get-aria-value.js +++ b/lib/commons/aria/get-aria-value.js @@ -35,11 +35,12 @@ export default function getAriaValue(node, attrName, options = {}) { return null; } + const { vNode, domNode } = nodeLookup(node); const { type } = attrStandard; const { lowercase } = options; for (const { source, getValue } of sources) { - let value = getValue(node, attrStandard, attrName); + let value = getValue(vNode, domNode, attrStandard, attrName); if (value === null || value === undefined) { continue; } @@ -68,8 +69,7 @@ export default function getAriaValue(node, attrName, options = {}) { return null; } -function getAttributeValue(node, attrStandard, attrName) { - const { vNode, domNode } = nodeLookup(node); +function getAttributeValue(vNode, domNode, attrStandard, attrName) { const { type } = attrStandard; const value = vNode ? vNode.attr(attrName) : domNode.getAttribute(attrName); @@ -85,24 +85,28 @@ function getAttributeValue(node, attrStandard, attrName) { } // return null for the attribute if the value is empty but the idref(s) prop is not - const propValue = getPropertyValue(node, attrStandard); + const propValue = getPropertyValue(vNode, domNode, attrStandard); const propEmpty = Array.isArray(propValue) ? propValue.length : !!propValue; return propEmpty ? null : value; } -function getPropertyValue(node, attrStandard) { - const { domNode } = nodeLookup(node); +function getPropertyValue(vNode, domNode, attrStandard) { const { prop } = attrStandard; return prop && domNode ? domNode[prop] : null; } -function getInternalValue(node, attrStandard) { - const { vNode, domNode } = nodeLookup(node); +function getInternalValue(vNode, domNode, attrStandard) { const { prop } = attrStandard; if (!prop) { return null; } + // feature flag to enable internals. uses globalThis.axe as it can be run outside of axe context + // TODO: remove when feature is fully enabled + if (!axe._enableElementInternals) { + return null; + } + const internals = vNode ? vNode.elementInternals : getElementInternals(domNode); diff --git a/lib/commons/aria/has-aria-value.js b/lib/commons/aria/has-aria-value.js index c68f5cb69d..c489c3a0d1 100644 --- a/lib/commons/aria/has-aria-value.js +++ b/lib/commons/aria/has-aria-value.js @@ -16,21 +16,21 @@ export default function hasAriaValue(node, attrName) { throw new TypeError(`Attribute ${attrName} is not an ARIA attribute`); } + const { vNode, domNode } = nodeLookup(node); + return ( - hasAttributeValue(node, attrName) || - hasPropertyValue(node, attrStandard) || - hasInternalValue(node, attrStandard) + hasAttributeValue(vNode, domNode, attrName) || + hasPropertyValue(vNode, domNode, attrStandard) || + hasInternalValue(vNode, domNode, attrStandard) ); } -function hasAttributeValue(node, attrName) { - const { vNode, domNode } = nodeLookup(node); +function hasAttributeValue(vNode, domNode, attrName) { return vNode ? vNode.hasAttr(attrName) : domNode.hasAttribute(attrName); } -function hasPropertyValue(node, attrStandard) { +function hasPropertyValue(vNode, domNode, attrStandard) { const { prop } = attrStandard; - const { domNode } = nodeLookup(node); if (prop && domNode) { const propValue = domNode[prop]; return propValue !== null && propValue !== undefined; @@ -38,9 +38,15 @@ function hasPropertyValue(node, attrStandard) { return false; } -function hasInternalValue(node, attrStandard) { - const { vNode, domNode } = nodeLookup(node); +function hasInternalValue(vNode, domNode, attrStandard) { const { prop } = attrStandard; + + // feature flag to enable internals. uses globalThis.axe as it can be run outside of axe context + // TODO: remove when feature is fully enabled + if (!axe._enableElementInternals) { + return false; + } + const internals = vNode ? vNode.elementInternals : getElementInternals(domNode); diff --git a/test/commons/aria/get-aria-value.js b/test/commons/aria/get-aria-value.js index 0a111e29e6..30ebae016e 100644 --- a/test/commons/aria/get-aria-value.js +++ b/test/commons/aria/get-aria-value.js @@ -3,6 +3,10 @@ describe('aria.getAriaValue', () => { const getAriaValue = axe.commons.aria.getAriaValue; const SerialVirtualNode = axe.SerialVirtualNode; + afterEach(() => { + axe._enableElementInternals = true; + }); + it('returns the aria attribute value', () => { const vNode = queryFixture( html`
` @@ -173,6 +177,7 @@ describe('aria.getAriaValue', () => { }); it('returns element internals from an element not in the tree', () => { + axe._elemen; fixture.innerHTML = html` { }); }); + it('does not return element internals from an element not in the tree when feature flag is off', () => { + axe._enableElementInternals = false; + fixture.innerHTML = html``; + const node = fixture.querySelector('#target'); + + assert.isNull(getAriaValue(node, 'aria-label')); + }); + describe('idref', () => { it('returns the attribute value over the property value', () => { const vNode = queryFixture( diff --git a/test/commons/aria/has-aria-value.js b/test/commons/aria/has-aria-value.js index ec712a926c..ee0576455b 100644 --- a/test/commons/aria/has-aria-value.js +++ b/test/commons/aria/has-aria-value.js @@ -3,6 +3,10 @@ describe('aria.hasAriaValue', () => { const hasAriaValue = axe.commons.aria.hasAriaValue; const SerialVirtualNode = axe.SerialVirtualNode; + afterEach(() => { + axe._enableElementInternals = true; + }); + it('returns true if element has attribute', () => { const vNode = queryFixture( html`
` @@ -95,6 +99,17 @@ describe('aria.hasAriaValue', () => { assert.isTrue(hasAriaValue(node, 'aria-label')); }); + it('returns false if element not in the tree has elementInternals but feature flag is off', () => { + axe._enableElementInternals = false; + fixture.innerHTML = html``; + const node = fixture.querySelector('#target'); + + assert.isFalse(hasAriaValue(node, 'aria-label')); + }); + describe('SerialVirtualNode', () => { it('returns true if element has attribute', () => { // SerialVirtualNode will not support `props` or `elementInternals` so everything must be part of the `attributes` property