-
Notifications
You must be signed in to change notification settings - Fork 791
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(label): avoid passing labels because of an input[value] (#3688)
* fix(label): avoid passing labels because of an input[value] * correct label examples * Add textarea integration test * Fix failing test * Use sanitize * Add addition test for explicit-evaluate
- Loading branch information
1 parent
95cf6e7
commit 54a8116
Showing
9 changed files
with
312 additions
and
223 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,43 @@ | ||
import { getRootNode, isVisibleOnScreen } from '../../commons/dom'; | ||
import { accessibleText } from '../../commons/text'; | ||
import { accessibleText, sanitize } from '../../commons/text'; | ||
import { escapeSelector } from '../../core/utils'; | ||
|
||
function explicitEvaluate(node, options, virtualNode) { | ||
if (virtualNode.attr('id')) { | ||
if (!virtualNode.actualNode) { | ||
return undefined; | ||
} | ||
if (!virtualNode.attr('id')) { | ||
return false; | ||
} | ||
if (!virtualNode.actualNode) { | ||
return undefined; | ||
} | ||
|
||
const root = getRootNode(virtualNode.actualNode); | ||
const id = escapeSelector(virtualNode.attr('id')); | ||
const labels = Array.from(root.querySelectorAll(`label[for="${id}"]`)); | ||
const root = getRootNode(virtualNode.actualNode); | ||
const id = escapeSelector(virtualNode.attr('id')); | ||
const labels = Array.from(root.querySelectorAll(`label[for="${id}"]`)); | ||
this.relatedNodes(labels); | ||
|
||
if (labels.length) { | ||
try { | ||
return labels.some(label => { | ||
// defer to hidden-explicit-label check for better messaging | ||
if (!isVisibleOnScreen(label)) { | ||
return true; | ||
} else { | ||
return !!accessibleText(label); | ||
} | ||
}); | ||
} catch (e) { | ||
return undefined; | ||
} | ||
} | ||
if (!labels.length) { | ||
return false; | ||
} | ||
|
||
return false; | ||
try { | ||
return labels.some(label => { | ||
// defer to hidden-explicit-label check for better messaging | ||
if (!isVisibleOnScreen(label)) { | ||
return true; | ||
} else { | ||
const explicitLabel = sanitize( | ||
accessibleText(label, { | ||
inControlContext: true, | ||
startNode: virtualNode | ||
}) | ||
); | ||
this.data({ explicitLabel }); | ||
return !!explicitLabel; | ||
} | ||
}); | ||
} catch (e) { | ||
return undefined; | ||
} | ||
} | ||
|
||
export default explicitEvaluate; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,136 +1,167 @@ | ||
describe('explicit-label', function() { | ||
'use strict'; | ||
|
||
var fixture = document.getElementById('fixture'); | ||
var fixtureSetup = axe.testUtils.fixtureSetup; | ||
var queryFixture = axe.testUtils.queryFixture; | ||
var shadowSupport = axe.testUtils.shadowSupport; | ||
|
||
afterEach(function() { | ||
fixture.innerHTML = ''; | ||
describe('explicit-label', () => { | ||
const fixtureSetup = axe.testUtils.fixtureSetup; | ||
const checkSetup = axe.testUtils.checkSetup; | ||
const checkEvaluate = axe.testUtils.getCheckEvaluate('explicit-label'); | ||
const checkContext = axe.testUtils.MockCheckContext(); | ||
|
||
afterEach(() => { | ||
checkContext.reset(); | ||
}); | ||
|
||
it('should return false if an empty label is present', function() { | ||
var vNode = queryFixture( | ||
it('returns false if an empty label is present', () => { | ||
const params = checkSetup( | ||
'<label for="target"></label><input type="text" id="target">' | ||
); | ||
assert.isFalse( | ||
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode) | ||
assert.isFalse(checkEvaluate.apply(checkContext, params)); | ||
}); | ||
|
||
it('returns false if the label is empty except for the target value', () => { | ||
const params = checkSetup( | ||
'<label for="target"> <input type="text" id="target" value="snacks"> </label>' | ||
); | ||
assert.isFalse(checkEvaluate.apply(checkContext, params)); | ||
}); | ||
|
||
it('should return true if a non-empty label is present', function() { | ||
var vNode = queryFixture( | ||
'<label for="target">Text</label><input type="text" id="target">' | ||
it('returns false if an empty label is present that uses aria-labelledby', () => { | ||
const params = checkSetup( | ||
'<input type="text" id="target">' + | ||
'<label for="target" aria-labelledby="lbl"></label>' + | ||
'<span id="lbl">aria label</span>' | ||
); | ||
assert.isTrue( | ||
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode) | ||
assert.isFalse(checkEvaluate.apply(checkContext, params)); | ||
}); | ||
|
||
it('returns true if a non-empty label is present', () => { | ||
const params = checkSetup( | ||
'<label for="target">Text</label><input type="text" id="target">' | ||
); | ||
assert.isTrue(checkEvaluate.apply(checkContext, params)); | ||
}); | ||
|
||
it('should return true if an invisible non-empty label is present, to defer to hidden-explicit-label', function() { | ||
var vNode = queryFixture( | ||
it('returns true if an invisible non-empty label is present, to defer to hidden-explicit-label', () => { | ||
const params = checkSetup( | ||
'<label for="target" style="display: none;">Text</label><input type="text" id="target">' | ||
); | ||
assert.isTrue( | ||
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode) | ||
); | ||
assert.isTrue(checkEvaluate.apply(checkContext, params)); | ||
}); | ||
|
||
it('should return false if a label is not present', function() { | ||
var vNode = queryFixture('<input type="text" id="target" />'); | ||
assert.isFalse( | ||
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode) | ||
); | ||
it('returns false if a label is not present', () => { | ||
const params = checkSetup('<input type="text" id="target" />'); | ||
assert.isFalse(checkEvaluate.apply(checkContext, params)); | ||
}); | ||
|
||
it('should work for multiple labels', function() { | ||
var vNode = queryFixture( | ||
it('should work for multiple labels', () => { | ||
const params = checkSetup( | ||
'<label for="target"></label><label for="target">Text</label><input type="text" id="target">' | ||
); | ||
assert.isTrue( | ||
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode) | ||
); | ||
assert.isTrue(checkEvaluate.apply(checkContext, params)); | ||
}); | ||
|
||
(shadowSupport.v1 ? it : xit)( | ||
'should return true if input and label are in the same shadow root', | ||
function() { | ||
var root = document.createElement('div'); | ||
var shadow = root.attachShadow({ mode: 'open' }); | ||
describe('.data', () => { | ||
it('is null if there is no label', () => { | ||
const params = checkSetup('<input type="text" id="target" />'); | ||
checkEvaluate.apply(checkContext, params); | ||
assert.isNull(checkContext._data); | ||
}); | ||
|
||
it('includes the `explicitLabel` text of the first non-empty label', () => { | ||
const params = checkSetup( | ||
'<label for="target"> </label>' + | ||
'<label for="target"> text </label>' + | ||
'<label for="target"> more text </label>' + | ||
'<input type="text" id="target" />' | ||
); | ||
checkEvaluate.apply(checkContext, params); | ||
assert.deepEqual(checkContext._data, { explicitLabel: 'text' }); | ||
}); | ||
|
||
it('is empty { explicitLabel: "" } if the label is empty', () => { | ||
const params = checkSetup( | ||
'<label for="target"> </label>' + | ||
'<label for="target"></label>' + | ||
'<input type="text" id="target" />' | ||
); | ||
checkEvaluate.apply(checkContext, params); | ||
assert.deepEqual(checkContext._data, { explicitLabel: '' }); | ||
}); | ||
}); | ||
|
||
describe('related nodes', () => { | ||
it('is empty when there are no labels', () => { | ||
const params = checkSetup('<input type="text" id="target" />'); | ||
checkEvaluate.apply(checkContext, params); | ||
assert.isEmpty(checkContext._relatedNodes); | ||
}); | ||
|
||
it('includes each associated label', () => { | ||
const params = checkSetup( | ||
'<label for="target" id="lbl1"></label>' + | ||
'<label for="target" id="lbl2"></label>' + | ||
'<input type="text" id="target" />' | ||
); | ||
checkEvaluate.apply(checkContext, params); | ||
const ids = checkContext._relatedNodes.map(node => '#' + node.id); | ||
assert.deepEqual(ids, ['#lbl1', '#lbl2']); | ||
}); | ||
}); | ||
|
||
describe('with shadow DOM', () => { | ||
it('returns true if input and label are in the same shadow root', () => { | ||
const root = document.createElement('div'); | ||
const shadow = root.attachShadow({ mode: 'open' }); | ||
shadow.innerHTML = | ||
'<label for="target">American band</label><input id="target">'; | ||
fixtureSetup(root); | ||
|
||
var vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target')); | ||
assert.isTrue( | ||
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode) | ||
); | ||
} | ||
); | ||
const vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target')); | ||
assert.isTrue(checkEvaluate.call(checkContext, null, {}, vNode)); | ||
}); | ||
|
||
(shadowSupport.v1 ? it : xit)( | ||
'should return true if label content is slotted', | ||
function() { | ||
var root = document.createElement('div'); | ||
it('returns true if label content is slotted', () => { | ||
const root = document.createElement('div'); | ||
root.innerHTML = 'American band'; | ||
var shadow = root.attachShadow({ mode: 'open' }); | ||
const shadow = root.attachShadow({ mode: 'open' }); | ||
shadow.innerHTML = | ||
'<label for="target"><slot></slot></label><input id="target">'; | ||
fixtureSetup(root); | ||
|
||
var vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target')); | ||
assert.isTrue( | ||
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode) | ||
); | ||
} | ||
); | ||
const vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target')); | ||
assert.isTrue(checkEvaluate.call(checkContext, null, {}, vNode)); | ||
}); | ||
|
||
(shadowSupport.v1 ? it : xit)( | ||
'should return false if input is inside shadow DOM and the label is not', | ||
function() { | ||
var root = document.createElement('div'); | ||
it('returns false if input is inside shadow DOM and the label is not', () => { | ||
const root = document.createElement('div'); | ||
root.innerHTML = '<label for="target">American band</label>'; | ||
var shadow = root.attachShadow({ mode: 'open' }); | ||
const shadow = root.attachShadow({ mode: 'open' }); | ||
shadow.innerHTML = '<slot></slot><input id="target">'; | ||
fixtureSetup(root); | ||
|
||
var vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target')); | ||
assert.isFalse( | ||
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode) | ||
); | ||
} | ||
); | ||
const vNode = axe.utils.getNodeFromTree(shadow.querySelector('#target')); | ||
assert.isFalse(checkEvaluate.call(checkContext, null, {}, vNode)); | ||
}); | ||
|
||
(shadowSupport.v1 ? it : xit)( | ||
'should return false if label is inside shadow DOM and the input is not', | ||
function() { | ||
var root = document.createElement('div'); | ||
it('returns false if label is inside shadow DOM and the input is not', () => { | ||
const root = document.createElement('div'); | ||
root.innerHTML = '<input id="target">'; | ||
var shadow = root.attachShadow({ mode: 'open' }); | ||
const shadow = root.attachShadow({ mode: 'open' }); | ||
shadow.innerHTML = | ||
'<label for="target">American band</label><slot></slot>'; | ||
fixtureSetup(root); | ||
|
||
var vNode = axe.utils.getNodeFromTree(root.querySelector('#target')); | ||
assert.isFalse( | ||
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, vNode) | ||
); | ||
} | ||
); | ||
const vNode = axe.utils.getNodeFromTree(root.querySelector('#target')); | ||
assert.isFalse(checkEvaluate.call(checkContext, null, {}, vNode)); | ||
}); | ||
}); | ||
|
||
describe('SerialVirtualNode', function() { | ||
it('should return undefined', function() { | ||
var virtualNode = new axe.SerialVirtualNode({ | ||
describe('SerialVirtualNode', () => { | ||
it('returns undefined', () => { | ||
const virtualNode = new axe.SerialVirtualNode({ | ||
nodeName: 'input', | ||
attributes: { | ||
type: 'text' | ||
} | ||
}); | ||
|
||
assert.isFalse( | ||
axe.testUtils.getCheckEvaluate('explicit-label')(null, {}, virtualNode) | ||
); | ||
assert.isFalse(checkEvaluate.call(checkContext, null, {}, virtualNode)); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.