diff --git a/lib/checks/visibility/hidden-content.js b/lib/checks/visibility/hidden-content.js
index 516267f70f..37ebcdbe48 100644
--- a/lib/checks/visibility/hidden-content.js
+++ b/lib/checks/visibility/hidden-content.js
@@ -1,13 +1,14 @@
-let styles = window.getComputedStyle(node);
+const whitelist = ['SCRIPT', 'HEAD', 'TITLE', 'NOSCRIPT', 'STYLE', 'TEMPLATE'];
+if (!whitelist.includes(node.tagName.toUpperCase()) &&
+ axe.commons.dom.hasContent(virtualNode)) {
-let whitelist = ['SCRIPT', 'HEAD', 'TITLE', 'NOSCRIPT', 'STYLE', 'TEMPLATE'];
-if (!whitelist.includes(node.tagName.toUpperCase()) && axe.commons.dom.hasContent(node)) {
+ const styles = window.getComputedStyle(node);
if (styles.getPropertyValue('display') === 'none') {
return undefined;
} else if (styles.getPropertyValue('visibility') === 'hidden') {
- if (node.parentNode) {
- var parentStyle = window.getComputedStyle(node.parentNode);
- }
+ // Check if visibility isn't inherited
+ const parent = axe.commons.dom.getComposedParent(node);
+ const parentStyle = parent && window.getComputedStyle(parent);
if (!parentStyle || parentStyle.getPropertyValue('visibility') !== 'hidden') {
return undefined;
}
diff --git a/lib/commons/dom/find-up.js b/lib/commons/dom/find-up.js
index 0b0714cb3c..06e20ffe05 100644
--- a/lib/commons/dom/find-up.js
+++ b/lib/commons/dom/find-up.js
@@ -22,11 +22,8 @@ dom.findUp = function (element, target) {
return null;
}
- parent = (element.assignedSlot) ? element.assignedSlot : element.parentNode;
- if (parent.nodeType === 11) {
- parent = parent.host;
- }
// recursively walk up the DOM, checking each parent node
+ parent = dom.getComposedParent(element);
while (parent && matches.indexOf(parent) === -1) {
parent = (parent.assignedSlot) ? parent.assignedSlot : parent.parentNode;
if (parent && parent.nodeType === 11) {
diff --git a/lib/commons/dom/get-composed-parent.js b/lib/commons/dom/get-composed-parent.js
new file mode 100644
index 0000000000..c5cfc74339
--- /dev/null
+++ b/lib/commons/dom/get-composed-parent.js
@@ -0,0 +1,19 @@
+/*global dom */
+/**
+ * Get an element's parent in the composed tree
+ * @param DOMNode Element
+ * @return DOMNode Parent element
+ */
+dom.getComposedParent = function getComposedParent (element) {
+ if (element.assignedSlot) {
+ return element.assignedSlot; // content of a shadow DOM slot
+ } else if (element.parentNode) {
+ var parentNode = element.parentNode;
+ if (parentNode.nodeType === 1) {
+ return parentNode; // Regular node
+ } else if (parentNode.host) {
+ return parentNode.host; // Shadow root
+ }
+ }
+ return null; // Root node
+};
diff --git a/lib/commons/dom/has-content.js b/lib/commons/dom/has-content.js
index 5581018d19..415c9ff3ab 100644
--- a/lib/commons/dom/has-content.js
+++ b/lib/commons/dom/has-content.js
@@ -1,4 +1,17 @@
-/*global dom, aria, axe */
+/*global dom, aria */
+const hiddenTextElms = [
+ 'HEAD', 'TITLE', 'TEMPLATE', 'SCRIPT','STYLE',
+ 'IFRAME', 'OBJECT', 'VIDEO', 'AUDIO', 'NOSCRIPT'
+];
+
+function hasChildTextNodes (elm) {
+ if (!hiddenTextElms.includes(elm.actualNode.nodeName.toUpperCase())) {
+ return elm.children.some(({ actualNode }) => (
+ actualNode.nodeType === 3 && actualNode.nodeValue.trim()
+ ));
+ }
+}
+
/**
* Check that the element has visible content
* in the form of either text, an aria-label or visual content such as image
@@ -6,22 +19,17 @@
* @param {Object} virtual DOM node
* @return boolean
*/
-dom.hasContent = function hasContent(elm) {
- if (
- elm.actualNode.textContent.trim() ||
- aria.label(elm)
- ) {
- return true;
- }
-
- const contentElms = axe.utils.querySelectorAll(elm, '*');
- for (let i = 0; i < contentElms.length; i++) {
- if (
- aria.label(contentElms[i]) ||
- dom.isVisualContent(contentElms[i].actualNode)
- ) {
- return true;
- }
- }
- return false;
+dom.hasContent = function hasContent (elm) {
+ return (
+ // It has text
+ hasChildTextNodes(elm) ||
+ // It is a graphical element
+ dom.isVisualContent(elm.actualNode) ||
+ // It has an ARIA label
+ !!aria.label(elm) ||
+ // or one of it's descendants does
+ elm.children.some(child => (
+ child.actualNode.nodeType === 1 && dom.hasContent(child)
+ ))
+ );
};
diff --git a/lib/commons/dom/is-visible.js b/lib/commons/dom/is-visible.js
index 60add9d77a..6318e2b8e8 100644
--- a/lib/commons/dom/is-visible.js
+++ b/lib/commons/dom/is-visible.js
@@ -65,11 +65,9 @@ dom.isVisible = function (el, screenReader, recursed) {
}
parent = (el.assignedSlot) ? el.assignedSlot : el.parentNode;
-
if (parent) {
return dom.isVisible(parent, screenReader, true);
}
return false;
-
};
diff --git a/test/checks/visibility/hidden-content.js b/test/checks/visibility/hidden-content.js
index ed836ac66e..f563f57ed2 100644
--- a/test/checks/visibility/hidden-content.js
+++ b/test/checks/visibility/hidden-content.js
@@ -1,8 +1,9 @@
+/* global xit */
describe('hidden content', function () {
'use strict';
- var fixture = document.getElementById('fixture');
-
+ var fixture = document.getElementById('fixture');
+ var shadowSupport = document.body && typeof document.body.attachShadow === 'function';
var checkContext = {
_data: null,
data: function (d) {
@@ -10,37 +11,71 @@ describe('hidden content', function () {
}
};
+ function checkSetup (html, options, target) {
+ fixture.innerHTML = html;
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ var node = fixture.querySelector(target || '#target');
+ var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node);
+ return [node, options, virtualNode];
+ }
+
afterEach(function () {
fixture.innerHTML = '';
checkContext._data = null;
+ axe._tree = undefined;
});
it('should return undefined with display:none and children', function () {
- fixture.innerHTML = '
';
- var node = fixture.querySelector('#target');
- assert.isUndefined(checks['hidden-content'].evaluate.call(checkContext, node));
+ var params = checkSetup('');
+ assert.isUndefined(checks['hidden-content'].evaluate.apply(checkContext, params));
});
it('should return undefined with visibility:hidden and children', function () {
- fixture.innerHTML = '';
- var node = fixture.querySelector('#target');
- assert.isUndefined(checks['hidden-content'].evaluate.call(checkContext, node));
+ var params = checkSetup('');
+ assert.isUndefined(checks['hidden-content'].evaluate.apply(checkContext, params));
});
it('should return true with visibility:hidden and parent with visibility:hidden', function () {
- fixture.innerHTML = '';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['hidden-content'].evaluate.call(checkContext, node));
+ var params = checkSetup('');
+ assert.isTrue(checks['hidden-content'].evaluate.apply(checkContext, params));
});
it('should return true with aria-hidden and no content', function () {
- fixture.innerHTML = '';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['hidden-content'].evaluate.call(checkContext, node));
+ var params = checkSetup('');
+ assert.isTrue(checks['hidden-content'].evaluate.apply(checkContext, params));
});
it('should skip whitelisted elements', function () {
var node = document.querySelector('head');
- assert.isTrue(checks['hidden-content'].evaluate.call(checkContext, node));
+ axe._tree = axe.utils.getFlattenedTree(document.documentElement);
+ var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node);
+ assert.isTrue(checks['hidden-content'].evaluate(node, undefined, virtualNode));
+ });
+
+ (shadowSupport ? it : xit)('works on elements in a shadow DOM', function () {
+ fixture.innerHTML = '';
+ var shadowRoot = document.getElementById('shadow').attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = '' +
+ '' +
+ '
';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+
+ var shadow = document.querySelector('#shadow');
+ var virtualShadow = axe.utils.getNodeFromTree(axe._tree[0], shadow);
+ assert.isTrue(
+ checks['hidden-content'].evaluate(shadow, undefined, virtualShadow)
+ );
+
+ var target = shadowRoot.querySelector('#target');
+ var virtualTarget = axe.utils.getNodeFromTree(axe._tree[0], target);
+ assert.isUndefined(
+ checks['hidden-content'].evaluate(target, undefined, virtualTarget)
+ );
+
+ var content = document.querySelector('#content');
+ var virtualContent = axe.utils.getNodeFromTree(axe._tree[0], content);
+ assert.isTrue(
+ checks['hidden-content'].evaluate(content, undefined, virtualContent)
+ );
});
});
diff --git a/test/commons/dom/get-composed-parent.js b/test/commons/dom/get-composed-parent.js
new file mode 100644
index 0000000000..4dfd41a230
--- /dev/null
+++ b/test/commons/dom/get-composed-parent.js
@@ -0,0 +1,72 @@
+/* global xit */
+describe('dom.getComposedParent', function () {
+ 'use strict';
+ var getComposedParent = axe.commons.dom.getComposedParent;
+ var fixture = document.getElementById('fixture');
+ var shadowSupport = document.body && typeof document.body.attachShadow === 'function';
+
+ afterEach(function () {
+ fixture.innerHTML = '';
+ });
+
+ it('returns the parentNode normally', function () {
+ fixture.innerHTML = '';
+
+ var actual = getComposedParent(document.getElementById('target'));
+ assert.instanceOf(actual, Node);
+ assert.equal(actual, document.getElementById('parent'));
+ });
+
+ it('returns null from the documentElement', function () {
+ assert.isNull(
+ getComposedParent(document.documentElement)
+ );
+ });
+
+ (shadowSupport ? it : xit)('returns the slot node for slotted content', function () {
+ fixture.innerHTML = '';
+ var shadowRoot = document.getElementById('shadow').attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = '' +
+ '' +
+ '
';
+
+ var actual = getComposedParent(fixture.querySelector('#target'));
+ assert.instanceOf(actual, Node);
+ assert.equal(actual, shadowRoot.querySelector('#parent'));
+ });
+
+ (shadowSupport ? it : xit)('returns explicitly slotted nodes', function () {
+ fixture.innerHTML = '';
+ var shadowRoot = document.getElementById('shadow').attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = '' +
+ '' +
+ '' +
+ '
';
+
+ var actual = getComposedParent(fixture.querySelector('#target'));
+ assert.instanceOf(actual, Node);
+ assert.equal(actual, shadowRoot.querySelector('#parent'));
+ });
+
+ (shadowSupport ? it : xit)('returns elements within a shadow tree', function () {
+ fixture.innerHTML = ' content
';
+ var shadowRoot = document.getElementById('shadow').attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = '' +
+ '' +
+ '
';
+
+ var actual = getComposedParent(shadowRoot.querySelector('#target'));
+ assert.instanceOf(actual, Node);
+ assert.equal(actual, shadowRoot.querySelector('#parent'));
+ });
+
+ (shadowSupport ? it : xit)('returns the host when it reaches the shadow root', function () {
+ fixture.innerHTML = ' content
';
+ var shadowRoot = document.getElementById('parent').attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = '
';
+
+ var actual = getComposedParent(shadowRoot.querySelector('#target'));
+ assert.instanceOf(actual, Node);
+ assert.equal(actual, fixture.querySelector('#parent'));
+ });
+});
diff --git a/test/commons/dom/has-content.js b/test/commons/dom/has-content.js
index 6d9e20bcc9..315956bafd 100644
--- a/test/commons/dom/has-content.js
+++ b/test/commons/dom/has-content.js
@@ -1,7 +1,9 @@
+/* global xit */
describe('dom.hasContent', function () {
'use strict';
var hasContent = axe.commons.dom.hasContent;
var fixture = document.getElementById('fixture');
+ var shadowSupport = document.body && typeof document.body.attachShadow === 'function';
var tree;
it('returns false if there is no content', function () {
@@ -51,4 +53,36 @@ describe('dom.hasContent', function () {
hasContent(axe.utils.querySelectorAll(tree, '#target')[0])
);
});
+
+ it('is false if the element does not show text', function () {
+ fixture.innerHTML = '';
+ tree = axe.utils.getFlattenedTree(fixture);
+ assert.isFalse(
+ hasContent(axe.utils.querySelectorAll(tree, '#target')[0])
+ );
+ });
+
+ (shadowSupport ? it : xit)('looks at content of shadow dom elements', function () {
+ fixture.innerHTML = '';
+ var shadow = fixture.querySelector('#target').attachShadow({ mode: 'open' });
+ shadow.innerHTML = 'Some text';
+ tree = axe.utils.getFlattenedTree(fixture);
+
+ assert.isTrue(
+ hasContent(axe.utils.querySelectorAll(tree, '#target')[0])
+ );
+ });
+
+ (shadowSupport ? it : xit)('looks at the slots in a shadow tree', function () {
+ fixture.innerHTML = 'some text
';
+ var shadow = fixture.querySelector('#shadow').attachShadow({ mode: 'open' });
+ shadow.innerHTML = '
';
+ tree = axe.utils.getFlattenedTree(fixture);
+ var node = axe.utils.querySelectorAll(tree, '.target');
+
+ axe.log(tree, node);
+ assert.isTrue(
+ hasContent(axe.utils.querySelectorAll(tree, '.target')[0])
+ );
+ });
});