"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getRoles = getRoles; exports.getImplicitAriaRoles = getImplicitAriaRoles; exports.isSubtreeInaccessible = isSubtreeInaccessible; exports.prettyRoles = prettyRoles; exports.isInaccessible = isInaccessible; exports.computeAriaSelected = computeAriaSelected; exports.computeAriaChecked = computeAriaChecked; exports.computeAriaPressed = computeAriaPressed; exports.computeAriaExpanded = computeAriaExpanded; exports.computeHeadingLevel = computeHeadingLevel; exports.logRoles = void 0; var _ariaQuery = require("aria-query"); var _domAccessibilityApi = require("dom-accessibility-api"); var _prettyDom = require("./pretty-dom"); var _config = require("./config"); const elementRoleList = buildElementRoleList(_ariaQuery.elementRoles); /** * @param {Element} element - * @returns {boolean} - `true` if `element` and its subtree are inaccessible */ function isSubtreeInaccessible(element) { if (element.hidden === true) { return true; } if (element.getAttribute('aria-hidden') === 'true') { return true; } const window = element.ownerDocument.defaultView; if (window.getComputedStyle(element).display === 'none') { return true; } return false; } /** * Partial implementation https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion * which should only be used for elements with a non-presentational role i.e. * `role="none"` and `role="presentation"` will not be excluded. * * Implements aria-hidden semantics (i.e. parent overrides child) * Ignores "Child Presentational: True" characteristics * * @param {Element} element - * @param {object} [options] - * @param {function (element: Element): boolean} options.isSubtreeInaccessible - * can be used to return cached results from previous isSubtreeInaccessible calls * @returns {boolean} true if excluded, otherwise false */ function isInaccessible(element, options = {}) { const { isSubtreeInaccessible: isSubtreeInaccessibleImpl = isSubtreeInaccessible } = options; const window = element.ownerDocument.defaultView; // since visibility is inherited we can exit early if (window.getComputedStyle(element).visibility === 'hidden') { return true; } let currentElement = element; while (currentElement) { if (isSubtreeInaccessibleImpl(currentElement)) { return true; } currentElement = currentElement.parentElement; } return false; } function getImplicitAriaRoles(currentNode) { // eslint bug here: // eslint-disable-next-line no-unused-vars for (const { match, roles } of elementRoleList) { if (match(currentNode)) { return [...roles]; } } return []; } function buildElementRoleList(elementRolesMap) { function makeElementSelector({ name, attributes }) { return `${name}${attributes.map(({ name: attributeName, value, constraints = [] }) => { const shouldNotExist = constraints.indexOf('undefined') !== -1; if (shouldNotExist) { return `:not([${attributeName}])`; } else if (value) { return `[${attributeName}="${value}"]`; } else { return `[${attributeName}]`; } }).join('')}`; } function getSelectorSpecificity({ attributes = [] }) { return attributes.length; } function bySelectorSpecificity({ specificity: leftSpecificity }, { specificity: rightSpecificity }) { return rightSpecificity - leftSpecificity; } function match(element) { return node => { let { attributes = [] } = element; // https://github.com/testing-library/dom-testing-library/issues/814 const typeTextIndex = attributes.findIndex(attribute => attribute.value && attribute.name === 'type' && attribute.value === 'text'); if (typeTextIndex >= 0) { // not using splice to not mutate the attributes array attributes = [...attributes.slice(0, typeTextIndex), ...attributes.slice(typeTextIndex + 1)]; if (node.type !== 'text') { return false; } } return node.matches(makeElementSelector({ ...element, attributes })); }; } let result = []; // eslint bug here: // eslint-disable-next-line no-unused-vars for (const [element, roles] of elementRolesMap.entries()) { result = [...result, { match: match(element), roles: Array.from(roles), specificity: getSelectorSpecificity(element) }]; } return result.sort(bySelectorSpecificity); } function getRoles(container, { hidden = false } = {}) { function flattenDOM(node) { return [node, ...Array.from(node.children).reduce((acc, child) => [...acc, ...flattenDOM(child)], [])]; } return flattenDOM(container).filter(element => { return hidden === false ? isInaccessible(element) === false : true; }).reduce((acc, node) => { let roles = []; // TODO: This violates html-aria which does not allow any role on every element if (node.hasAttribute('role')) { roles = node.getAttribute('role').split(' ').slice(0, 1); } else { roles = getImplicitAriaRoles(node); } return roles.reduce((rolesAcc, role) => Array.isArray(rolesAcc[role]) ? { ...rolesAcc, [role]: [...rolesAcc[role], node] } : { ...rolesAcc, [role]: [node] }, acc); }, {}); } function prettyRoles(dom, { hidden }) { const roles = getRoles(dom, { hidden }); // We prefer to skip generic role, we don't recommend it return Object.entries(roles).filter(([role]) => role !== 'generic').map(([role, elements]) => { const delimiterBar = '-'.repeat(50); const elementsString = elements.map(el => { const nameString = `Name "${(0, _domAccessibilityApi.computeAccessibleName)(el, { computedStyleSupportsPseudoElements: (0, _config.getConfig)().computedStyleSupportsPseudoElements })}":\n`; const domString = (0, _prettyDom.prettyDOM)(el.cloneNode(false)); return `${nameString}${domString}`; }).join('\n\n'); return `${role}:\n\n${elementsString}\n\n${delimiterBar}`; }).join('\n'); } const logRoles = (dom, { hidden = false } = {}) => console.log(prettyRoles(dom, { hidden })); /** * @param {Element} element - * @returns {boolean | undefined} - false/true if (not)selected, undefined if not selectable */ exports.logRoles = logRoles; function computeAriaSelected(element) { // implicit value from html-aam mappings: https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings // https://www.w3.org/TR/html-aam-1.0/#details-id-97 if (element.tagName === 'OPTION') { return element.selected; } // explicit value return checkBooleanAttribute(element, 'aria-selected'); } /** * @param {Element} element - * @returns {boolean | undefined} - false/true if (not)checked, undefined if not checked-able */ function computeAriaChecked(element) { // implicit value from html-aam mappings: https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings // https://www.w3.org/TR/html-aam-1.0/#details-id-56 // https://www.w3.org/TR/html-aam-1.0/#details-id-67 if ('indeterminate' in element && element.indeterminate) { return undefined; } if ('checked' in element) { return element.checked; } // explicit value return checkBooleanAttribute(element, 'aria-checked'); } /** * @param {Element} element - * @returns {boolean | undefined} - false/true if (not)pressed, undefined if not press-able */ function computeAriaPressed(element) { // https://www.w3.org/TR/wai-aria-1.1/#aria-pressed return checkBooleanAttribute(element, 'aria-pressed'); } /** * @param {Element} element - * @returns {boolean | undefined} - false/true if (not)expanded, undefined if not expand-able */ function computeAriaExpanded(element) { // https://www.w3.org/TR/wai-aria-1.1/#aria-expanded return checkBooleanAttribute(element, 'aria-expanded'); } function checkBooleanAttribute(element, attribute) { const attributeValue = element.getAttribute(attribute); if (attributeValue === 'true') { return true; } if (attributeValue === 'false') { return false; } return undefined; } /** * @param {Element} element - * @returns {number | undefined} - number if implicit heading or aria-level present, otherwise undefined */ function computeHeadingLevel(element) { // https://w3c.github.io/html-aam/#el-h1-h6 // https://w3c.github.io/html-aam/#el-h1-h6 const implicitHeadingLevels = { H1: 1, H2: 2, H3: 3, H4: 4, H5: 5, H6: 6 }; // explicit aria-level value // https://www.w3.org/TR/wai-aria-1.2/#aria-level const ariaLevelAttribute = element.getAttribute('aria-level') && Number(element.getAttribute('aria-level')); return ariaLevelAttribute || implicitHeadingLevels[element.tagName]; }