UNPKG

axe-core

Version:

Accessibility engine for automated Web UI testing

157 lines (138 loc) 4.66 kB
/* global document */ import { getAccessibleRefs } from '../commons/aria'; import { findUpVirtual, visuallyOverlaps, getRootNode } from '../commons/dom'; import { visibleVirtual, removeUnicode, sanitize } from '../commons/text'; import { isDisabled } from '../commons/forms'; import { getNodeFromTree, querySelectorAll, tokenList } from '../core/utils'; function colorContrastMatches(node, virtualNode) { const { nodeName, type: inputType } = virtualNode.props; // Don't test options, color contrast doesn't work well on these if (nodeName === 'option') { return false; } // Don't test empty select elements if (nodeName === 'select' && !node.options.length) { return false; } // some input types don't have text, so the rule shouldn't be applied const nonTextInput = [ 'hidden', 'range', 'color', 'checkbox', 'radio', 'image' ]; if (nodeName === 'input' && nonTextInput.includes(inputType)) { return false; } if (isDisabled(virtualNode)) { return false; } // form elements that don't have direct child text nodes need to check that // the text indent has not been changed and moved the text away from the // control const formElements = ['input', 'select', 'textarea']; if (formElements.includes(nodeName)) { const style = window.getComputedStyle(node); const textIndent = parseInt(style.getPropertyValue('text-indent'), 10); if (textIndent) { // since we can't get the actual bounding rect of the text node, we'll // use the current nodes bounding rect and adjust by the text-indent to // see if it still overlaps the node let rect = node.getBoundingClientRect(); rect = { top: rect.top, bottom: rect.bottom, left: rect.left + textIndent, right: rect.right + textIndent }; if (!visuallyOverlaps(rect, node)) { return false; } } // Match all form fields, regardless of if they have text return true; } const nodeParentLabel = findUpVirtual(virtualNode, 'label'); if (nodeName === 'label' || nodeParentLabel) { const labelNode = nodeParentLabel || node; const labelVirtual = nodeParentLabel ? getNodeFromTree(nodeParentLabel) : virtualNode; // explicit label of disabled control if (labelNode.htmlFor) { const doc = getRootNode(labelNode); const explicitControl = doc.getElementById(labelNode.htmlFor); const explicitControlVirtual = explicitControl && getNodeFromTree(explicitControl); if (explicitControlVirtual && isDisabled(explicitControlVirtual)) { return false; } } // implicit label of disabled control const query = 'input:not(' + '[type="hidden"],' + '[type="image"],' + '[type="button"],' + '[type="submit"],' + '[type="reset"]' + '), select, textarea'; const implicitControl = querySelectorAll(labelVirtual, query)[0]; if (implicitControl && isDisabled(implicitControl)) { return false; } } const ariaLabelledbyControls = []; let ancestorNode = virtualNode; while (ancestorNode) { // Find any ancestor (including itself) that is used with aria-labelledby if (ancestorNode.props.id) { const virtualControls = getAccessibleRefs(ancestorNode) .filter(control => { return tokenList( control.getAttribute('aria-labelledby') || '' ).includes(ancestorNode.props.id); }) .map(control => getNodeFromTree(control)); ariaLabelledbyControls.push(...virtualControls); } ancestorNode = ancestorNode.parent; } if ( ariaLabelledbyControls.length > 0 && ariaLabelledbyControls.every(isDisabled) ) { return false; } const visibleText = visibleVirtual(virtualNode, false, true); const removeUnicodeOptions = { emoji: true, nonBmp: false, punctuations: true }; if (!visibleText || !removeUnicode(visibleText, removeUnicodeOptions)) { return false; } const range = document.createRange(); const childNodes = virtualNode.children; for (let index = 0; index < childNodes.length; index++) { const child = childNodes[index]; if ( child.actualNode.nodeType === 3 && sanitize(child.actualNode.nodeValue) !== '' ) { range.selectNodeContents(child.actualNode); } } const rects = range.getClientRects(); for (let index = 0; index < rects.length; index++) { //check to see if the rectangle impinges if (visuallyOverlaps(rects[index], node)) { return true; } } return false; } export default colorContrastMatches;