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"]
]
}