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 : []; function owns(node, virtualTree, role, ariaOwned) { if (node === null) { return false; } var implicit = implicitNodes(role), selector = ['[role="' + role + '"]']; if (implicit) { selector = selector.concat( implicit.map(implicitSelector => implicitSelector + ':not([role])') ); } selector = selector.join(','); return ariaOwned ? matchesSelector(node, selector) || !!axe.utils.querySelectorAll(virtualTree, selector)[0] : !!axe.utils.querySelectorAll(virtualTree, selector)[0]; } function ariaOwns(nodes, role) { var index, length; for (index = 0, length = nodes.length; index < length; index++) { if (nodes[index] === null) { continue; } const virtualTree = axe.utils.getNodeFromTree(nodes[index]); if (owns(nodes[index], virtualTree, role, true)) { return true; } } return false; } function missingRequiredChildren(node, childRoles, all, role) { var index, length = childRoles.length, missing = [], ownedElements = idrefs(node, 'aria-owns'); for (index = 0; index < length; index++) { var childRole = childRoles[index]; if ( owns(node, virtualNode, childRole) || ariaOwns(ownedElements, childRole) ) { if (!all) { return null; } } else { if (all) { missing.push(childRole); } } } // combobox exceptions if (role === 'combobox') { // remove 'textbox' from missing roles if combobox is a native text-type input or owns a 'searchbox' var textboxIndex = missing.indexOf('textbox'); var textTypeInputs = ['text', 'search', 'email', 'url', 'tel']; if ( (textboxIndex >= 0 && (node.nodeName.toUpperCase() === 'INPUT' && textTypeInputs.includes(node.type))) || (owns(node, virtualNode, 'searchbox') || ariaOwns(ownedElements, 'searchbox')) ) { missing.splice(textboxIndex, 1); } // remove 'listbox' from missing roles if combobox is collapsed var listboxIndex = missing.indexOf('listbox'); var expanded = node.getAttribute('aria-expanded'); if (listboxIndex >= 0 && (!expanded || expanded === 'false')) { missing.splice(listboxIndex, 1); } } if (missing.length) { return missing; } if (!all && childRoles.length) { return childRoles; } 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); if (!required) { return true; } var all = false; var childRoles = required.one; if (!childRoles) { var all = true; childRoles = required.all; } var missing = missingRequiredChildren(node, childRoles, all, role); if (!missing) { return true; } 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) && !hasContentVirtual(virtualNode, false, true) && !hasDecendantWithRole(virtualNode) && idrefs(node, 'aria-owns').length === 0 ) { return undefined; } else { return false; }