scrollable-region-focusable-matches.js 1.79 KB
import { hasContentVirtual } from '../commons/dom';
import { getExplicitRole } from '../commons/aria';
import standards from '../standards';
import {
	querySelectorAll,
	getScroll,
	closest,
	getRootNode,
	tokenList
} from '../core/utils';

function scrollableRegionFocusableMatches(node, virtualNode) {
	/**
	 * Note:
	 * `excludeHidden=true` for this rule, thus considering only elements in the accessibility tree.
	 */

	/**
	 * if not scrollable -> `return`
	 */
	if (!!getScroll(node, 13) === false) {
		return false;
	}

	/**
	 * ignore scrollable regions owned by combobox. limit to roles
	 * ownable by combobox so we don't keep calling closest for every
	 * node (which would be slow)
	 * @see https://github.com/dequelabs/axe-core/issues/1763
	 */
	const role = getExplicitRole(virtualNode);
	if (standards.ariaRoles.combobox.requiredOwned.includes(role)) {
		// in ARIA 1.1 the container has role=combobox
		if (closest(virtualNode, '[role~="combobox"]')) {
			return false;
		}

		// in ARIA 1.0 and 1.2 the combobox owns (1.0) or controls (1.2)
		// the listbox
		const id = virtualNode.attr('id');
		if (id) {
			const doc = getRootNode(node);
			const owned = Array.from(
				doc.querySelectorAll(`[aria-owns~="${id}"], [aria-controls~="${id}"]`)
			);
			const comboboxOwned = owned.some(el => {
				const roles = tokenList(el.getAttribute('role'));
				return roles.includes('combobox');
			});

			if (comboboxOwned) {
				return false;
			}
		}
	}

	/**
	 * check if node has visible contents
	 */
	const nodeAndDescendents = querySelectorAll(virtualNode, '*');
	const hasVisibleChildren = nodeAndDescendents.some(elm =>
		hasContentVirtual(
			elm,
			true, // noRecursion
			true // ignoreAria
		)
	);
	if (!hasVisibleChildren) {
		return false;
	}

	return true;
}

export default scrollableRegionFocusableMatches;