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