import { validateAttrValue, getAriaValue } from '../../commons/aria';
import standards from '../../standards';
/**
* Check that each ARIA attribute on an element has a valid value.
*
* Valid ARIA attribute values are taken from the `ariaAttrs` standards object from an attributes `type` property.
*
* ##### Data:
*
*
*
* | Type |
* Description |
*
*
*
*
* Mixed |
* Object with Strings `messageKey` and `needsReview` if `aria-current` or `aria-describedby` are invalid. Otherwise a list of all invalid ARIA attributes and their value |
*
*
*
*
* @memberof checks
* @return {Mixed} True if all ARIA attributes have a valid value. Undefined for invalid `aria-current` or `aria-describedby` values. False otherwise.
*/
export default function ariaValidAttrValueEvaluate(node, options, virtualNode) {
options = Array.isArray(options.value) ? options.value : [];
let needsReview = '';
let messageKey = '';
const invalid = [];
const aria = /^aria-/;
const skipAttrs = ['aria-errormessage'];
const preChecks = {
// aria-controls should only check if element exists if the element
// doesn't have aria-expanded=false, aria-selected=false (tabs),
// or aria-haspopup (may load later)
// @see https://github.com/dequelabs/axe-core/issues/1463
// @see https://github.com/dequelabs/axe-core/issues/4363
'aria-controls': () => {
const hasPopup = !['false', null].includes(
getAriaValue(virtualNode, 'aria-haspopup').value ?? null
);
if (hasPopup) {
needsReview = `aria-controls="${getAriaValue(virtualNode, 'aria-controls').value}"`;
messageKey = 'controlsWithinPopup';
}
return (
getAriaValue(virtualNode, 'aria-expanded').value !== 'false' &&
getAriaValue(virtualNode, 'aria-selected').value !== 'false' &&
hasPopup === false
);
},
// aria-current should mark as needs review if any value is used that is
// not one of the valid values (since any value is treated as "true")
'aria-current': validValue => {
if (!validValue) {
needsReview = `aria-current="${getAriaValue(virtualNode, 'aria-current').value}"`;
messageKey = 'ariaCurrent';
}
return;
},
// aria-owns should only check if element exists if the element
// doesn't have aria-expanded=false (combobox)
// @see https://github.com/dequelabs/axe-core/issues/1524
'aria-owns': () => {
return getAriaValue(virtualNode, 'aria-expanded').value !== 'false';
},
// aria-describedby should not mark missing element as violation but
// instead as needs review
// @see https://github.com/dequelabs/axe-core/issues/1151
'aria-describedby': validValue => {
if (!validValue) {
needsReview = `aria-describedby="${getAriaValue(virtualNode, 'aria-describedby').value}"`;
// TODO: es-modules_tree
messageKey =
axe._tree && axe._tree[0]._hasShadowRoot ? 'noIdShadow' : 'noId';
}
return;
},
// aria-labelledby should not mark missing element as violation but
// instead as needs review
// @see https://github.com/dequelabs/axe-core/issues/2621
'aria-labelledby': validValue => {
if (!validValue) {
needsReview = `aria-labelledby="${getAriaValue(virtualNode, 'aria-labelledby').value}"`;
// TODO: es-modules_tree
messageKey =
axe._tree && axe._tree[0]._hasShadowRoot ? 'noIdShadow' : 'noId';
}
}
};
virtualNode.attrNames.forEach(attrName => {
if (
skipAttrs.includes(attrName) ||
options.includes(attrName) ||
!aria.test(attrName)
) {
return;
}
let validValue;
const attrValue = virtualNode.attr(attrName);
try {
validValue = validateAttrValue(virtualNode, attrName);
} catch {
needsReview = `${attrName}="${attrValue}"`;
messageKey = 'idrefs';
return;
}
if (
(preChecks[attrName] ? preChecks[attrName](validValue) : true) &&
!validValue
) {
if (attrValue === '' && !isStringType(attrName)) {
needsReview = attrName;
messageKey = 'empty';
} else {
invalid.push(`${attrName}="${attrValue}"`);
}
}
});
if (invalid.length) {
this.data(invalid);
return false;
}
if (needsReview) {
this.data({ messageKey, needsReview });
return undefined;
}
return true;
}
function isStringType(attrName) {
return standards.ariaAttrs[attrName]?.type === 'string';
}