Skip to content
Open
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
34 changes: 24 additions & 10 deletions lib/commons/aria/get-aria-value.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { nodeLookup } from '../../core/utils';
import { nodeLookup, getElementInternals } from '../../core/utils';
import standards from '../../standards';

const idrefTypes = ['idref', 'idrefs'];
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Comment thread
straker marked this conversation as resolved.

// 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') === ''
Expand All @@ -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;
}

Comment thread
straker marked this conversation as resolved.
// 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;
}
38 changes: 25 additions & 13 deletions lib/commons/aria/has-aria-value.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { nodeLookup } from '../../core/utils';
import { nodeLookup, getElementInternals } from '../../core/utils';
import standards from '../../standards';

/**
Expand All @@ -16,31 +16,43 @@ export default function hasAriaValue(node, attrName) {
throw new TypeError(`Attribute ${attrName} is not an ARIA attribute`);
}

const { vNode } = nodeLookup(node);
Comment thread
straker marked this conversation as resolved.
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here, need to put the feature flag gate in. Or maybe put that into getElementInternals directly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We didn't want to put it directly into getElementInternals as it's used by the gather-internals script and that couldn't really use a global axe due to the extensions needing it.


if (prop && internals) {
const internalsValue = internals[prop];
return internalsValue !== null && internalsValue !== undefined;
}
return false;
Expand Down
72 changes: 72 additions & 0 deletions test/commons/aria/get-aria-value.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`<div id="target" aria-label="hello"></div>`
Expand Down Expand Up @@ -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`<div id="target" aria-label="hello"></div>`;
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`<testutils-element
id="target"
with-aria-label="hello"
></testutils-element>`;
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`<testutils-element
id="target"
with-aria-label="hello"
></testutils-element>`;
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(
Expand Down Expand Up @@ -204,6 +245,21 @@ describe('aria.getAriaValue', () => {
source: 'attribute'
});
});

it('returns stringified value from an element not in the tree', () => {
fixture.innerHTML = html`<div id="target">
<div id="child"></div>
</div>`;
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', () => {
Expand Down Expand Up @@ -249,6 +305,22 @@ describe('aria.getAriaValue', () => {
source: 'attribute'
});
});

it('returns stringified value from an element not in the tree', () => {
fixture.innerHTML = html`<div id="target"></div>
<div id="label1"></div>
<div id="label2"></div>`;
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', () => {
Expand Down
34 changes: 33 additions & 1 deletion test/commons/aria/has-aria-value.js
Original file line number Diff line number Diff line change
@@ -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`<div id="target" aria-label="hello"></div>`
Expand Down Expand Up @@ -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`<div id="target" aria-label="hello"></div>`;
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`<testutils-element
id="target"
with-aria-label="hello"
></testutils-element>`;
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`<testutils-element
id="target"
with-aria-label="hello"
></testutils-element>`;
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
Expand Down
Loading