From 36c574cff3ce3294052ae643050be74dfdebc493 Mon Sep 17 00:00:00 2001 From: Alice Boxhall Date: Wed, 2 Dec 2015 17:45:47 -0800 Subject: [PATCH] Short-circuit collectMatchingElements if a non-visible subtree is found. --- src/js/AccessibilityUtils.js | 46 +++++++++++++------ src/js/AuditRule.js | 2 + test/audits/aria-on-reserved-element-test.js | 10 ++-- test/audits/bad-aria-attribute-test.js | 2 +- .../audits/unsupported-aria-attribute-test.js | 2 +- 5 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/js/AccessibilityUtils.js b/src/js/AccessibilityUtils.js index 657348a7..b98bc403 100644 --- a/src/js/AccessibilityUtils.js +++ b/src/js/AccessibilityUtils.js @@ -430,9 +430,6 @@ axs.utils.getContrastRatioForElement = function(element) { * @return {?number} */ axs.utils.getContrastRatioForElementWithComputedStyle = function(style, element) { - if (axs.utils.isElementHidden(element)) - return null; - var bgColor = axs.utils.getBgColor(style, element); if (!bgColor) return null; @@ -569,22 +566,41 @@ axs.utils.isElementDisabled = function(element) { /** * @param {Element} element An element to check. - * @return {boolean} True if the element is hidden from accessibility. + * @return {boolean} True if an element itself has a style attribute which causes it + * not to be visible. */ -axs.utils.isElementHidden = function(element) { - if (!(element instanceof element.ownerDocument.defaultView.HTMLElement)) - return false; - - if (element.hasAttribute('chromevoxignoreariahidden')) - var chromevoxignoreariahidden = true; - +axs.utils.elementHasNonVisibleStyle = function(element) { var style = window.getComputedStyle(element, null); if (style.display == 'none' || style.visibility == 'hidden') return true; + return false; +} + +/** + * @param {Element} element An element to check. + * @return {boolean} True if the element is not visible to any user. + */ +axs.utils.elementIsNotVisible = function(element) { + if (axs.utils.elementHasNonVisibleStyle(element)) + return true; + var boundingClientRect = element.getBoundingClientRect(); + if (boundingClientRect.width === 0 && boundingClientRect.height === 0 && + boundingClientRect.top === 0 && boundingClientRect.bottom === 0 && + boundingClientRect.left === 0 && boundingClientRect.right === 0) { + return true; + } + return false; +} +/** + * @param {Element} element An element to check. + * @return {boolean} True if the element is hidden from accessibility. + */ +axs.utils.elementIsAriaHidden = function(element) { if (element.hasAttribute('aria-hidden') && - element.getAttribute('aria-hidden').toLowerCase() == 'true') { - return !chromevoxignoreariahidden; + element.getAttribute('aria-hidden').toLowerCase() == 'true' && + !document.documentElement.hasAttribute('chromevoxignoreariahidden')) { + return true; } return false; @@ -596,7 +612,9 @@ axs.utils.isElementHidden = function(element) { * hidden from accessibility. */ axs.utils.isElementOrAncestorHidden = function(element) { - if (axs.utils.isElementHidden(element)) + if (axs.utils.elementIsNotVisible(element)) + return true; + if (axs.utils.elementIsAriaHidden(element)) return true; if (axs.dom.parentElement(element)) diff --git a/src/js/AuditRule.js b/src/js/AuditRule.js index 6257340b..4d9d0c2d 100644 --- a/src/js/AuditRule.js +++ b/src/js/AuditRule.js @@ -133,6 +133,8 @@ axs.AuditRule.collectMatchingElements = function(scope, matcher, collection, opt * @return boolean */ function relevantElementCollector(element) { + if (axs.utils.elementHasNonVisibleStyle(element)) + return false; if (opt_ignoreSelectors) { for (var i = 0; i < opt_ignoreSelectors.length; i++) { if (axs.browserUtils.matchSelector(element, opt_ignoreSelectors[i])) diff --git a/test/audits/aria-on-reserved-element-test.js b/test/audits/aria-on-reserved-element-test.js index 8f141ced..88a5699d 100644 --- a/test/audits/aria-on-reserved-element-test.js +++ b/test/audits/aria-on-reserved-element-test.js @@ -46,7 +46,7 @@ test('Reserved element with role and aria- attributes', function() { var fixture = document.getElementById('qunit-fixture'); - var widget = fixture.appendChild(document.createElement('meta')); + var widget = fixture.appendChild(document.createElement('map')); widget.setAttribute('role', 'spinbutton'); widget.setAttribute('aria-hidden', 'false'); // global widget.setAttribute('aria-required', 'true'); // supported @@ -59,7 +59,7 @@ test('Reserved element with role only', function() { var fixture = document.getElementById('qunit-fixture'); - var widget = fixture.appendChild(document.createElement('meta')); + var widget = fixture.appendChild(document.createElement('map')); widget.setAttribute('role', 'spinbutton'); var expected = { elements: [widget], result: axs.constants.AuditResult.FAIL }; deepEqual(rule.run({ scope: fixture }), expected, 'Reserved elements can\'t take any ARIA attributes.'); @@ -67,7 +67,7 @@ test('Reserved element with aria-attributes only', function() { var fixture = document.getElementById('qunit-fixture'); - var widget = fixture.appendChild(document.createElement('meta')); + var widget = fixture.appendChild(document.createElement('map')); widget.setAttribute('aria-hidden', 'false'); // global var expected = { elements: [widget], result: axs.constants.AuditResult.FAIL }; deepEqual(rule.run({ scope: fixture }), expected, 'Reserved elements can\'t take any ARIA attributes.'); @@ -75,7 +75,7 @@ test('Using ignoreSelectors, reserved element with aria-attributes only', function() { var fixture = document.getElementById('qunit-fixture'); - var widget = fixture.appendChild(document.createElement('meta')); + var widget = fixture.appendChild(document.createElement('map')); var ignoreSelectors = ['#' + (widget.id = 'ignoreMe')]; widget.setAttribute('aria-hidden', 'false'); // global var expected = { result: axs.constants.AuditResult.NA }; @@ -84,7 +84,7 @@ test('Reserved element with no ARIA attributes', function() { var fixture = document.getElementById('qunit-fixture'); - fixture.appendChild(document.createElement('meta')); + fixture.appendChild(document.createElement('map')); var expected = { elements: [], result: axs.constants.AuditResult.PASS }; deepEqual(rule.run({ scope: fixture }), expected, 'A reserved element with no ARIA attributes should pass'); }); diff --git a/test/audits/bad-aria-attribute-test.js b/test/audits/bad-aria-attribute-test.js index 41944601..cabbf010 100644 --- a/test/audits/bad-aria-attribute-test.js +++ b/test/audits/bad-aria-attribute-test.js @@ -60,7 +60,7 @@ */ test('Element with role and global but missing supported and required attributes', function() { var fixture = document.getElementById('qunit-fixture'); - var widget = fixture.appendChild(document.createElement('meta')); // note, a reserved HTML element + var widget = fixture.appendChild(document.createElement('map')); // note, a reserved HTML element widget.setAttribute('role', 'spinbutton'); widget.setAttribute('aria-hidden', 'false'); // global (so the audit will encounter this element) var expected = { elements: [], result: axs.constants.AuditResult.PASS }; diff --git a/test/audits/unsupported-aria-attribute-test.js b/test/audits/unsupported-aria-attribute-test.js index 4b90664c..32f5f14a 100644 --- a/test/audits/unsupported-aria-attribute-test.js +++ b/test/audits/unsupported-aria-attribute-test.js @@ -66,7 +66,7 @@ */ test('Element with role and global but missing supported and required attributes', function() { var fixture = document.getElementById('qunit-fixture'); - var widget = fixture.appendChild(document.createElement('meta')); // note, a reserved HTML element + var widget = fixture.appendChild(document.createElement('map')); // note, a reserved HTML element var expected = { elements: [], result: axs.constants.AuditResult.PASS }; widget.setAttribute('role', 'spinbutton'); widget.setAttribute('aria-hidden', 'false'); // global (so the audit will encounter this element)