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: * * * * * * * * * * * * * *
TypeDescription
MixedObject 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'; }