diff --git a/doc/issue_impact.md b/doc/issue_impact.md new file mode 100644 index 0000000000..be68f145db --- /dev/null +++ b/doc/issue_impact.md @@ -0,0 +1,21 @@ +# Issue Impacts + +Axe-core assigns an impact according to our assessment of the likely impact of an issue on a user with a disability that would be affected by this issue. In any given context the actual impact for the user could be lower; in some instances, it could be higher. For this reason, we encourage users of tools to evaluate each individual issue and assess the impact in the context of their application or content. + +## Definitions + +### Minor + +Considered to be a nuisance or an annoyance bug. Prioritize fixing if the fix only takes a few minutes and the developer is working on the same screen/feature at the same time, otherwise the issue should not be prioritized. Will still get in the way of compliance if not fixed. + +### Moderate + +Results in some difficulty for people with disabilities, but will generally not prevent them from accessing fundamental features or content. Users may be frustrated and abandon non-critical workflows. Prioritize fixing in this release, if there are no higher-priority issues. Will get in the way of compliance if not fixed. + +### Serious + +Results in serious barriers for people with disabilities, and will partially or fully prevent them from accessing fundamental features or content. People relying on assistive technologies will experience significant frustration and may abandon essential workflows. Issues falling under this category are major problems, and remediation should be a priority. + +### Critical + +Results in blocked content for people with disabilities, and will definitely prevent them from accessing fundamental features or content. This type of issue puts your organization at risk. Prioritize fixing as soon as possible, within the week if possible. Remediation should be a top priority. diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index bfd63193fd..11cd46558a 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -80,37 +80,37 @@ Rules that do not necessarily conform to WCAG success criterion but are industry accepted practices that improve the user experience. -| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | -| :----------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :----------------- | :---------------------------------------------- | :------------------------- | :------------------------------------------------- | -| [accesskeys](https://dequeuniversity.com/rules/axe/4.3/accesskeys?application=RuleDescription) | Ensures every accesskey attribute value is unique | Serious | cat.keyboard, best-practice | failure | | -| [aria-allowed-role](https://dequeuniversity.com/rules/axe/4.3/aria-allowed-role?application=RuleDescription) | Ensures role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | failure, needs review | | -| [aria-dialog-name](https://dequeuniversity.com/rules/axe/4.3/aria-dialog-name?application=RuleDescription) | Ensures every ARIA dialog and alertdialog node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | -| [aria-text](https://dequeuniversity.com/rules/axe/4.3/aria-text?application=RuleDescription) | Ensures "role=text" is used on elements with no focusable descendants | Serious | cat.aria, best-practice | failure, needs review | | -| [aria-treeitem-name](https://dequeuniversity.com/rules/axe/4.3/aria-treeitem-name?application=RuleDescription) | Ensures every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | -| [empty-heading](https://dequeuniversity.com/rules/axe/4.3/empty-heading?application=RuleDescription) | Ensures headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | | -| [frame-tested](https://dequeuniversity.com/rules/axe/4.3/frame-tested?application=RuleDescription) | Ensures <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, review-item, best-practice | failure, needs review | | -| [frame-title-unique](https://dequeuniversity.com/rules/axe/4.3/frame-title-unique?application=RuleDescription) | Ensures <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, best-practice | failure | | -| [heading-order](https://dequeuniversity.com/rules/axe/4.3/heading-order?application=RuleDescription) | Ensures the order of headings is semantically correct | Moderate | cat.semantics, best-practice | failure, needs review | | -| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.3/image-redundant-alt?application=RuleDescription) | Ensure image alternative is not repeated as text | Minor | cat.text-alternatives, best-practice | failure | | -| [label-title-only](https://dequeuniversity.com/rules/axe/4.3/label-title-only?application=RuleDescription) | Ensures that every form element is not solely labeled using the title or aria-describedby attributes | Serious | cat.forms, best-practice | failure | | -| [landmark-banner-is-top-level](https://dequeuniversity.com/rules/axe/4.3/landmark-banner-is-top-level?application=RuleDescription) | Ensures the banner landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-complementary-is-top-level](https://dequeuniversity.com/rules/axe/4.3/landmark-complementary-is-top-level?application=RuleDescription) | Ensures the complementary landmark or aside is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-contentinfo-is-top-level](https://dequeuniversity.com/rules/axe/4.3/landmark-contentinfo-is-top-level?application=RuleDescription) | Ensures the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-main-is-top-level](https://dequeuniversity.com/rules/axe/4.3/landmark-main-is-top-level?application=RuleDescription) | Ensures the main landmark is at top level | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.3/landmark-no-duplicate-banner?application=RuleDescription) | Ensures the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.3/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensures the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.3/landmark-no-duplicate-main?application=RuleDescription) | Ensures the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.3/landmark-one-main?application=RuleDescription) | Ensures the document has a main landmark | Moderate | cat.semantics, best-practice | failure | | -| [landmark-unique](https://dequeuniversity.com/rules/axe/4.3/landmark-unique?application=RuleDescription) | Landmarks should have a unique role or role/label/title (i.e. accessible name) combination | Moderate | cat.semantics, best-practice | failure | | -| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.3/meta-viewport-large?application=RuleDescription) | Ensures <meta name="viewport"> can scale a significant amount | Minor | cat.sensory-and-visual-cues, best-practice | failure | | -| [meta-viewport](https://dequeuniversity.com/rules/axe/4.3/meta-viewport?application=RuleDescription) | Ensures <meta name="viewport"> does not disable text scaling and zooming | Critical | cat.sensory-and-visual-cues, best-practice, ACT | failure | [b4f0c3](https://act-rules.github.io/rules/b4f0c3) | -| [page-has-heading-one](https://dequeuniversity.com/rules/axe/4.3/page-has-heading-one?application=RuleDescription) | Ensure that the page, or at least one of its frames contains a level-one heading | Moderate | cat.semantics, best-practice | failure | | -| [presentation-role-conflict](https://dequeuniversity.com/rules/axe/4.3/presentation-role-conflict?application=RuleDescription) | Flags elements whose role is none or presentation and which cause the role conflict resolution to trigger. | Minor | cat.aria, best-practice | failure | | -| [region](https://dequeuniversity.com/rules/axe/4.3/region?application=RuleDescription) | Ensures all page content is contained by landmarks | Moderate | cat.keyboard, best-practice | failure | | -| [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.3/scope-attr-valid?application=RuleDescription) | Ensures the scope attribute is used correctly on tables | Moderate, Critical | cat.tables, best-practice | failure | | -| [skip-link](https://dequeuniversity.com/rules/axe/4.3/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice | failure, needs review | | -| [tabindex](https://dequeuniversity.com/rules/axe/4.3/tabindex?application=RuleDescription) | Ensures tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | | -| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.3/table-duplicate-name?application=RuleDescription) | Ensure that tables do not have the same summary and caption | Minor | cat.tables, best-practice | failure | | +| Rule ID | Description | Impact | Tags | Issue Type | ACT Rules | +| :----------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | :----------------- | :---------------------------------------------- | :------------------------- | :------------------------------------------------- | +| [accesskeys](https://dequeuniversity.com/rules/axe/4.3/accesskeys?application=RuleDescription) | Ensures every accesskey attribute value is unique | Serious | cat.keyboard, best-practice | failure | | +| [aria-allowed-role](https://dequeuniversity.com/rules/axe/4.3/aria-allowed-role?application=RuleDescription) | Ensures role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | failure, needs review | | +| [aria-dialog-name](https://dequeuniversity.com/rules/axe/4.3/aria-dialog-name?application=RuleDescription) | Ensures every ARIA dialog and alertdialog node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | +| [aria-text](https://dequeuniversity.com/rules/axe/4.3/aria-text?application=RuleDescription) | Ensures "role=text" is used on elements with no focusable descendants | Serious | cat.aria, best-practice | failure, needs review | | +| [aria-treeitem-name](https://dequeuniversity.com/rules/axe/4.3/aria-treeitem-name?application=RuleDescription) | Ensures every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | +| [empty-heading](https://dequeuniversity.com/rules/axe/4.3/empty-heading?application=RuleDescription) | Ensures headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | | +| [frame-tested](https://dequeuniversity.com/rules/axe/4.3/frame-tested?application=RuleDescription) | Ensures <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, review-item, best-practice | failure, needs review | | +| [frame-title-unique](https://dequeuniversity.com/rules/axe/4.3/frame-title-unique?application=RuleDescription) | Ensures <iframe> and <frame> elements contain a unique title attribute | Serious | cat.text-alternatives, best-practice | failure | | +| [heading-order](https://dequeuniversity.com/rules/axe/4.3/heading-order?application=RuleDescription) | Ensures the order of headings is semantically correct | Moderate | cat.semantics, best-practice | failure, needs review | | +| [image-redundant-alt](https://dequeuniversity.com/rules/axe/4.3/image-redundant-alt?application=RuleDescription) | Ensure image alternative is not repeated as text | Minor | cat.text-alternatives, best-practice | failure | | +| [label-title-only](https://dequeuniversity.com/rules/axe/4.3/label-title-only?application=RuleDescription) | Ensures that every form element has a visible label and is not solely labeled using hidden labels, or the title or aria-describedby attributes | Serious | cat.forms, best-practice | failure | | +| [landmark-banner-is-top-level](https://dequeuniversity.com/rules/axe/4.3/landmark-banner-is-top-level?application=RuleDescription) | Ensures the banner landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-complementary-is-top-level](https://dequeuniversity.com/rules/axe/4.3/landmark-complementary-is-top-level?application=RuleDescription) | Ensures the complementary landmark or aside is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-contentinfo-is-top-level](https://dequeuniversity.com/rules/axe/4.3/landmark-contentinfo-is-top-level?application=RuleDescription) | Ensures the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-main-is-top-level](https://dequeuniversity.com/rules/axe/4.3/landmark-main-is-top-level?application=RuleDescription) | Ensures the main landmark is at top level | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-banner](https://dequeuniversity.com/rules/axe/4.3/landmark-no-duplicate-banner?application=RuleDescription) | Ensures the document has at most one banner landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-contentinfo](https://dequeuniversity.com/rules/axe/4.3/landmark-no-duplicate-contentinfo?application=RuleDescription) | Ensures the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-no-duplicate-main](https://dequeuniversity.com/rules/axe/4.3/landmark-no-duplicate-main?application=RuleDescription) | Ensures the document has at most one main landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-one-main](https://dequeuniversity.com/rules/axe/4.3/landmark-one-main?application=RuleDescription) | Ensures the document has a main landmark | Moderate | cat.semantics, best-practice | failure | | +| [landmark-unique](https://dequeuniversity.com/rules/axe/4.3/landmark-unique?application=RuleDescription) | Landmarks should have a unique role or role/label/title (i.e. accessible name) combination | Moderate | cat.semantics, best-practice | failure | | +| [meta-viewport-large](https://dequeuniversity.com/rules/axe/4.3/meta-viewport-large?application=RuleDescription) | Ensures <meta name="viewport"> can scale a significant amount | Minor | cat.sensory-and-visual-cues, best-practice | failure | | +| [meta-viewport](https://dequeuniversity.com/rules/axe/4.3/meta-viewport?application=RuleDescription) | Ensures <meta name="viewport"> does not disable text scaling and zooming | Critical | cat.sensory-and-visual-cues, best-practice, ACT | failure | [b4f0c3](https://act-rules.github.io/rules/b4f0c3) | +| [page-has-heading-one](https://dequeuniversity.com/rules/axe/4.3/page-has-heading-one?application=RuleDescription) | Ensure that the page, or at least one of its frames contains a level-one heading | Moderate | cat.semantics, best-practice | failure | | +| [presentation-role-conflict](https://dequeuniversity.com/rules/axe/4.3/presentation-role-conflict?application=RuleDescription) | Flags elements whose role is none or presentation and which cause the role conflict resolution to trigger. | Minor | cat.aria, best-practice | failure | | +| [region](https://dequeuniversity.com/rules/axe/4.3/region?application=RuleDescription) | Ensures all page content is contained by landmarks | Moderate | cat.keyboard, best-practice | failure | | +| [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.3/scope-attr-valid?application=RuleDescription) | Ensures the scope attribute is used correctly on tables | Moderate, Critical | cat.tables, best-practice | failure | | +| [skip-link](https://dequeuniversity.com/rules/axe/4.3/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice | failure, needs review | | +| [tabindex](https://dequeuniversity.com/rules/axe/4.3/tabindex?application=RuleDescription) | Ensures tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | | +| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.3/table-duplicate-name?application=RuleDescription) | Ensure that tables do not have the same summary and caption | Minor | cat.tables, best-practice | failure | | ## WCAG 2.0 and 2.1 level AAA rules diff --git a/lib/checks/color/color-contrast-evaluate.js b/lib/checks/color/color-contrast-evaluate.js index 18a58f4e4b..8aa165d37d 100644 --- a/lib/checks/color/color-contrast-evaluate.js +++ b/lib/checks/color/color-contrast-evaluate.js @@ -12,7 +12,7 @@ import { getContrast, getOwnBackgroundColor, getTextShadowColors, - flattenColors + flattenShadowColors } from '../../commons/color'; import { memoize } from '../../core/utils'; @@ -73,7 +73,7 @@ export default function colorContrastEvaluate(node, options, virtualNode) { } else if (fgColor && bgColor) { // Thin shadows can pass either by contrasting with the text color // or when contrasting with the background. - shadowColor = [...shadowColors, bgColor].reduce(flattenColors); + shadowColor = [...shadowColors, bgColor].reduce(flattenShadowColors); const bgContrast = getContrast(bgColor, shadowColor); const fgContrast = getContrast(shadowColor, fgColor); contrast = Math.max(bgContrast, fgContrast); diff --git a/lib/commons/aria/arialabelledby-text.js b/lib/commons/aria/arialabelledby-text.js index e74c77370a..f1a7fa8745 100644 --- a/lib/commons/aria/arialabelledby-text.js +++ b/lib/commons/aria/arialabelledby-text.js @@ -13,7 +13,7 @@ import { getNodeFromTree } from '../../core/utils'; * @property {Bool} inControlContext Whether or not the lookup is part of a native label reference * @property {Element} startNode First node in accessible name computation * @property {Bool} debug Enable logging for formControlValue - * @return {string} Cancatinated text value for referenced elements + * @return {string} Concatenated text value for referenced elements */ function arialabelledbyText(vNode, context = {}) { if (!(vNode instanceof AbstractVirtualNode)) { diff --git a/lib/commons/aria/label-virtual.js b/lib/commons/aria/label-virtual.js index 04fabf688d..edd5b94480 100644 --- a/lib/commons/aria/label-virtual.js +++ b/lib/commons/aria/label-virtual.js @@ -21,7 +21,7 @@ function labelVirtual(virtualNode) { candidate = ref .map(thing => { const vNode = getNodeFromTree(thing); - return vNode ? visibleVirtual(vNode, true) : ''; + return vNode ? visibleVirtual(vNode) : ''; }) .join(' ') .trim(); diff --git a/lib/commons/color/flatten-colors.js b/lib/commons/color/flatten-colors.js index e4dc587fa6..2c3de0e033 100644 --- a/lib/commons/color/flatten-colors.js +++ b/lib/commons/color/flatten-colors.js @@ -1,5 +1,33 @@ import Color from './color'; +// how to combine background and foreground colors together when using +// the CSS property `mix-blend-mode`. Defaults to `normal` +// @see https://www.w3.org/TR/compositing-1/#blendingseparable +const blendFunctions = { + normal(Cb, Cs) { + return Cs; + } +}; + +// Simple Alpha Compositing written as non-premultiplied. +// formula: Rrgb × Ra = Srgb × Sa + Drgb × Da × (1 − Sa) +// Cs: the source color +// αs: the source alpha +// Cb: the backdrop color +// αb: the backdrop alpha +// @see https://www.w3.org/TR/compositing-1/#simplealphacompositing +// @see https://www.w3.org/TR/compositing-1/#blending +// @see https://ciechanow.ski/alpha-compositing/ +function simpleAlphaCompositing(Cs, αs, Cb, αb, blendMode) { + // RGB color space doesn't have decimal values so we will follow what browsers do and round + // e.g. rgb(255.2, 127.5, 127.8) === rgb(255, 128, 128) + return Math.round( + αs * (1 - αb) * Cs + + αs * αb * blendFunctions[blendMode](Cb, Cs) + + (1 - αs) * αb * Cb + ); +} + /** * Combine the two given color according to alpha blending. * @method flattenColors @@ -9,12 +37,36 @@ import Color from './color'; * @param {Color} bgColor Background color * @return {Color} Blended color */ -function flattenColors(fgColor, bgColor) { - var alpha = fgColor.alpha; - var r = (1 - alpha) * bgColor.red + alpha * fgColor.red; - var g = (1 - alpha) * bgColor.green + alpha * fgColor.green; - var b = (1 - alpha) * bgColor.blue + alpha * fgColor.blue; - var a = fgColor.alpha + bgColor.alpha * (1 - fgColor.alpha); +function flattenColors(fgColor, bgColor, blendMode = 'normal') { + // foreground is the "source" color and background is the "backdrop" color + const r = simpleAlphaCompositing( + fgColor.red, + fgColor.alpha, + bgColor.red, + bgColor.alpha, + blendMode + ); + const g = simpleAlphaCompositing( + fgColor.green, + fgColor.alpha, + bgColor.green, + bgColor.alpha, + blendMode + ); + const b = simpleAlphaCompositing( + fgColor.blue, + fgColor.alpha, + bgColor.blue, + bgColor.alpha, + blendMode + ); + + // formula: αo = αs + αb x (1 - αs) + // clamp alpha between 0 and 1 + const a = Math.max( + 0, + Math.min(fgColor.alpha + bgColor.alpha * (1 - fgColor.alpha), 1) + ); return new Color(r, g, b, a); } diff --git a/lib/commons/color/flatten-shadow-colors.js b/lib/commons/color/flatten-shadow-colors.js new file mode 100644 index 0000000000..404aae41ac --- /dev/null +++ b/lib/commons/color/flatten-shadow-colors.js @@ -0,0 +1,22 @@ +import Color from './color'; + +/** + * Combine the two given shadow colors according to alpha blending. + * @method flattenColors + * @memberof axe.commons.color.Color + * @instance + * @param {Color} fgColor Foreground color + * @param {Color} bgColor Background color + * @return {Color} Blended color + */ +function flattenColors(fgColor, bgColor) { + var alpha = fgColor.alpha; + var r = (1 - alpha) * bgColor.red + alpha * fgColor.red; + var g = (1 - alpha) * bgColor.green + alpha * fgColor.green; + var b = (1 - alpha) * bgColor.blue + alpha * fgColor.blue; + var a = fgColor.alpha + bgColor.alpha * (1 - fgColor.alpha); + + return new Color(r, g, b, a); +} + +export default flattenColors; diff --git a/lib/commons/color/get-background-color.js b/lib/commons/color/get-background-color.js index f281b7ebba..230fa9d568 100644 --- a/lib/commons/color/get-background-color.js +++ b/lib/commons/color/get-background-color.js @@ -4,6 +4,7 @@ import getOwnBackgroundColor from './get-own-background-color'; import elementHasImage from './element-has-image'; import Color from './color'; import flattenColors from './flatten-colors'; +import flattenShadowColors from './flatten-shadow-colors'; import getTextShadowColors from './get-text-shadow-colors'; import visuallyContains from '../dom/visually-contains'; @@ -38,6 +39,9 @@ function elmPartiallyObscured(elm, bgElm, bgColor) { */ function getBackgroundColor(elm, bgElms = [], shadowOutlineEmMax = 0.1) { let bgColors = getTextShadowColors(elm, { minRatio: shadowOutlineEmMax }); + if (bgColors.length) { + bgColors = [bgColors.reduce(flattenShadowColors)]; + } const elmStack = getBackgroundStack(elm); // Search the stack until we have an alpha === 1 background @@ -62,7 +66,7 @@ function getBackgroundColor(elm, bgElms = [], shadowOutlineEmMax = 0.1) { if (bgColor.alpha !== 0) { // store elements contributing to the br color. bgElms.push(bgElm); - bgColors.push(bgColor); + bgColors.unshift(bgColor); // Exit if the background is opaque return bgColor.alpha === 1; @@ -75,9 +79,14 @@ function getBackgroundColor(elm, bgElms = [], shadowOutlineEmMax = 0.1) { return null; } - // Mix the colors together, on top of a default white - bgColors.push(new Color(255, 255, 255, 1)); - var colors = bgColors.reduce(flattenColors); + // Mix the colors together, on top of a default white. Colors must be mixed + // in bottom up order (background to foreground order) to produce the correct + // result. + // @see https://github.com/dequelabs/axe-core/issues/2924 + bgColors.unshift(new Color(255, 255, 255, 1)); + var colors = bgColors.reduce((bgColor, fgColor) => { + return flattenColors(fgColor, bgColor); + }); return colors; } diff --git a/lib/commons/color/get-foreground-color.js b/lib/commons/color/get-foreground-color.js index 594d31cd15..d9b8139af3 100644 --- a/lib/commons/color/get-foreground-color.js +++ b/lib/commons/color/get-foreground-color.js @@ -2,6 +2,7 @@ import Color from './color'; import getBackgroundColor from './get-background-color'; import incompleteData from './incomplete-data'; import flattenColors from './flatten-colors'; +import flattenShadowColors from './flatten-shadow-colors'; import getTextShadowColors from './get-text-shadow-colors'; import { getNodeFromTree } from '../../core/utils'; @@ -66,7 +67,7 @@ function getForegroundColor(node, _, bgColor) { if (fgColor.alpha < 1) { const textShadowColors = getTextShadowColors(node, { minRatio: 0 }); - return [fgColor, ...textShadowColors, bgColor].reduce(flattenColors); + return [fgColor, ...textShadowColors, bgColor].reduce(flattenShadowColors); } return flattenColors(fgColor, bgColor); diff --git a/lib/commons/color/index.js b/lib/commons/color/index.js index c795340b85..156cb3eded 100644 --- a/lib/commons/color/index.js +++ b/lib/commons/color/index.js @@ -9,6 +9,7 @@ export { default as elementHasImage } from './element-has-image'; export { default as elementIsDistinct } from './element-is-distinct'; export { default as filteredRectStack } from './filtered-rect-stack'; export { default as flattenColors } from './flatten-colors'; +export { default as flattenShadowColors } from './flatten-shadow-colors'; export { default as getBackgroundColor } from './get-background-color'; export { default as getBackgroundStack } from './get-background-stack'; export { default as getContrast } from './get-contrast'; diff --git a/lib/commons/dom/get-rect-stack.js b/lib/commons/dom/get-rect-stack.js index 06172262f4..2f9c5183e3 100644 --- a/lib/commons/dom/get-rect-stack.js +++ b/lib/commons/dom/get-rect-stack.js @@ -360,6 +360,8 @@ function addNodeToGrid(grid, vNode) { const endRow = ((y + rect.height) / gridSize) | 0; const endCol = ((x + rect.width) / gridSize) | 0; + grid.numCols = Math.max(grid.numCols ?? 0, endCol); + for (let row = startRow; row <= endRow; row++) { grid.cells[row] = grid.cells[row] || []; @@ -475,21 +477,34 @@ export function getRectStack(grid, rect, recursed = false) { // went with pixel perfect collision rather than rounding const row = (y / gridSize) | 0; const col = (x / gridSize) | 0; - let stack = grid.cells[row][col].filter(gridCellNode => { - return gridCellNode.clientRects.find(clientRect => { - const rectX = clientRect.left; - const rectY = clientRect.top; - - // perform an AABB (axis-aligned bounding box) collision check for the - // point inside the rect - return ( - x <= rectX + clientRect.width && - x >= rectX && - y <= rectY + clientRect.height && - y >= rectY - ); - }); - }); + + // we're making an assumption that there cannot be an element in the + // grid which escapes the grid bounds. For example, if the grid is 4x4 there + // can't be an element whose midpoint is at column 5. If this happens this + // means there's an error in our grid logic that needs to be fixed + if (row > grid.cells.length || col > grid.numCols) { + throw new Error('Element midpoint exceeds the grid bounds'); + } + + // it is acceptable if a row has empty cells due to client rects not filling + // the entire bounding rect of an element + // @see https://github.com/dequelabs/axe-core/issues/3166 + let stack = + grid.cells[row][col]?.filter(gridCellNode => { + return gridCellNode.clientRects.find(clientRect => { + const rectX = clientRect.left; + const rectY = clientRect.top; + + // perform an AABB (axis-aligned bounding box) collision check for the + // point inside the rect + return ( + x <= rectX + clientRect.width && + x >= rectX && + y <= rectY + clientRect.height && + y >= rectY + ); + }); + }) ?? []; const gridContainer = grid.container; if (gridContainer) { diff --git a/lib/rules/label-title-only.json b/lib/rules/label-title-only.json index 5ac4a78549..79813f2183 100644 --- a/lib/rules/label-title-only.json +++ b/lib/rules/label-title-only.json @@ -4,7 +4,7 @@ "matches": "label-matches", "tags": ["cat.forms", "best-practice"], "metadata": { - "description": "Ensures that every form element is not solely labeled using the title or aria-describedby attributes", + "description": "Ensures that every form element has a visible label and is not solely labeled using hidden labels, or the title or aria-describedby attributes", "help": "Form elements should have a visible label" }, "all": [], diff --git a/lib/standards/aria-roles.js b/lib/standards/aria-roles.js index a21fe4a2b1..f1291d37f0 100644 --- a/lib/standards/aria-roles.js +++ b/lib/standards/aria-roles.js @@ -284,7 +284,7 @@ const ariaRoles = { }, listbox: { type: 'composite', - requiredOwned: ['option'], + requiredOwned: ['group', 'option'], allowedAttrs: [ 'aria-multiselectable', 'aria-readonly', @@ -418,7 +418,7 @@ const ariaRoles = { }, option: { type: 'widget', - requiredContext: ['listbox'], + requiredContext: ['group', 'listbox'], // Note: since the option role has an implicit // aria-selected value it is not required to be added by // the user diff --git a/lib/standards/html-elms.js b/lib/standards/html-elms.js index 3b854e1b71..472529b5fe 100644 --- a/lib/standards/html-elms.js +++ b/lib/standards/html-elms.js @@ -936,7 +936,7 @@ const htmlElms = { }, wbr: { contentTypes: ['phrasing', 'flow'], - allowedRoles: true + allowedRoles: ['presentation', 'none'] } }; diff --git a/test/checks/aria/required-children.js b/test/checks/aria/required-children.js index 38e1ccaae9..c5fba9de19 100644 --- a/test/checks/aria/required-children.js +++ b/test/checks/aria/required-children.js @@ -323,7 +323,7 @@ describe('aria-required-children', function() { it('should fail when role does not allow group', function() { var params = checkSetup( - '
' +
+ '\nx x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x ' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\nx' +
+ '\n
'
+ );
+ assert.doesNotThrow(function() {
+ contrastEvaluate.apply(checkContext, params);
+ });
+ });
+
+ describe('with pseudo elements', function() {
it('should return undefined if :before pseudo element has a background color', function() {
var params = checkSetup(
'' +
'Content
Content
Content
Content
Content
Content
Content
Content
Content
Content
' - ); - assert.isUndefined(contrastEvaluate.apply(checkContext, params)); - }); + (isIE11 ? it : xit)( + 'should return undefined if the unit is not in px', + function() { + var params = checkSetup( + '' + + 'Content
' + ); + assert.isUndefined(contrastEvaluate.apply(checkContext, params)); + } + ); }); - describe('with special texts', function () { + describe('with special texts', function() { it('should return undefined for a single character text with insufficient contrast', function() { var params = checkSetup( 'Content
', { - pseudoSizeThreshold: 0.20 + pseudoSizeThreshold: 0.2 } ); assert.isUndefined(contrastEvaluate.apply(checkContext, params)); }); }); - describe('with shadowDOM', function () { + describe('with shadowDOM', function() { (shadowSupported ? it : xit)( 'returns colors across Shadow DOM boundaries', function() { diff --git a/test/commons/color/flatten-colors.js b/test/commons/color/flatten-colors.js index c9244e40ca..bceb351f52 100644 --- a/test/commons/color/flatten-colors.js +++ b/test/commons/color/flatten-colors.js @@ -6,7 +6,10 @@ describe('color.flattenColors', function() { var fullblack = new axe.commons.color.Color(0, 0, 0, 1); var transparent = new axe.commons.color.Color(0, 0, 0, 0); var white = new axe.commons.color.Color(255, 255, 255, 1); - var gray = new axe.commons.color.Color(127.5, 127.5, 127.5, 1); + var gray = new axe.commons.color.Color(128, 128, 128, 1); + var halfRed = new axe.commons.color.Color(255, 0, 0, 0.5); + var quarterLightGreen = new axe.commons.color.Color(0, 128, 0, 0.25); + var flat = axe.commons.color.flattenColors(halfblack, white); assert.equal(flat.red, gray.red); assert.equal(flat.green, gray.green); @@ -21,5 +24,23 @@ describe('color.flattenColors', function() { assert.equal(flat3.red, white.red); assert.equal(flat3.green, white.green); assert.equal(flat3.blue, white.blue); + + var flat4 = axe.commons.color.flattenColors(halfRed, white); + assert.equal(flat4.red, 255); + assert.equal(flat4.green, 128); + assert.equal(flat4.blue, 128); + assert.equal(flat4.alpha, 1); + + var flat5 = axe.commons.color.flattenColors(quarterLightGreen, white); + assert.equal(flat5.red, 191); + assert.equal(flat5.green, 223); + assert.equal(flat5.blue, 191); + assert.equal(flat5.alpha, 1); + + var flat6 = axe.commons.color.flattenColors(quarterLightGreen, halfRed); + assert.equal(flat6.red, 96); + assert.equal(flat6.green, 32); + assert.equal(flat6.blue, 0); + assert.equal(flat6.alpha, 0.625); }); }); diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index ed31136946..290213430e 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -348,6 +348,46 @@ describe('dom.getElementStack', function() { assert.deepEqual(stack, []); }); + it('should throw error if element midpoint-x exceeds the grid', function() { + fixture.innerHTML = '+ Use this page to verify that axe-core produces the correct colors for each + blended background. If using Chrome, please ensure it is using the + sRGB color profile by navigating to chrome://flags/, searching for "Force + color profile" and setting it to "sRGB" (otherwise it uses the OS color + profile which for Mac, which we believe is "Display P3 D65" and will + produce the incorrect result color when blending). +
++ For more information, see + https://github.com/dequelabs/axe-core/issues/2924 +
+