Skip to content

Commit

Permalink
fix(color-contrast): check for size before ignoring pseudo elements (#…
Browse files Browse the repository at this point in the history
…3097)

* fix(color-contrast): check for size before ignoring pseudo elements

* chore(eslint): disable no-use-bef-re-define

* chore: tweak tests for IE

* chore: address IE11 problem

* Apply suggestions from code review

Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com>

* chore: tweak

* Update lib/checks/color/color-contrast-evaluate.js

Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com>

Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com>
  • Loading branch information
WilcoFiers and straker authored Jul 27, 2021
1 parent e79c65c commit e0f6c0c
Show file tree
Hide file tree
Showing 5 changed files with 504 additions and 430 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ module.exports = {
},
rules: {
'func-names': [2, 'as-needed'],
'prefer-const': 2
'prefer-const': 2,
'no-use-before-define': 'off'
}
},
{
Expand Down
3 changes: 2 additions & 1 deletion doc/check-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,12 @@ All checks allow these global options:
| ----------------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ignoreUnicode` | `true` | Do not check the color contrast of Unicode characters |
| `ignoreLength` | `false` | Do not check the color contrast of short text content |
| `ignorePseudo` | `false` | Do not mark pseudo elements as Needs Review |
| `ignorePseudo` | `false` | Do not mark pseudo elements as Needs Review |
| `boldValue` | `700` | The minimum CSS `font-weight` value that designates bold text |
| `boldTextPt` | `14` | The minimum CSS `font-size` pt value that designates bold text as being large |
| `largeTextPt` | `18` | The minimum CSS `font-size` pt value that designates text as being large |
| `shadowOutlineEmMax` | `0.1` | The maximum `blur-radius` value (in ems) of the CSS `text-shadow` property. `blur-radius` values greater than this value will be treated as a background color rather than an outline color. |
| `pseudoSizeThreshold` | `0.25` | Minimum area of the pseudo element, relative to the text element, below which it will be ignored for colot contrast. |
| `contrastRatio` | N/A | Contrast ratio options |
| &nbsp;&nbsp;`contrastRatio.normal` | N/A | Contrast ratio requirements for normal text (non-bold text or text smaller than `largeTextPt`) |
| &nbsp;&nbsp;&nbsp;&nbsp;`contrastRatio.normal.expected` | `4.5` | The expected contrast ratio for normal text |
Expand Down
143 changes: 83 additions & 60 deletions lib/checks/color/color-contrast-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,7 @@ import {
} from '../../commons/color';
import { memoize } from '../../core/utils';

const hasPseudoElement = memoize(function hasPseudoElement(node, pseudo) {
const style = window.getComputedStyle(node, pseudo);
const backgroundColor = getOwnBackgroundColor(style);

// element has a non-transparent color or image, has
// non-zero width, and is visible
return (
style.getPropertyValue('content') !== 'none' &&
style.getPropertyValue('position') === 'absolute' &&
parseInt(style.getPropertyValue('width')) !== 0 &&
parseInt(style.getPropertyValue('height')) !== 0 &&
style.getPropertyValue('display') !== 'none' &&
style.getPropertyValue('visibility') !== 'hidden' &&
(backgroundColor.alpha !== 0 ||
style.getPropertyValue('background-image') !== 'none')
);
});

function colorContrastEvaluate(node, options, virtualNode) {
if (!isVisible(node, false)) {
return true;
}

export default function colorContrastEvaluate(node, options, virtualNode) {
const {
ignoreUnicode,
ignoreLength,
Expand All @@ -47,25 +25,29 @@ function colorContrastEvaluate(node, options, virtualNode) {
boldTextPt,
largeTextPt,
contrastRatio,
shadowOutlineEmMax
shadowOutlineEmMax,
pseudoSizeThreshold
} = options;

if (!isVisible(node, false)) {
return true;
}

const visibleText = visibleVirtual(virtualNode, false, true);
const textContainsOnlyUnicode =
hasUnicode(visibleText, {
nonBmp: true
}) &&
sanitize(
removeUnicode(visibleText, {
nonBmp: true
})
) === '';

if (textContainsOnlyUnicode && ignoreUnicode) {
if (ignoreUnicode && textIsEmojis(visibleText)) {
this.data({ messageKey: 'nonBmp' });
return undefined;
}

// if element or a parent has pseudo content then we need to mark
// as needs review
const pseudoElm = findPseudoElement(virtualNode, { ignorePseudo, pseudoSizeThreshold })
if (pseudoElm) {
this.data({ messageKey: 'pseudoContent' });
this.relatedNodes(pseudoElm.actualNode);
return undefined;
}

const bgNodes = [];
const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax);
const fgColor = getForegroundColor(node, false, bgColor);
Expand Down Expand Up @@ -105,26 +87,6 @@ function colorContrastEvaluate(node, options, virtualNode) {
: contrastRatio.large;
const isValid = contrast > expected;

// if element or a parent has pseudo content then we need to mark
// as needs review
if (!ignorePseudo) {
let nodeToCheck = node;
while (nodeToCheck) {
if (
hasPseudoElement(nodeToCheck, ':before') ||
hasPseudoElement(nodeToCheck, ':after')
) {
this.data({
messageKey: 'pseudoContent'
});
this.relatedNodes(nodeToCheck);
return undefined;
}

nodeToCheck = nodeToCheck.parentElement;
}
}

// ratio is outside range
if (
(typeof minThreshold === 'number' && contrast < minThreshold) ||
Expand Down Expand Up @@ -155,7 +117,7 @@ function colorContrastEvaluate(node, options, virtualNode) {
}

// need both independently in case both are missing
const data = {
this.data({
fgColor: fgColor ? fgColor.toHexString() : undefined,
bgColor: bgColor ? bgColor.toHexString() : undefined,
contrastRatio: truncatedResult,
Expand All @@ -164,9 +126,7 @@ function colorContrastEvaluate(node, options, virtualNode) {
messageKey: missing,
expectedContrastRatio: expected + ':1',
shadowColor: shadowColor ? shadowColor.toHexString() : undefined
};

this.data(data);
});

// We don't know, so we'll put it into Can't Tell
if (
Expand All @@ -188,4 +148,67 @@ function colorContrastEvaluate(node, options, virtualNode) {
return isValid;
}

export default colorContrastEvaluate;
function findPseudoElement(vNode, {
pseudoSizeThreshold = 0.25,
ignorePseudo = false
}) {
if (ignorePseudo) {
return;
}
const rect = vNode.boundingClientRect;
const minimumSize = rect.width * rect.height * pseudoSizeThreshold;
do {
const beforeSize = getPseudoElementArea(vNode.actualNode, ':before')
const afterSize = getPseudoElementArea(vNode.actualNode, ':after')
if (beforeSize + afterSize > minimumSize) {
return vNode // Combined area of before and after exceeds the minimum size
}
} while (vNode = vNode.parent);
}

const getPseudoElementArea = memoize(function getPseudoElementArea(node, pseudo) {
const style = window.getComputedStyle(node, pseudo);
const matchPseudoStyle = (prop, value) => style.getPropertyValue(prop) === value;
if (
matchPseudoStyle('content', 'none') ||
matchPseudoStyle('display', 'none') ||
matchPseudoStyle('visibility', 'hidden') ||
matchPseudoStyle('position', 'absolute') === false
) {
return 0; // The pseudo element isn't visible
}

if (
getOwnBackgroundColor(style).alpha === 0 &&
matchPseudoStyle('background-image', 'none')
) {
return 0; // There is no background
}

// Find the size of the pseudo element;
const pseudoWidth = parseUnit(style.getPropertyValue('width'));
const pseudoHeight = parseUnit(style.getPropertyValue('height'));
if (pseudoWidth.unit !== 'px' || pseudoHeight.unit !== 'px') {
// IE doesn't normalize to px. Infinity gets everything to undefined
return (pseudoWidth.value === 0 || pseudoHeight.value === 0
? 0 : Infinity
);
}
return pseudoWidth.value * pseudoHeight.value;
});

function textIsEmojis(visibleText) {
const options = { nonBmp: true };
const hasUnicodeChars = hasUnicode(visibleText, options);
const hasNonUnicodeChars = sanitize(removeUnicode(visibleText, options)) === ''
return hasUnicodeChars && hasNonUnicodeChars
}

function parseUnit(str) {
const unitRegex = /^([0-9.]+)([a-z]+)$/i;
const [, value = '', unit = ''] = str.match(unitRegex) || [];
return {
value: parseFloat(value),
unit: unit.toLowerCase()
};
}
1 change: 1 addition & 0 deletions lib/checks/color/color-contrast.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"expected": 3
}
},
"pseudoSizeThreshold": 0.25,
"shadowOutlineEmMax": 0.1
},
"metadata": {
Expand Down
Loading

0 comments on commit e0f6c0c

Please sign in to comment.