diff --git a/lib/commons/aria/get-aria-value.js b/lib/commons/aria/get-aria-value.js index 6a3fa0c692..48453bf221 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']; @@ -35,12 +35,12 @@ export default function getAriaValue(node, attrName, options = {}) { return null; } + const { vNode, domNode } = nodeLookup(node); 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(vNode, domNode, attrStandard, attrName); if (value === null || value === undefined) { continue; } @@ -69,9 +69,9 @@ export default function getAriaValue(node, attrName, options = {}) { return null; } -function getAttributeValue(vNode, attrStandard, attrName) { +function getAttributeValue(vNode, domNode, attrStandard, attrName) { 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') === '' @@ -85,17 +85,31 @@ function getAttributeValue(vNode, 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(vNode, domNode, attrStandard); const propEmpty = Array.isArray(propValue) ? propValue.length : !!propValue; return propEmpty ? null : value; } -function getPropertyValue(vNode, attrStandard) { +function getPropertyValue(vNode, domNode, attrStandard) { const { prop } = attrStandard; - return prop && vNode.actualNode ? vNode.actualNode[prop] : null; + return prop && domNode ? domNode[prop] : null; } -function getInternalValue(vNode, attrStandard) { +function getInternalValue(vNode, domNode, attrStandard) { const { prop } = attrStandard; - return prop && vNode.elementInternals ? vNode.elementInternals[prop] : null; + 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); + + 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..c489c3a0d1 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,43 @@ export default function hasAriaValue(node, attrName) { throw new TypeError(`Attribute ${attrName} is not an ARIA attribute`); } - const { vNode } = nodeLookup(node); + const { vNode, domNode } = nodeLookup(node); + return ( - hasAttributeValue(vNode, attrName) || - hasPropertyValue(vNode, attrStandard) || - hasInternalValue(vNode, attrStandard) + hasAttributeValue(vNode, domNode, attrName) || + hasPropertyValue(vNode, domNode, attrStandard) || + hasInternalValue(vNode, domNode, attrStandard) ); } -function hasAttributeValue(vNode, attrName) { - return vNode.hasAttr(attrName); +function hasAttributeValue(vNode, domNode, attrName) { + return vNode ? vNode.hasAttr(attrName) : domNode.hasAttribute(attrName); } -function hasPropertyValue(vNode, attrStandard) { +function hasPropertyValue(vNode, domNode, attrStandard) { const { prop } = attrStandard; - if (prop && vNode.actualNode) { - const propValue = vNode.actualNode[prop]; + if (prop && domNode) { + const propValue = domNode[prop]; return propValue !== null && propValue !== undefined; } return false; } -function hasInternalValue(vNode, attrStandard) { +function hasInternalValue(vNode, domNode, attrStandard) { const { prop } = attrStandard; - if (prop && vNode.elementInternals) { - const internalsValue = vNode.elementInternals[prop]; + + // 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); + + 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..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`
` @@ -161,6 +165,43 @@ 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', () => { + axe._elemen; + fixture.innerHTML = html``; + const node = fixture.querySelector('#target'); + + const result = getAriaValue(node, 'aria-label'); + assert.deepEqual(result, { + value: 'hello', + source: 'internals' + }); + }); + + 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( @@ -204,6 +245,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', () => { @@ -249,6 +305,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', () => { diff --git a/test/commons/aria/has-aria-value.js b/test/commons/aria/has-aria-value.js index b1eb6ac0d9..ee0576455b 100644 --- a/test/commons/aria/has-aria-value.js +++ b/test/commons/aria/has-aria-value.js @@ -1,8 +1,12 @@ describe('aria.hasAriaValue', () => { - const { queryFixture, html } = axe.testUtils; + const { queryFixture, fixture, html } = axe.testUtils; 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`
` @@ -78,6 +82,34 @@ 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 not in the tree has elementInternals', () => { + fixture.innerHTML = html``; + const node = fixture.querySelector('#target'); + + 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