Skip to content

Commit

Permalink
feat(shadow DOM): Create commons virtual methods, for backward compat…
Browse files Browse the repository at this point in the history
…ibility

* feat(aria.label): Works without a virtualNode

* feat: Add hasContentVirtual method

* feat(is-offscreen): Add shadow DOM support

* chore: Some code cleanup

* feat(text.visible): Created text.visibleVirtual for shadowDOM

* fix: Pass all tests that use accessibleText

* feat: add shadow support to aria-required-children

Closes #421

* test: use abstracted checkSetup from testutils

* fix: get virtualNode with getNodeFromTree

* test: More testing for accessibleText()

# Conflicts:
#	lib/commons/dom/find-elms-in-context.js
#	lib/commons/text/accessible-text.js
#	test/checks/keyboard/focusable-no-name.js
#	test/checks/tables/same-caption-summary.js
#	test/commons/text/accessible-text.js

* feat(aria-required-parent): add Shadow DOM support

Closes #422

* fix: Rename text.label to text.labelVirtual

* fix: Create aria.labelVirtual method
  • Loading branch information
WilcoFiers committed Aug 3, 2017
1 parent e910d57 commit 86a4c25
Show file tree
Hide file tree
Showing 21 changed files with 259 additions and 249 deletions.
2 changes: 1 addition & 1 deletion lib/checks/label/duplicate-img-label.js
@@ -1,4 +1,4 @@
const text = axe.commons.text.visible(virtualNode, true).toLowerCase();
const text = axe.commons.text.visibleVirtual(virtualNode, true).toLowerCase();
if (text === '') {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/label/help-same-as-label.js
@@ -1,5 +1,5 @@

var labelText = axe.commons.text.label(virtualNode),
var labelText = axe.commons.text.labelVirtual(virtualNode),
check = node.getAttribute('title');

if (!labelText) {
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/label/title-only.js
@@ -1,2 +1,2 @@
var labelText = axe.commons.text.label(virtualNode);
var labelText = axe.commons.text.labelVirtual(virtualNode);
return !labelText && !!(node.getAttribute('title') || node.getAttribute('aria-describedby'));
1 change: 0 additions & 1 deletion lib/checks/media/description.js
Expand Up @@ -5,7 +5,6 @@ if (tracks.length) {
var out = !tracks.some(({ actualNode }) => (
(actualNode.getAttribute('kind') || '').toLowerCase() === 'descriptions'
));
axe.log(tracks.map(t => t.actualNode.getAttribute('kind')), out);
return out;
}
// Undefined if there are no tracks - media may be decorative
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/visibility/hidden-content.js
@@ -1,6 +1,6 @@
const whitelist = ['SCRIPT', 'HEAD', 'TITLE', 'NOSCRIPT', 'STYLE', 'TEMPLATE'];
if (!whitelist.includes(node.tagName.toUpperCase()) &&
axe.commons.dom.hasContent(virtualNode)) {
axe.commons.dom.hasContentVirtual(virtualNode)) {

const styles = window.getComputedStyle(node);
if (styles.getPropertyValue('display') === 'none') {
Expand Down
39 changes: 39 additions & 0 deletions lib/commons/aria/label-virtual.js
@@ -0,0 +1,39 @@
/*global axe, aria, dom, text */
/**
* Gets the accessible ARIA label text of a given element
* @see http://www.w3.org/WAI/PF/aria/roles#namecalculation
* @param {Object} The virtualNode to test
* @return {Mixed} String of visible text, or `null` if no label is found
*/
aria.labelVirtual = function ({ actualNode }) {
let ref, candidate;

if (actualNode.getAttribute('aria-labelledby')) {
// aria-labelledby
ref = dom.idrefs(actualNode, 'aria-labelledby');
candidate = ref.map(function (thing) {
const vNode = axe.utils.getNodeFromTree(axe._tree[0], thing);
return vNode ? text.visibleVirtual(vNode, true) : '';
}).join(' ').trim();

if (candidate) {
return candidate;
}
}

// aria-label
candidate = actualNode.getAttribute('aria-label');
if (candidate) {
candidate = text.sanitize(candidate).trim();
if (candidate) {
return candidate;
}
}

return null;
};

aria.label = function (node) {
node = axe.utils.getNodeFromTree(axe._tree[0], node);
return aria.labelVirtual(node);
};
37 changes: 0 additions & 37 deletions lib/commons/aria/label.js

This file was deleted.

3 changes: 1 addition & 2 deletions lib/commons/dom/find-elms-in-context.js
Expand Up @@ -3,7 +3,7 @@
* Find elements referenced from a given context
*
* @param object {
* context: Node | virtual node Element in the same context
* context: 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)
Expand All @@ -12,7 +12,6 @@
*/
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
Expand Down
Expand Up @@ -19,20 +19,29 @@ function hasChildTextNodes (elm) {
* @param {Object} virtual DOM node
* @return boolean
*/
dom.hasContent = function hasContent (elm, noRecursion) {
if (!elm.actualNode) {
elm = axe.utils.getNodeFromTree(axe._tree[0], elm);
}
dom.hasContentVirtual = function (elm, noRecursion) {
return (
// It has text
hasChildTextNodes(elm) ||
// It is a graphical element
dom.isVisualContent(elm.actualNode) ||
// It has an ARIA label
!!aria.label(elm) ||
!!aria.labelVirtual(elm) ||
// or one of it's descendants does
(!noRecursion && elm.children.some(child => (
child.actualNode.nodeType === 1 && dom.hasContent(child)
child.actualNode.nodeType === 1 && dom.hasContentVirtual(child)
)))
);
};

/**
* Like hasContentVirtual, except with a DOM Node
* IMPORTANT: This method requires the composed tree at axe._tree
*
* @param {Object} DOM node
* @return boolean
*/
dom.hasContent = function hasContent (elm, noRecursion) {
elm = axe.utils.getNodeFromTree(axe._tree[0], elm);
return dom.hasContentVirtual(elm, noRecursion);
};
38 changes: 18 additions & 20 deletions lib/commons/dom/is-offscreen.js
@@ -1,27 +1,26 @@
/*global dom */

dom.isOffscreen = function (element) {
'use strict';
var noParentScrolled = function (element, offset) {
element = element.parentNode;
while (element.nodeName.toLowerCase() !== 'html') {
if (element.scrollTop) {
offset += element.scrollTop;
if (offset >= 0) {
return false;
}
function noParentScrolled (element, offset) {
element = dom.getComposedParent(element);
while (element && element.nodeName.toLowerCase() !== 'html') {
if (element.scrollTop) {
offset += element.scrollTop;
if (offset >= 0) {
return false;
}
element = element.parentNode;
}
return true;
};
element = dom.getComposedParent(element);
}
return true;
}

var leftBoundary,
docElement = document.documentElement,
styl = window.getComputedStyle(element),
dir = window.getComputedStyle(document.body || docElement)
.getPropertyValue('direction'),
coords = dom.getElementCoordinates(element);
dom.isOffscreen = function (element) {
let leftBoundary;
const docElement = document.documentElement;
const styl = window.getComputedStyle(element);
const dir = window.getComputedStyle(document.body || docElement)
.getPropertyValue('direction');
const coords = dom.getElementCoordinates(element);

// bottom edge beyond
if (coords.bottom < 0 && (noParentScrolled(element, coords.bottom) ||
Expand All @@ -47,5 +46,4 @@ dom.isOffscreen = function (element) {
}

return false;

};
4 changes: 2 additions & 2 deletions lib/commons/table/is-data-table.js
Expand Up @@ -9,7 +9,7 @@
*/
table.isDataTable = function (node) {

var role = node.getAttribute('role');
var role = (node.getAttribute('role') || '').toLowerCase();

// The element is not focusable and has role=presentation
if ((role === 'presentation' || role === 'none') && !dom.isFocusable(node)) {
Expand Down Expand Up @@ -70,7 +70,7 @@ table.isDataTable = function (node) {
if (cell.getAttribute('scope') || cell.getAttribute('headers') || cell.getAttribute('abbr')) {
return true;
}
if (['columnheader', 'rowheader'].indexOf(cell.getAttribute('role')) !== -1) {
if (['columnheader', 'rowheader'].includes((cell.getAttribute('role') || '').toLowerCase())) {
return true;
}
// abbr element as a single child element of table cell
Expand Down
2 changes: 1 addition & 1 deletion lib/commons/table/is-row-header.js
Expand Up @@ -6,5 +6,5 @@
* @return {Boolean}
*/
table.isRowHeader = function (node) {
return (['row', 'auto'].indexOf(table.getScope(node)) !== -1);
return (['row', 'auto'].includes(table.getScope(node)));
};
11 changes: 7 additions & 4 deletions lib/commons/text/label.js → lib/commons/text/label-virtual.js
Expand Up @@ -5,10 +5,10 @@
* @param {Object} node The virtual node mapping to the input to test
* @return {Mixed} String of visible text, or `null` if no label is found
*/
text.label = function (node) {
text.labelVirtual = function (node) {
var ref, candidate, doc;

candidate = aria.label(node);
candidate = aria.labelVirtual(node);
if (candidate) {
return candidate;
}
Expand All @@ -18,19 +18,22 @@ text.label = function (node) {
const id = axe.commons.utils.escapeSelector(node.actualNode.getAttribute('id'));
doc = axe.commons.dom.getRootNode(node.actualNode);
ref = doc.querySelector('label[for="' + id + '"]');
ref = axe.utils.getNodeFromTree(axe._tree[0], ref);
candidate = ref && text.visible(ref, true);
if (candidate) {
return candidate;
}
}

ref = dom.findUp(node.actualNode, 'label');
ref = axe.utils.getNodeFromTree(axe._tree[0], ref);
candidate = ref && text.visible(ref, true);
if (candidate) {
return candidate;
}

return null;
};

text.label = function (node) {
node = axe.utils.getNodeFromTree(axe._tree[0], node);
return text.labelVirtual(node);
};
31 changes: 13 additions & 18 deletions lib/commons/text/visible.js → lib/commons/text/visible-virtual.js
@@ -1,31 +1,26 @@
/*global text, dom */
/*global text, dom, axe */

/**
* NOTE: when calculating the text or accessible text of a node that includes shadow
* roots attached to it or its children, the flattened tree must be considered
* rather than the "light DOM"
*/

text.visible = function (element, screenReader, noRecursing) {
'use strict';

var index, child, nodeValue,
childNodes = element.children,
length = childNodes.length,
result = '';

for (index = 0; index < length; index++) {
child = childNodes[index];

text.visibleVirtual = function (element, screenReader, noRecursing) {
const result = element.children.map(child => {
if (child.actualNode.nodeType === 3) { // filter on text nodes
nodeValue = child.actualNode.nodeValue;
const nodeValue = child.actualNode.nodeValue;
if (nodeValue && dom.isVisible(element.actualNode, screenReader)) {
result += nodeValue;
return nodeValue;
}

} else if (!noRecursing) {
result += text.visible(child, screenReader);
return text.visibleVirtual(child, screenReader);
}
}

}).join('');
return text.sanitize(result);
};

text.visible = function (element, screenReader, noRecursing) {
element = axe.utils.getNodeFromTree(axe._tree[0], element);
return text.visibleVirtual(element, screenReader, noRecursing);
};
2 changes: 1 addition & 1 deletion lib/rules/color-contrast-matches.js
Expand Up @@ -63,7 +63,7 @@ if (node.getAttribute('id')) {
}
}

if (axe.commons.text.visible(virtualNode, false, true) === '') {
if (axe.commons.text.visibleVirtual(virtualNode, false, true) === '') {
return false;
}

Expand Down

0 comments on commit 86a4c25

Please sign in to comment.