From 699697bc237b6c69050e4572ba5cfdc5f338f450 Mon Sep 17 00:00:00 2001
From: Steven Lambert <2433219+straker@users.noreply.github.com>
Date: Wed, 6 Oct 2021 10:15:03 -0600
Subject: [PATCH 1/6] fix(color-contrast): account for elements that do not
fill entire bounding size (#3186)
* fix(color-contrast): account for elements that do not fill entire bounding size
* not private
---
lib/commons/dom/get-rect-stack.js | 45 ++++++----
test/checks/color/color-contrast.js | 121 ++++++++++++++++++--------
test/commons/dom/get-element-stack.js | 40 +++++++++
3 files changed, 153 insertions(+), 53 deletions(-)
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/test/checks/color/color-contrast.js b/test/checks/color/color-contrast.js
index f2f322eb4a..339e5d1a3b 100644
--- a/test/checks/color/color-contrast.js
+++ b/test/checks/color/color-contrast.js
@@ -358,13 +358,55 @@ describe('color-contrast', function() {
);
});
- describe('with pseudo elements', function () {
+ it('should not error if client rects do not fill entire bounding rect', 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
'
);
-
+
assert.isUndefined(contrastEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, {
messageKey: 'pseudoContent'
@@ -374,13 +416,13 @@ describe('color-contrast', function() {
document.querySelector('#background')
);
});
-
+
it('should return undefined if :after pseudo element has a background color', function() {
var params = checkSetup(
'' +
'
Content
'
);
-
+
assert.isUndefined(contrastEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, {
messageKey: 'pseudoContent'
@@ -390,21 +432,21 @@ describe('color-contrast', function() {
document.querySelector('#background')
);
});
-
+
it('should return undefined if pseudo element has a background image', function() {
var dataURI =
'data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/' +
'XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkA' +
'ABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKU' +
'E1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7';
-
+
var params = checkSetup(
'' +
'
Content
'
);
-
+
assert.isUndefined(contrastEvaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, {
messageKey: 'pseudoContent'
@@ -414,62 +456,62 @@ describe('color-contrast', function() {
document.querySelector('#background')
);
});
-
+
it('should not return undefined if pseudo element has no content', function() {
var params = checkSetup(
'' +
'
Content
'
);
-
+
assert.isTrue(contrastEvaluate.apply(checkContext, params));
});
-
+
it('should not return undefined if pseudo element is not absolutely positioned no content', function() {
var params = checkSetup(
'' +
'
Content
'
);
-
+
assert.isTrue(contrastEvaluate.apply(checkContext, params));
});
-
+
it('should not return undefined if pseudo element is has zero dimension', function() {
var params = checkSetup(
'' +
'
Content
'
);
-
+
assert.isTrue(contrastEvaluate.apply(checkContext, params));
});
-
+
it("should not return undefined if pseudo element doesn't have a background", function() {
var params = checkSetup(
'' +
'
Content
'
);
-
+
assert.isTrue(contrastEvaluate.apply(checkContext, params));
});
-
+
it('should not return undefined if pseudo element has visibility: hidden', function() {
var params = checkSetup(
'' +
'
Content
'
);
-
+
assert.isTrue(contrastEvaluate.apply(checkContext, params));
});
-
+
it('should not return undefined if pseudo element has display: none', function() {
var params = checkSetup(
'' +
'
Content
'
);
-
+
assert.isTrue(contrastEvaluate.apply(checkContext, params));
});
- it('should return undefined if pseudo element is more than 25% of the element', function () {
+ it('should return undefined if pseudo element is more than 25% of the element', function() {
var params = checkSetup(
'' +
@@ -478,7 +520,7 @@ describe('color-contrast', function() {
assert.isUndefined(contrastEvaluate.apply(checkContext, params));
});
- it('should not return undefined if pseudo element is 25% of the element', function () {
+ it('should not return undefined if pseudo element is 25% of the element', function() {
var params = checkSetup(
'' +
@@ -487,17 +529,20 @@ describe('color-contrast', function() {
assert.isTrue(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));
- });
+ (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(
'
' +
@@ -582,7 +627,7 @@ describe('color-contrast', function() {
});
});
- describe('options', function () {
+ describe('options', function() {
it('should support options.boldValue', function() {
var params = checkSetup(
'
' +
@@ -732,24 +777,24 @@ describe('color-contrast', function() {
ignorePseudo: true
}
);
-
+
assert.isTrue(contrastEvaluate.apply(checkContext, params));
});
-
- it('should adjust the pseudo element minimum size with the options.pseudoSizeThreshold', function () {
+
+ it('should adjust the pseudo element minimum size with the options.pseudoSizeThreshold', 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/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 = '
';
+ axe.testUtils.flatTreeSetup(fixture);
+ var target = fixture.querySelector('#target');
+ var vNode = axe.utils.getNodeFromTree(target);
+ Object.defineProperty(vNode, 'boundingClientRect', {
+ get: function() {
+ return {
+ left: 0,
+ top: 10,
+ width: 10,
+ height: 10000
+ };
+ }
+ });
+ assert.throws(function() {
+ getElementStack(target);
+ }, 'Element midpoint exceeds the grid bounds');
+ });
+
// IE11 either only supports clip paths defined by url() or not at all,
// MDN and caniuse.com give different results...
(isIE11 ? it.skip : it)(
From 2bdd9538c3d71280a3d51fb02f44142007b41569 Mon Sep 17 00:00:00 2001
From: Dylan Barrell
Date: Thu, 7 Oct 2021 04:10:05 -0400
Subject: [PATCH 2/6] docs: create issue impact documentation (#3164)
* feat: create issue impact documentation
* Update doc/issue_impact.md
Co-authored-by: Wilco Fiers
* Update issue_impact.md
* Update doc/issue_impact.md
Co-authored-by: Stephen Mathieson
Co-authored-by: Wilco Fiers
Co-authored-by: Stephen Mathieson
---
doc/issue_impact.md | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
create mode 100644 doc/issue_impact.md
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.
From cb019755d9cb52b997aae340f406ac26d0cf90e5 Mon Sep 17 00:00:00 2001
From: Steven Lambert <2433219+straker@users.noreply.github.com>
Date: Fri, 8 Oct 2021 09:33:11 -0600
Subject: [PATCH 3/6] fix(aria-allowed-children,aria-allowed-parent): allow
group role in listbox (#3195)
* fix(aria-allowed-children,aria-allowed-parent): allow group role in listbox
* fix tests
---
lib/standards/aria-roles.js | 4 ++--
test/checks/aria/required-children.js | 2 +-
.../aria-required-children/aria-required-children.html | 6 ++++++
.../aria-required-children/aria-required-children.json | 3 ++-
.../rules/aria-required-parent/aria-required-parent.html | 6 ++++++
.../rules/aria-required-parent/aria-required-parent.json | 3 ++-
6 files changed, 19 insertions(+), 5 deletions(-)
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/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(
- '
diff --git a/test/integration/rules/aria-required-parent/aria-required-parent.json b/test/integration/rules/aria-required-parent/aria-required-parent.json
index df291caf49..0092d17059 100644
--- a/test/integration/rules/aria-required-parent/aria-required-parent.json
+++ b/test/integration/rules/aria-required-parent/aria-required-parent.json
@@ -22,6 +22,7 @@
["#pass9"],
["#pass10"],
["#pass11"],
- ["#pass12"]
+ ["#pass12"],
+ ["#pass13"]
]
}
From e930a7081d4308549370f74e9d341badd9661584 Mon Sep 17 00:00:00 2001
From: Steven Lambert <2433219+straker@users.noreply.github.com>
Date: Fri, 8 Oct 2021 09:33:54 -0600
Subject: [PATCH 4/6] fix(color-contrast): properly blend multiple alpha colors
(#3193)
* fix(color-contrast): properly blend multiple alpha colors
* revert shadow
* add back shadow color flatten
* fix
* fix ie11
* type
* typo
---
lib/checks/color/color-contrast-evaluate.js | 4 +-
lib/commons/color/flatten-colors.js | 64 ++++++-
lib/commons/color/flatten-shadow-colors.js | 22 +++
lib/commons/color/get-background-color.js | 17 +-
lib/commons/color/get-foreground-color.js | 3 +-
lib/commons/color/index.js | 1 +
test/commons/color/flatten-colors.js | 23 ++-
test/integration/full/contrast/blending.html | 189 +++++++++++++++++++
test/integration/full/contrast/blending.js | 56 ++++++
9 files changed, 365 insertions(+), 14 deletions(-)
create mode 100644 lib/commons/color/flatten-shadow-colors.js
create mode 100644 test/integration/full/contrast/blending.html
create mode 100644 test/integration/full/contrast/blending.js
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/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/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/integration/full/contrast/blending.html b/test/integration/full/contrast/blending.html
new file mode 100644
index 0000000000..2fbc1ad89e
--- /dev/null
+++ b/test/integration/full/contrast/blending.html
@@ -0,0 +1,189 @@
+
+
+
+ Color Contrast Blending Verification Tests
+
+
+
+
+
+
+
+
+
+
+ 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).
+
+
+
+
+
+
+
diff --git a/test/integration/full/contrast/blending.js b/test/integration/full/contrast/blending.js
new file mode 100644
index 0000000000..7d1bde4859
--- /dev/null
+++ b/test/integration/full/contrast/blending.js
@@ -0,0 +1,56 @@
+describe('color-contrast blending test', function() {
+ var include = [];
+ var resultElms = [];
+ var expected = [
+ 'rgb(223, 112, 96)',
+ 'rgb(255, 128, 128)',
+ 'rgb(191, 223, 191)',
+ 'rgb(125, 38, 54)',
+ 'rgb(179, 38, 0)',
+ 'rgb(179, 0, 77)',
+ 'rgb(143, 192, 80)',
+ 'rgb(147, 153, 119)',
+ 'rgb(221, 221, 221)'
+ ];
+ var testElms = Array.from(document.querySelectorAll('#fixture > div'));
+ testElms.forEach(function(testElm) {
+ var id = testElm.id;
+ var target = testElm.querySelector('#' + id + '-target');
+ var result = testElm.querySelector('#' + id + '-result');
+ include.push(target);
+ resultElms.push(result);
+ });
+
+ before(function(done) {
+ axe.run({ include: include }, { runOnly: ['color-contrast'] }, function(
+ err,
+ res
+ ) {
+ assert.isNull(err);
+
+ // don't care where the result goes as we just want to
+ // extract the background color for each one
+ var results = []
+ .concat(res.passes)
+ .concat(res.violations)
+ .concat(res.incomplete);
+ results.forEach(function(result) {
+ result.nodes.forEach(function(node) {
+ var bgColor = node.any[0].data.bgColor;
+ var id = node.target[0].split('-')[0];
+ var result = document.querySelector(id + '-result');
+ result.style.backgroundColor = bgColor;
+ });
+ });
+
+ done();
+ });
+ });
+
+ resultElms.forEach(function(elm, index) {
+ it('produces the correct blended color for ' + elm.id, function() {
+ var style = window.getComputedStyle(elm);
+ assert.equal(style.getPropertyValue('background-color'), expected[index]);
+ });
+ });
+});
From 2f439b3fdb7e7fa3228e663c5313af0f08aa4327 Mon Sep 17 00:00:00 2001
From: Scott O'Hara
Date: Fri, 8 Oct 2021 11:42:03 -0400
Subject: [PATCH 5/6] fix(aria-allowed-role): updates the allowed roles for the
wbr element to none and presentation (#3192)
related to issue #3177
---
lib/standards/html-elms.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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']
}
};
From cad39949c29bc3f83863e3484feef82e89e12118 Mon Sep 17 00:00:00 2001
From: Esteban Gehring
Date: Fri, 8 Oct 2021 19:48:35 +0200
Subject: [PATCH 6/6] fix(label-title-only): allow hidden labels (#3183)
---
doc/rule-descriptions.md | 64 +++++++++----------
lib/commons/aria/arialabelledby-text.js | 2 +-
lib/commons/aria/label-virtual.js | 2 +-
lib/rules/label-title-only.json | 2 +-
.../label-title-only/label-title-only.html | 11 ++++
.../label-title-only/label-title-only.json | 5 +-
test/integration/rules/label/label.html | 3 +
test/integration/rules/label/label.json | 3 +-
8 files changed, 55 insertions(+), 37 deletions(-)
diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md
index e145658a76..089fe89db5 100644
--- a/doc/rule-descriptions.md
+++ b/doc/rule-descriptions.md
@@ -79,38 +79,38 @@
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 | |
-| [identical-links-same-purpose](https://dequeuniversity.com/rules/axe/4.3/identical-links-same-purpose?application=RuleDescription) | Ensure that links with the same accessible name serve a similar purpose | Minor | cat.semantics, wcag2aaa, wcag249, best-practice | needs review | [b20e66](https://act-rules.github.io/rules/b20e66), [fd3a94](https://act-rules.github.io/rules/fd3a94) |
-| [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 | |
+| [identical-links-same-purpose](https://dequeuniversity.com/rules/axe/4.3/identical-links-same-purpose?application=RuleDescription) | Ensure that links with the same accessible name serve a similar purpose | Minor | cat.semantics, wcag2aaa, wcag249, best-practice | needs review | [b20e66](https://act-rules.github.io/rules/b20e66), [fd3a94](https://act-rules.github.io/rules/fd3a94) |
+| [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 | |
## Experimental Rules
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/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/test/integration/rules/label-title-only/label-title-only.html b/test/integration/rules/label-title-only/label-title-only.html
index e92fc033c8..c0493b970f 100644
--- a/test/integration/rules/label-title-only/label-title-only.html
+++ b/test/integration/rules/label-title-only/label-title-only.html
@@ -27,4 +27,15 @@
+
+