Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(label): work with virtual nodes #2354

Merged
merged 8 commits into from
Jul 20, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions lib/checks/label/explicit-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@ import { getRootNode, isVisible } from '../../commons/dom';
import { accessibleText } from '../../commons/text';
import { escapeSelector } from '../../core/utils';

function explicitEvaluate(node) {
if (node.getAttribute('id')) {
const root = getRootNode(node);
const id = escapeSelector(node.getAttribute('id'));
const label = root.querySelector(`label[for="${id}"]`);
function explicitEvaluate(node, options, virtualNode) {
try {
if (virtualNode.attr('id')) {
const root = getRootNode(virtualNode.actualNode);
const id = escapeSelector(virtualNode.attr('id'));
const label = root.querySelector(`label[for="${id}"]`);

if (label) {
// defer to hidden-explicit-label check for better messaging
if (!isVisible(label)) {
return true;
} else {
return !!accessibleText(label);
if (label) {
// defer to hidden-explicit-label check for better messaging
if (!isVisible(label)) {
return true;
} else {
return !!accessibleText(label);
}
}
}
return false;
} catch (e) {
return undefined;
}
return false;
}

export default explicitEvaluate;
3 changes: 2 additions & 1 deletion lib/checks/label/explicit.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"impact": "critical",
"messages": {
"pass": "Form element has an explicit <label>",
"fail": "Form element does not have an explicit <label>"
"fail": "Form element does not have an explicit <label>",
"incomplete": "Unable to determine if form element has an explicit <label>"
}
}
}
36 changes: 20 additions & 16 deletions lib/checks/label/help-same-as-label-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,31 @@ import { labelVirtual, accessibleText, sanitize } from '../../commons/text';
import { idrefs } from '../../commons/dom';

function helpSameAsLabelEvaluate(node, options, virtualNode) {
var labelText = labelVirtual(virtualNode),
check = node.getAttribute('title');
try {
const labelText = labelVirtual(virtualNode);
let check = virtualNode.attr('title');

if (!labelText) {
return false;
}
if (!labelText) {
return false;
}

if (!check) {
check = '';
if (!check) {
check = '';

if (node.getAttribute('aria-describedby')) {
var ref = idrefs(node, 'aria-describedby');
check = ref
.map(function(thing) {
return thing ? accessibleText(thing) : '';
})
.join('');
if (virtualNode.attr('aria-describedby')) {
const ref = idrefs(virtualNode.actualNode, 'aria-describedby');
check = ref
.map(function(thing) {
return thing ? accessibleText(thing) : '';
})
.join('');
}
}
}

return sanitize(check) === sanitize(labelText);
return sanitize(check) === sanitize(labelText);
} catch (e) {
return undefined;
}
}

export default helpSameAsLabelEvaluate;
3 changes: 2 additions & 1 deletion lib/checks/label/help-same-as-label.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"impact": "minor",
"messages": {
"pass": "Help text (title or aria-describedby) does not duplicate label text",
"fail": "Help text (title or aria-describedby) text is the same as the label text"
"fail": "Help text (title or aria-describedby) text is the same as the label text",
"incomplete": "Unable to determine if Help text (title or aria-describedby) text is the same as the label text"
}
}
}
4 changes: 4 additions & 0 deletions lib/checks/label/hidden-explicit-label-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { accessibleTextVirtual } from '../../commons/text';
import { escapeSelector } from '../../core/utils';

function hiddenExplicitLabelEvaluate(node, options, virtualNode) {
if (!node) {
return undefined;
}

if (node.getAttribute('id')) {
const root = getRootNode(node);
const id = escapeSelector(node.getAttribute('id'));
Expand Down
3 changes: 2 additions & 1 deletion lib/checks/label/hidden-explicit-label.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"impact": "critical",
"messages": {
"pass": "Form element has a visible explicit <label>",
"fail": "Form element has explicit <label> that is hidden"
"fail": "Form element has explicit <label> that is hidden",
"incomplete": "Unable to determine if form element has explicit <label> that is hidden"
}
}
}
16 changes: 10 additions & 6 deletions lib/checks/label/implicit-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { findUpVirtual } from '../../commons/dom';
import { accessibleText } from '../../commons/text';
import { closest } from '../../core/utils';
import { accessibleTextVirtual } from '../../commons/text';

function implicitEvaluate(node, options, virtualNode) {
const label = findUpVirtual(virtualNode, 'label');
if (label) {
return !!accessibleText(label, { inControlContext: true });
try {
const label = closest(virtualNode, 'label');
straker marked this conversation as resolved.
Show resolved Hide resolved
if (label) {
return !!accessibleTextVirtual(label, { inControlContext: true });
}
return false;
} catch (e) {
return undefined;
}
return false;
}

export default implicitEvaluate;
3 changes: 2 additions & 1 deletion lib/checks/label/implicit.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"impact": "critical",
"messages": {
"pass": "Form element has an implicit (wrapped) <label>",
"fail": "Form element does not have an implicit (wrapped) <label>"
"fail": "Form element does not have an implicit (wrapped) <label>",
"incomplete": "Unable to determine if form element has an implicit (wrapped} <label>"
}
}
}
10 changes: 5 additions & 5 deletions lib/commons/aria/label-virtual.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import sanitize from '../text/sanitize';
* @method labelVirtual
* @memberof axe.commons.aria
* @instance
* @param {Object} actualNode The virtualNode to test
* @param {VirtualNode} virtualNode The virtualNode to test
* @return {Mixed} String of visible text, or `null` if no label is found
*/
function labelVirtual({ actualNode }) {
function labelVirtual(virtualNode) {
let ref, candidate;

if (actualNode.getAttribute('aria-labelledby')) {
if (virtualNode.attr('aria-labelledby')) {
// aria-labelledby
ref = idrefs(actualNode, 'aria-labelledby');
ref = idrefs(virtualNode.actualNode, 'aria-labelledby');
candidate = ref
.map(function(thing) {
// TODO: es-module-utils.getNodeFromTree
Expand All @@ -32,7 +32,7 @@ function labelVirtual({ actualNode }) {
}

// aria-label
candidate = actualNode.getAttribute('aria-label');
candidate = virtualNode.attr('aria-label');
if (candidate) {
candidate = sanitize(candidate).trim();
if (candidate) {
Expand Down
27 changes: 18 additions & 9 deletions lib/commons/text/label-virtual.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ariaLabelVirtual from '../aria/label-virtual';
import visible from './visible';
import getRootNode from '../dom/get-root-node';
import findUpVirtual from '../dom/find-up-virtual';
import { closest, escapeSelector } from '../../core/utils';

/**
* Gets the visible text of a label for a given input
Expand All @@ -12,28 +12,37 @@ import findUpVirtual from '../dom/find-up-virtual';
* @param {VirtualNode} node The virtual node mapping to the input to test
* @return {Mixed} String of visible text, or `null` if no label is found
*/
function labelVirtual(node) {
function labelVirtual(virtualNode) {
var ref, candidate, doc;

candidate = ariaLabelVirtual(node);
candidate = ariaLabelVirtual(virtualNode);
if (candidate) {
return candidate;
}

// explicit label
if (node.actualNode.id) {
// TODO: es-module-utils.escapeSelector
const id = axe.utils.escapeSelector(node.actualNode.getAttribute('id'));
doc = getRootNode(node.actualNode);
if (virtualNode.attr('id')) {
if (!virtualNode.actualNode) {
throw new TypeError(
'Cannot resolve explicit label reference for non-DOM nodes'
);
}

const id = escapeSelector(virtualNode.attr('id'));
doc = getRootNode(virtualNode.actualNode);
ref = doc.querySelector('label[for="' + id + '"]');
candidate = ref && visible(ref, true);
if (candidate) {
return candidate;
}
}

ref = findUpVirtual(node, 'label');
candidate = ref && visible(ref, true);
if (!virtualNode.actualNode) {
throw new TypeError('Cannot determine if non-DOM node is visible');
}
straker marked this conversation as resolved.
Show resolved Hide resolved

ref = closest(virtualNode, 'label');
candidate = ref && visible(ref.actualNode, true);
straker marked this conversation as resolved.
Show resolved Hide resolved
if (candidate) {
return candidate;
}
Expand Down
8 changes: 4 additions & 4 deletions lib/rules/label-matches.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
function labelMatches(node) {
function labelMatches(node, virtualNode) {
if (
node.nodeName.toLowerCase() !== 'input' ||
node.hasAttribute('type') === false
virtualNode.props.nodeName !== 'input' ||
virtualNode.hasAttr('type') === false
) {
return true;
}

var type = node.getAttribute('type').toLowerCase();
var type = virtualNode.attr('type').toLowerCase();
return (
['hidden', 'image', 'button', 'submit', 'reset'].includes(type) === false
);
Expand Down
72 changes: 50 additions & 22 deletions test/checks/label/explicit.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,45 @@ describe('explicit-label', function() {

var fixture = document.getElementById('fixture');
var fixtureSetup = axe.testUtils.fixtureSetup;
var queryFixture = axe.testUtils.queryFixture;
var shadowSupport = axe.testUtils.shadowSupport;

afterEach(function() {
fixture.innerHTML = '';
});

it('should return false if an empty label is present', function() {
fixtureSetup('<label for="target"></label><input type="text" id="target">');
var node = fixture.querySelector('#target');
assert.isFalse(axe.testUtils.getCheckEvaluate('explicit-label')(node));
var vNode = queryFixture(
'<label for="target"></label><input type="text" id="target">'
);
assert.isFalse(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
});

it('should return true if a non-empty label is present', function() {
fixtureSetup(
var vNode = queryFixture(
'<label for="target">Text</label><input type="text" id="target">'
);
var node = fixture.querySelector('#target');
assert.isTrue(axe.testUtils.getCheckEvaluate('explicit-label')(node));
assert.isTrue(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
});

it('should return true if an invisible non-empty label is present, to defer to hidden-explicit-label', function() {
fixtureSetup(
var vNode = queryFixture(
'<label for="target" style="display: none;">Text</label><input type="text" id="target">'
);
var node = fixture.querySelector('#target');
assert.isTrue(axe.testUtils.getCheckEvaluate('explicit-label')(node));
assert.isTrue(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
});

it('should return false if a label is not present', function() {
var node = document.createElement('input');
node.type = 'text';
fixtureSetup(node);

assert.isFalse(axe.testUtils.getCheckEvaluate('explicit-label')(node));
var vNode = queryFixture('<input type="text" id="target" />');
assert.isFalse(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
});

(shadowSupport.v1 ? it : xit)(
Expand All @@ -48,8 +53,10 @@ describe('explicit-label', function() {
'<label for="target">American band</label><input id="target">';
fixtureSetup(root);

var node = shadow.querySelector('#target');
assert.isTrue(axe.testUtils.getCheckEvaluate('explicit-label')(node));
var vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
assert.isTrue(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
}
);

Expand All @@ -63,8 +70,10 @@ describe('explicit-label', function() {
'<label for="target"><slot></slot></label><input id="target">';
fixtureSetup(root);

var node = shadow.querySelector('#target');
assert.isTrue(axe.testUtils.getCheckEvaluate('explicit-label')(node));
var vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
assert.isTrue(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
}
);

Expand All @@ -77,8 +86,10 @@ describe('explicit-label', function() {
shadow.innerHTML = '<slot></slot><input id="target">';
fixtureSetup(root);

var node = shadow.querySelector('#target');
assert.isFalse(axe.testUtils.getCheckEvaluate('explicit-label')(node));
var vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target'));
assert.isFalse(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
}
);

Expand All @@ -92,8 +103,25 @@ describe('explicit-label', function() {
'<label for="target">American band</label><slot></slot>';
fixtureSetup(root);

var node = root.querySelector('#target');
assert.isFalse(axe.testUtils.getCheckEvaluate('explicit-label')(node));
var vNode = axe.utils.getNodeFromTree(root.querySelector('#target'));
assert.isFalse(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode)
);
}
);

describe('SerialVirtualNode', function() {
it('should return undefined', function() {
var virtualNode = new axe.SerialVirtualNode({
nodeName: 'input',
attributes: {
type: 'text'
}
});

assert.isFalse(
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, virtualNode)
);
});
});
});
Loading