UNPKG

@markuplint/ml-spec

Version:

Types and schema that specs of the Markup languages for markuplint

211 lines (210 loc) 7.39 kB
import { ariaSpecs } from '../specs/aria-specs.js'; import { getSpecByTagName } from '../specs/get-spec-by-tag-name.js'; import { isPresentational } from '../specs/is-presentational.js'; import { resolveNamespace } from '../utils/resolve-namespace.js'; import { getComputedRole } from './get-computed-role.js'; /** * Detect including/excluding from the Accessibility Tree * * @see https://www.w3.org/TR/wai-aria-1.2/#accessibility_tree * * @param specs * @param el * @param version */ export function isExposed( // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types el, specs, version) { // According to WAI-ARIA if (isExcluding(el, specs, version)) { return false; } // Return true if the element is unknown, deprecated, or obsolete. const { localName, namespace } = resolveNamespace(el.localName, el.namespaceURI); const spec = getSpecByTagName(specs.specs, localName, namespace); if (!spec || spec.deprecated || spec.obsolete != null) { return true; } // According to HTML and SVG Specs with **the author's interpretation** { if (!isExposedElement(el, specs)) { return false; } } // According to WAI-ARIA { const exposable = isIncluding(el, specs, version); if (exposable) { return true; } } // Default return true; } /** * Excluding Elements from the Accessibility Tree * * @see https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion * * @param specs * @param el * @param version */ function isExcluding( // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types el, specs, version) { /** * The following elements are not exposed via the accessibility API and * user agents MUST NOT include them in the accessibility tree: * - Elements, including their descendent elements, * that have host language semantics specifying * that the element is not displayed, such as CSS display:none, * visibility:hidden, or the HTML hidden attribute. */ { let currentEl = el; while (currentEl) { if (hasDisplayNodeOrVisibilityHidden(currentEl) || currentEl.hasAttribute('hidden')) { return true; } currentEl = currentEl.parentElement; } } /** * - Elements with none or presentation as the first role in the role attribute. * However, their exclusion is conditional. In addition, * the element's descendants and text content are generally included. * These exceptions and conditions are documented in the presentation (role) * section. */ if (isPresentational((el.getAttribute('role') ?? '').split(/\s+/)[0])) { return true; } /** * If not already excluded from the accessibility tree per the above rules, * user agents SHOULD NOT include the following elements * in the accessibility tree: * - Elements, including their descendants, that have aria-hidden set to true. * In other words, aria-hidden="true" on a parent overrides aria-hidden="false" * on descendants. */ { let currentEl = el; while (currentEl) { if (currentEl.getAttribute('aria-hidden') === 'true') { return true; } currentEl = currentEl.parentElement; } } /** * - Any descendants of elements that have the characteristic * "Children Presentational: True" unless the descendant * is not allowed to be presentational because it meets one of * the conditions for exception described in * Presentational Roles Conflict Resolution. * However, the text content of any excluded descendants is included. */ { let currentEl = el.parentElement; while (currentEl) { const { role } = getComputedRole(specs, currentEl, version); if (role?.childrenPresentational) { return true; } currentEl = currentEl.parentElement; } } return false; } /** * Including Elements in the Accessibility Tree * * If not excluded from or marked as hidden in the accessibility tree * per the rules above in Excluding Elements in the Accessibility Tree, * user agents MUST provide an accessible object in the accessibility tree * for DOM elements that meet any of the following criteria: * * @see https://www.w3.org/TR/wai-aria-1.2/#tree_inclusion * * @param specs * @param el * @param version */ function isIncluding( // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types el, specs, version) { /** * > **meet any** of the following criteria: */ const results = []; /** * Elements that are not hidden and may fire an accessibility API event, * including: * - Elements that are currently focused, even if the element or one of * its ancestor elements has its aria-hidden attribute set to true. */ // 🚫 Can't detect the element is focused. // results.push(true); /** * - Elements that are a valid target of an aria-activedescendant attribute. */ // TODO: Compute aria-activedescendant. // results.push(true); /** * Elements that have an explicit role or a global WAI-ARIA attribute and * do not have aria-hidden set to true. * (See Excluding Elements in the Accessibility Tree for * additional guidance on aria-hidden.) */ if (el.getAttribute('aria-hidden') !== 'true') { const globalAria = ariaSpecs(specs, version).props.filter(prop => prop.isGlobal); const { role } = getComputedRole(specs, el, version); // Has an explicit role if (role && !role.isImplicit) { results.push(true); } // Has a global WAI-ARIA attribute for (const attr of el.attributes) { if (globalAria.some(aria => aria.name === attr.localName)) { results.push(true); break; } } } /** * Elements that are not hidden and have an ID that is referenced * by another element via a WAI-ARIA property. */ // TODO: Compute refering ID. // results.push(true); return results.includes(true); } function hasDisplayNodeOrVisibilityHidden( // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types el) { const style = el.getAttribute('style'); if (!style) { return false; } // TODO: Improve accuracy return /display\s*:\s*none|visibility\s*:\s*hidden/i.test(style); } function isExposedElement( // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types el, specs) { const svgRenderedConditions = specs.def['#contentModels']['#SVGRenderable']?.join(','); if (svgRenderedConditions && el.matches(svgRenderedConditions)) { return true; } return isNotMetaOrHiddenHTMLElement(el, specs); } function isNotMetaOrHiddenHTMLElement( // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types el, specs) { const metadataConditions = specs.def['#contentModels']['#metadata']?.join(','); if (metadataConditions && el.matches(metadataConditions)) { return false; } return !el.matches('input[type=hidden i]'); }