Skip to content

Commit

Permalink
feat: Run text.accessibleText() on virtual elements (#420)
Browse files Browse the repository at this point in the history
  • Loading branch information
WilcoFiers authored Jul 17, 2017
1 parent 0478cbd commit 414fcbe
Show file tree
Hide file tree
Showing 16 changed files with 585 additions and 282 deletions.
26 changes: 26 additions & 0 deletions lib/commons/dom/find-elms-in-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* global axe, dom */
/**
* Find elements referenced from a given context
*
* @param object {
* context: Node | virtual node Element in the same context
* value: String attribute value to search for
* attr: String attribute name to search for
* elm: String ndoeName to search for (optional)
* }
* @return Array[Node]
*/
dom.findElmsInContext = function ({ context, value, attr, elm = '' }) {
let root;
context = context.actualNode || context;
const escapedValue = axe.utils.escapeSelector(value);

if (context.nodeType === 9 || context.nodeType === 11) { // It's already root
root = context;
} else {
root = dom.getRootNode(context);
}
return Array.from(
root.querySelectorAll(elm + '[' + attr + '=' + escapedValue + ']')
);
};
174 changes: 95 additions & 79 deletions lib/commons/text/accessible-text.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*global text, dom, aria, axe */
/*jshint maxstatements: 25, maxcomplexity: 19 */
/*jshint maxstatements: 27, maxcomplexity: 19 */

var defaultButtonValues = {
submit: 'Submit',
Expand All @@ -19,34 +19,34 @@ var phrasingElements = ['A', 'EM', 'STRONG', 'SMALL', 'MARK', 'ABBR', 'DFN', 'I'
* @param {HTMLElement} element The HTMLElement
* @return {HTMLElement} The label element, or null if none is found
*/
function findLabel(element) {
var ref = null;
if (element.id) {
ref = document.querySelector('label[for="' + axe.utils.escapeSelector(element.id) + '"]');
if (ref) {
return ref;
}
function findLabel({ actualNode }) {
let label;
if (actualNode.id) {
label = dom.findElmsInContext({
elm: 'label', attr: 'for', value: actualNode.id, context: actualNode
})[0];
} else {
label = dom.findUp(actualNode, 'label');
}
ref = dom.findUp(element, 'label');
return ref;
return axe.utils.getNodeFromTree(axe._tree[0], label);
}

function isButton(element) {
return ['button', 'reset', 'submit'].indexOf(element.type) !== -1;
function isButton({ actualNode }) {
return ['button', 'reset', 'submit'].includes(actualNode.type.toLowerCase());
}

function isInput(element) {
var nodeName = element.nodeName.toUpperCase();
function isInput({ actualNode }) {
var nodeName = actualNode.nodeName.toUpperCase();
return (nodeName === 'TEXTAREA' || nodeName === 'SELECT') ||
(nodeName === 'INPUT' && element.type.toLowerCase() !== 'hidden');
(nodeName === 'INPUT' && actualNode.type.toLowerCase() !== 'hidden');
}

function shouldCheckSubtree(element) {
return ['BUTTON', 'SUMMARY', 'A'].indexOf(element.nodeName.toUpperCase()) !== -1;
function shouldCheckSubtree({ actualNode }) {
return ['BUTTON', 'SUMMARY', 'A'].includes(actualNode.nodeName.toUpperCase());
}

function shouldNeverCheckSubtree(element) {
return ['TABLE', 'FIGURE'].indexOf(element.nodeName.toUpperCase()) !== -1;
function shouldNeverCheckSubtree({ actualNode }) {
return ['TABLE', 'FIGURE'].includes(actualNode.nodeName.toUpperCase());
}

/**
Expand All @@ -55,19 +55,18 @@ function shouldNeverCheckSubtree(element) {
* @param {HTMLElement} element The HTMLElement
* @return {string} The calculated value
*/
function formValueText(element) {
var nodeName = element.nodeName.toUpperCase();
function formValueText({ actualNode }) {
const nodeName = actualNode.nodeName.toUpperCase();
if (nodeName === 'INPUT') {
if (!element.hasAttribute('type') || (
inputTypes.indexOf(element.getAttribute('type').toLowerCase()) !== -1) &&
element.value) {
return element.value;
if (!actualNode.hasAttribute('type') ||
inputTypes.includes(actualNode.type.toLowerCase())) {
return actualNode.value;
}
return '';
}

if (nodeName === 'SELECT') {
var opts = element.options;
var opts = actualNode.options;
if (opts && opts.length) {
var returnText = '';
for (var i = 0; i < opts.length; i++) {
Expand All @@ -80,14 +79,18 @@ function formValueText(element) {
return '';
}

if (nodeName === 'TEXTAREA' && element.value) {
return element.value;
if (nodeName === 'TEXTAREA' && actualNode.value) {
return actualNode.value;
}
return '';
}

function checkDescendant(element, nodeName) {
var candidate = element.querySelector(nodeName.toLowerCase());
/**
* Get the accessible text of first matching node
* IMPORTANT: This method does not look at the composed tree
*/
function checkDescendant({ actualNode }, nodeName) {
var candidate = actualNode.querySelector(nodeName.toLowerCase());
if (candidate) {
return text.accessibleText(candidate);
}
Expand All @@ -102,25 +105,27 @@ function checkDescendant(element, nodeName) {
* @param {HTMLElement} element The HTMLElement
* @return {boolean} True if embedded control
*/
function isEmbeddedControl(e) {
if (!e) {
function isEmbeddedControl(elm) {
if (!elm) {
return false;
}
switch (e.nodeName.toUpperCase()) {
const { actualNode } = elm;
switch (actualNode.nodeName.toUpperCase()) {
case 'SELECT':
case 'TEXTAREA':
return true;
case 'INPUT':
return !e.hasAttribute('type') || (inputTypes.indexOf(e.getAttribute('type').toLowerCase()) !== -1);
return (!actualNode.hasAttribute('type') ||
inputTypes.includes(actualNode.getAttribute('type').toLowerCase()));
default:
return false;
}
}

function shouldCheckAlt(element) {
var nodeName = element.nodeName.toUpperCase();
return (nodeName === 'INPUT' && element.type.toLowerCase() === 'image') ||
['IMG', 'APPLET', 'AREA'].indexOf(nodeName) !== -1;
function shouldCheckAlt({ actualNode }) {
const nodeName = actualNode.nodeName.toUpperCase();
return ['IMG', 'APPLET', 'AREA'].includes(nodeName) ||
(nodeName === 'INPUT' && actualNode.type.toLowerCase() === 'image');
}

function nonEmptyText(t) {
Expand All @@ -137,34 +142,33 @@ function nonEmptyText(t) {
* @return {string}
*/
text.accessibleText = function(element, inLabelledByContext) {
//todo: implement shadowDOM
var accessibleNameComputation;
var encounteredNodes = [];
let accessibleNameComputation;
const encounteredNodes = [];
if (element instanceof Node) {
element = axe.utils.getNodeFromTree(axe._tree[0], element);
}

function getInnerText (element, inLabelledByContext, inControlContext) {
var nodes = element.childNodes;
var returnText = '';
var node;

for (var i = 0; i < nodes.length; i++) {
node = nodes[i];
if (node.nodeType === 3) {
returnText += node.textContent;
} else if (node.nodeType === 1) {
if (phrasingElements.indexOf(node.nodeName.toUpperCase()) === -1) {
return element.children.reduce((returnText, child) => {
const { actualNode } = child;
if (actualNode.nodeType === 3) {
returnText += actualNode.nodeValue;
} else if (actualNode.nodeType === 1) {
if (!phrasingElements.includes(actualNode.nodeName.toUpperCase())) {
returnText += ' ';
}
returnText += accessibleNameComputation(nodes[i], inLabelledByContext, inControlContext);
returnText += accessibleNameComputation(child, inLabelledByContext,
inControlContext);
}
}

return returnText;
return returnText;
}, '');
}

function checkNative (element, inLabelledByContext, inControlContext) {
// jshint maxstatements:30
var returnText = '';
var nodeName = element.nodeName.toUpperCase();
let returnText = '';
const { actualNode } = element;
const nodeName = actualNode.nodeName.toUpperCase();

if (shouldCheckSubtree(element)) {
returnText = getInnerText(element, false, false) || '';
Expand All @@ -187,20 +191,21 @@ text.accessibleText = function(element, inLabelledByContext) {
return returnText;
}

returnText = element.getAttribute('title') || element.getAttribute('summary') || '';
returnText = (actualNode.getAttribute('title') ||
actualNode.getAttribute('summary') || '');

if (nonEmptyText(returnText)) {
return returnText;
}
}

if (shouldCheckAlt(element)) {
return element.getAttribute('alt') || '';
return actualNode.getAttribute('alt') || '';
}

if (isInput(element) && !inControlContext) {
if (isButton(element)) {
return element.value || element.title || defaultButtonValues[element.type] || '';
return actualNode.value || actualNode.title || defaultButtonValues[actualNode.type] || '';
}

var labelElement = findLabel(element);
Expand All @@ -213,21 +218,29 @@ text.accessibleText = function(element, inLabelledByContext) {
}

function checkARIA (element, inLabelledByContext, inControlContext) {

if (!inLabelledByContext && element.hasAttribute('aria-labelledby')) {
return text.sanitize(dom.idrefs(element, 'aria-labelledby').map(function(l) {
if (element === l) {
encounteredNodes.pop();
} //let element be encountered twice
return accessibleNameComputation(l, true, element !== l);
let returnText = '';
const { actualNode } = element;
if (!inLabelledByContext && actualNode.hasAttribute('aria-labelledby')) {
// Store the return text, if it's empty, fall back to aria-label
returnText = text.sanitize(dom.idrefs(actualNode, 'aria-labelledby').map(label => {
if (label !== null) {// handle unfound elements by dom.idref
if (actualNode === label) {
encounteredNodes.pop();
} //let element be encountered twice
const vLabel = axe.utils.getNodeFromTree(axe._tree[0], label);
return accessibleNameComputation(vLabel, true, actualNode !== label);
} else {
return '';
}
}).join(' '));
}

if (!(inControlContext && isEmbeddedControl(element)) && element.hasAttribute('aria-label')) {
return text.sanitize(element.getAttribute('aria-label'));
if (!returnText && !(inControlContext && isEmbeddedControl(element)) &&
actualNode.hasAttribute('aria-label')) {
return text.sanitize(actualNode.getAttribute('aria-label'));
}

return '';
return returnText;
}

/**
Expand All @@ -240,20 +253,22 @@ text.accessibleText = function(element, inLabelledByContext) {
* @return {string}
*/
accessibleNameComputation = function (element, inLabelledByContext, inControlContext) {
'use strict';

var returnText;
let returnText;
// If the node was already checked or is null, skip
if (element === null || (encounteredNodes.indexOf(element) !== -1)) {
if (!element || encounteredNodes.includes(element)) {
return '';

// if the node is invalid, throw
} else if (element !== null && element.actualNode instanceof Node !== true) {
throw new Error('Invalid argument. Virtual Node must be provided');

//Step 2a: Skip if the element is hidden, unless part of labelledby
} else if(!inLabelledByContext && !dom.isVisible(element, true)) {
} else if(!inLabelledByContext && !dom.isVisible(element.actualNode, true)) {
return '';
}

encounteredNodes.push(element);
var role = element.getAttribute('role');
var role = element.actualNode.getAttribute('role');

//Step 2b & 2c
returnText = checkARIA(element, inLabelledByContext, inControlContext);
Expand All @@ -276,7 +291,8 @@ text.accessibleText = function(element, inLabelledByContext) {
}

//Step 2f
if (!shouldNeverCheckSubtree(element) && (!role || aria.getRolesWithNameFromContents().indexOf(role) !== -1)) {
if (!shouldNeverCheckSubtree(element) &&
(!role || aria.getRolesWithNameFromContents().indexOf(role) !== -1)) {

returnText = getInnerText(element, inLabelledByContext, inControlContext);

Expand All @@ -288,8 +304,8 @@ text.accessibleText = function(element, inLabelledByContext) {
//Step 2g - if text node, return value (handled in getInnerText)

//Step 2h
if (element.hasAttribute('title')) {
return element.getAttribute('title');
if (element.actualNode.hasAttribute('title')) {
return element.actualNode.getAttribute('title');
}

return '';
Expand Down
Loading

0 comments on commit 414fcbe

Please sign in to comment.