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 6 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>"
}
}
}
22 changes: 13 additions & 9 deletions lib/checks/label/hidden-explicit-label-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ import { accessibleTextVirtual } from '../../commons/text';
import { escapeSelector } from '../../core/utils';

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

if (label && !isVisible(label, true)) {
const name = accessibleTextVirtual(virtualNode).trim();
const isNameEmpty = name === '';
return isNameEmpty;
if (label && !isVisible(label, true)) {
const name = accessibleTextVirtual(virtualNode).trim();
const isNameEmpty = name === '';
return isNameEmpty;
}
}
return false;
} catch (e) {
return undefined;
}
return false;
}

export default hiddenExplicitLabelEvaluate;
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
7 changes: 6 additions & 1 deletion lib/commons/dom/is-visible.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@ function isAreaVisible(el, screenReader, recursed) {
* @return {Boolean} The element's visibilty status
*/
function isVisible(el, screenReader, recursed) {
'use strict';
if (!el) {
throw new TypeError(
'Cannot determine if element is visible for non-DOM nodes'
);
}

const vNode = getNodeFromTree(el);
const cacheName = '_isVisible' + (screenReader ? 'ScreenReader' : '');

Expand Down
13 changes: 8 additions & 5 deletions lib/commons/standards/implicit-html-roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,14 @@ const implicitHtmlRoles = {
},
input: vNode => {
// Source: https://www.w3.org/TR/html52/sec-forms.html#suggestions-source-element
const listElement = idrefs(vNode.actualNode, 'list').filter(
node => !!node
)[0];
const suggestionsSourceElement =
listElement && listElement.nodeName.toLowerCase() === 'datalist';
let suggestionsSourceElement;
if (vNode.hasAttr('list')) {
const listElement = idrefs(vNode.actualNode, 'list').filter(
node => !!node
)[0];
suggestionsSourceElement =
listElement && listElement.nodeName.toLowerCase() === 'datalist';
}

switch ((vNode.attr('type') || '').toLowerCase()) {
case 'button':
Expand Down
77 changes: 46 additions & 31 deletions lib/commons/text/form-control-value.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import isAriaCombobox from '../forms/is-aria-combobox';
import isAriaRange from '../forms/is-aria-range';
import getOwnedVirtual from '../aria/get-owned-virtual';
import isHiddenWithCSS from '../dom/is-hidden-with-css';
import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node';
import { getNodeFromTree, querySelectorAll } from '../../core/utils';

const controlValueRoles = [
'textbox',
Expand Down Expand Up @@ -80,9 +82,11 @@ function formControlValue(virtualNode, context = {}) {
* @return {string} The calculated value
*/
function nativeTextboxValue(node) {
node = node.actualNode || node;
if (isNativeTextbox(node)) {
return node.value || '';
const vNode =
node instanceof AbstractVirtualNode ? node : getNodeFromTree(node);

if (isNativeTextbox(vNode)) {
return vNode.props.value || '';
}
return '';
}
Expand All @@ -94,16 +98,22 @@ function nativeTextboxValue(node) {
* @return {string} The calculated value
*/
function nativeSelectValue(node) {
node = node.actualNode || node;
if (!isNativeSelect(node)) {
const vNode =
node instanceof AbstractVirtualNode ? node : getNodeFromTree(node);

if (!isNativeSelect(vNode)) {
return '';
}
return (
Array.from(node.options)
.filter(option => option.selected)
.map(option => option.text)
.join(' ') || ''
);

const options = querySelectorAll(vNode, 'option');
const selectedOptions = options.filter(option => option.hasAttr('selected'));

// browser automatically selects the first option
if (!selectedOptions.length) {
selectedOptions.push(options[0]);
}

return selectedOptions.map(option => visibleVirtual(option)).join(' ') || '';
}

/**
Expand All @@ -112,13 +122,16 @@ function nativeSelectValue(node) {
* @param {VirtualNode} element The VirtualNode instance whose value we want
* @return {string} The calculated value
*/
function ariaTextboxValue(virtualNode) {
const { actualNode } = virtualNode;
if (!isAriaTextbox(actualNode)) {
function ariaTextboxValue(node) {
const vNode =
node instanceof AbstractVirtualNode ? node : getNodeFromTree(node);

let { actualNode } = vNode;
if (!isAriaTextbox(vNode)) {
return '';
}
if (!isHiddenWithCSS(actualNode)) {
return visibleVirtual(virtualNode, true);
if (!actualNode || (actualNode && !isHiddenWithCSS(actualNode))) {
return visibleVirtual(vNode, true);
} else {
return actualNode.textContent;
}
Expand All @@ -134,16 +147,17 @@ function ariaTextboxValue(virtualNode) {
* @property {Bool} debug Enable logging for formControlValue
* @return {string} The calculated value
*/
function ariaListboxValue(virtualNode, context) {
const { actualNode } = virtualNode;
if (!isAriaListbox(actualNode)) {
function ariaListboxValue(node, context) {
const vNode =
node instanceof AbstractVirtualNode ? node : getNodeFromTree(node);

if (!isAriaListbox(vNode)) {
return '';
}

const selected = getOwnedVirtual(virtualNode).filter(
const selected = getOwnedVirtual(vNode).filter(
owned =>
getRole(owned) === 'option' &&
owned.actualNode.getAttribute('aria-selected') === 'true'
getRole(owned) === 'option' && owned.attr('aria-selected') === 'true'
);

if (selected.length === 0) {
Expand All @@ -165,17 +179,16 @@ function ariaListboxValue(virtualNode, context) {
* @property {Bool} debug Enable logging for formControlValue
* @return {string} The calculated value
*/
function ariaComboboxValue(virtualNode, context) {
const { actualNode } = virtualNode;
function ariaComboboxValue(node, context) {
const vNode =
node instanceof AbstractVirtualNode ? node : getNodeFromTree(node);
let listbox;

// For combobox, find the first owned listbox:
if (!isAriaCombobox(actualNode)) {
if (!isAriaCombobox(vNode)) {
return '';
}
listbox = getOwnedVirtual(virtualNode).filter(
elm => getRole(elm) === 'listbox'
)[0];
listbox = getOwnedVirtual(vNode).filter(elm => getRole(elm) === 'listbox')[0];

return listbox ? ariaListboxValue(listbox, context) : '';
}
Expand All @@ -187,13 +200,15 @@ function ariaComboboxValue(virtualNode, context) {
* @return {string} The calculated value
*/
function ariaRangeValue(node) {
node = node.actualNode || node;
if (!isAriaRange(node) || !node.hasAttribute('aria-valuenow')) {
const vNode =
node instanceof AbstractVirtualNode ? node : getNodeFromTree(node);

if (!isAriaRange(vNode) || !vNode.hasAttr('aria-valuenow')) {
return '';
}
// Validate the number, if not, return 0.
// Chrome 70 typecasts this, Firefox 62 does not
const valueNow = +node.getAttribute('aria-valuenow');
const valueNow = +vNode.attr('aria-valuenow');
return !isNaN(valueNow) ? String(valueNow) : '0';
}

Expand Down
24 changes: 15 additions & 9 deletions lib/commons/text/label-virtual.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import ariaLabelVirtual from '../aria/label-virtual';
import visible from './visible';
import visibleVirtual from './visible-virtual';
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 +13,33 @@ 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);
ref = closest(virtualNode, 'label');
candidate = ref && visibleVirtual(ref, true);
if (candidate) {
return candidate;
}
Expand Down
Loading