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