diff --git a/lib/checks/aria/required-children.js b/lib/checks/aria/required-children.js index 8183bc1d4f..2151908d6e 100644 --- a/lib/checks/aria/required-children.js +++ b/lib/checks/aria/required-children.js @@ -2,6 +2,7 @@ const requiredOwned = axe.commons.aria.requiredOwned; const implicitNodes = axe.commons.aria.implicitNodes; const matchesSelector = axe.utils.matchesSelector; const idrefs = axe.commons.dom.idrefs; +const hasContentVirtual = axe.commons.dom.hasContentVirtual; const reviewEmpty = options && Array.isArray(options.reviewEmpty) ? options.reviewEmpty : []; @@ -94,6 +95,19 @@ function missingRequiredChildren(node, childRoles, all, role) { return null; } +function hasDecendantWithRole(node) { + return ( + node.children && + node.children.some(child => { + const role = axe.commons.aria.getRole(child); + return ( + !['presentation', 'none', null].includes(role) || + hasDecendantWithRole(child) + ); + }) + ); +} + var role = node.getAttribute('role'); var required = requiredOwned(role); @@ -119,7 +133,8 @@ this.data(missing); // Only review empty nodes when a node is both empty and does not have an aria-owns relationship if ( reviewEmpty.includes(role) && - node.children.length === 0 && + !hasContentVirtual(virtualNode, false, true) && + !hasDecendantWithRole(virtualNode) && idrefs(node, 'aria-owns').length === 0 ) { return undefined; diff --git a/test/checks/aria/required-children.js b/test/checks/aria/required-children.js index 959d561a4f..e447590bd4 100644 --- a/test/checks/aria/required-children.js +++ b/test/checks/aria/required-children.js @@ -342,5 +342,65 @@ describe('aria-required-children', function() { checks['aria-required-children'].evaluate.apply(checkContext, params) ); }); + + it('should return undefined when the element has empty children', function() { + var params = checkSetup( + '
' + ); + params[1] = { + reviewEmpty: ['listbox'] + }; + assert.isUndefined( + checks['aria-required-children'].evaluate.apply(checkContext, params) + ); + }); + + it('should return false when the element has empty child with role', function() { + var params = checkSetup( + '
' + ); + params[1] = { + reviewEmpty: ['listbox'] + }; + assert.isFalse( + checks['aria-required-children'].evaluate.apply(checkContext, params) + ); + }); + + it('should return undefined when the element has empty child with role=presentation', function() { + var params = checkSetup( + '
' + ); + params[1] = { + reviewEmpty: ['listbox'] + }; + assert.isUndefined( + checks['aria-required-children'].evaluate.apply(checkContext, params) + ); + }); + + it('should return undefined when the element has empty child with role=none', function() { + var params = checkSetup( + '
' + ); + params[1] = { + reviewEmpty: ['listbox'] + }; + assert.isUndefined( + checks['aria-required-children'].evaluate.apply(checkContext, params) + ); + }); + + it('should return undefined when the element has empty child and aria-label', function() { + var params = checkSetup( + '
' + ); + params[1] = { + reviewEmpty: ['listbox'] + }; + assert.isUndefined( + checks['aria-required-children'].evaluate.apply(checkContext, params) + ); + }); }); }); diff --git a/test/integration/rules/aria-required-children/aria-required-children.html b/test/integration/rules/aria-required-children/aria-required-children.html index 857677e604..e3e913eda5 100644 --- a/test/integration/rules/aria-required-children/aria-required-children.html +++ b/test/integration/rules/aria-required-children/aria-required-children.html @@ -19,3 +19,4 @@
+
diff --git a/test/integration/rules/aria-required-children/aria-required-children.json b/test/integration/rules/aria-required-children/aria-required-children.json index 509b38c072..7030e1cbcb 100644 --- a/test/integration/rules/aria-required-children/aria-required-children.json +++ b/test/integration/rules/aria-required-children/aria-required-children.json @@ -22,6 +22,7 @@ ["#incomplete7"], ["#incomplete8"], ["#incomplete9"], - ["#incomplete10"] + ["#incomplete10"], + ["#incomplete11"] ] }