/** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as css from '@isomorphic/cssTokenizer'; import { getGlobalOptions, closestCrossShadow, elementSafeTagName, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils'; import type { AriaRole } from '@isomorphic/ariaSnapshot'; function hasExplicitAccessibleName(e: Element) { return e.hasAttribute('aria-label') || e.hasAttribute('aria-labelledby'); } // https://www.w3.org/TR/wai-aria-practices/examples/landmarks/HTML5.html const kAncestorPreventingLandmark = 'article:not([role]), aside:not([role]), main:not([role]), nav:not([role]), section:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]'; // https://www.w3.org/TR/wai-aria-1.2/#global_states const kGlobalAriaAttributes: [string, string[] | undefined][] = [ ['aria-atomic', undefined], ['aria-busy', undefined], ['aria-controls', undefined], ['aria-current', undefined], ['aria-describedby', undefined], ['aria-details', undefined], // Global use deprecated in ARIA 1.2 // ['aria-disabled', undefined], ['aria-dropeffect', undefined], // Global use deprecated in ARIA 1.2 // ['aria-errormessage', undefined], ['aria-flowto', undefined], ['aria-grabbed', undefined], // Global use deprecated in ARIA 1.2 // ['aria-haspopup', undefined], ['aria-hidden', undefined], // Global use deprecated in ARIA 1.2 // ['aria-invalid', undefined], ['aria-keyshortcuts', undefined], ['aria-label', ['caption', 'code', 'deletion', 'emphasis', 'generic', 'insertion', 'paragraph', 'presentation', 'strong', 'subscript', 'superscript']], ['aria-labelledby', ['caption', 'code', 'deletion', 'emphasis', 'generic', 'insertion', 'paragraph', 'presentation', 'strong', 'subscript', 'superscript']], ['aria-live', undefined], ['aria-owns', undefined], ['aria-relevant', undefined], ['aria-roledescription', ['generic']], ]; function hasGlobalAriaAttribute(element: Element, forRole?: string | null) { return kGlobalAriaAttributes.some(([attr, prohibited]) => { return !prohibited?.includes(forRole || '') && element.hasAttribute(attr); }); } function hasTabIndex(element: Element) { return !Number.isNaN(Number(String(element.getAttribute('tabindex')))); } function isFocusable(element: Element) { // TODO: // - "inert" attribute makes the whole substree not focusable // - when dialog is open on the page - everything but the dialog is not focusable return !isNativelyDisabled(element) && (isNativelyFocusable(element) || hasTabIndex(element)); } function isNativelyFocusable(element: Element) { const tagName = elementSafeTagName(element); if (['BUTTON', 'DETAILS', 'SELECT', 'TEXTAREA'].includes(tagName)) return true; if (tagName === 'A' || tagName === 'AREA') return element.hasAttribute('href'); if (tagName === 'INPUT') return !(element as HTMLInputElement).hidden; return false; } // https://w3c.github.io/html-aam/#html-element-role-mappings // https://www.w3.org/TR/html-aria/#docconformance const kImplicitRoleByTagName: { [tagName: string]: (e: Element) => AriaRole | null } = { 'A': (e: Element) => { return e.hasAttribute('href') ? 'link' : null; }, 'AREA': (e: Element) => { return e.hasAttribute('href') ? 'link' : null; }, 'ARTICLE': () => 'article', 'ASIDE': () => 'complementary', 'BLOCKQUOTE': () => 'blockquote', 'BUTTON': () => 'button', 'CAPTION': () => 'caption', 'CODE': () => 'code', 'DATALIST': () => 'listbox', 'DD': () => 'definition', 'DEL': () => 'deletion', 'DETAILS': () => 'group', 'DFN': () => 'term', 'DIALOG': () => 'dialog', 'DT': () => 'term', 'EM': () => 'emphasis', 'FIELDSET': () => 'group', 'FIGURE': () => 'figure', 'FOOTER': (e: Element) => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : 'contentinfo', 'FORM': (e: Element) => hasExplicitAccessibleName(e) ? 'form' : null, 'H1': () => 'heading', 'H2': () => 'heading', 'H3': () => 'heading', 'H4': () => 'heading', 'H5': () => 'heading', 'H6': () => 'heading', 'HEADER': (e: Element) => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : 'banner', 'HR': () => 'separator', 'HTML': () => 'document', 'IMG': (e: Element) => (e.getAttribute('alt') === '') && !e.getAttribute('title') && !hasGlobalAriaAttribute(e) && !hasTabIndex(e) ? 'presentation' : 'img', 'INPUT': (e: Element) => { const type = (e as HTMLInputElement).type.toLowerCase(); if (type === 'search') return e.hasAttribute('list') ? 'combobox' : 'searchbox'; if (['email', 'tel', 'text', 'url', ''].includes(type)) { // https://html.spec.whatwg.org/multipage/input.html#concept-input-list const list = getIdRefs(e, e.getAttribute('list'))[0]; return (list && elementSafeTagName(list) === 'DATALIST') ? 'combobox' : 'textbox'; } if (type === 'hidden') return null; // File inputs do not have a role by the spec: https://www.w3.org/TR/html-aam-1.0/#el-input-file. // However, there are open issues about fixing it: https://github.com/w3c/aria/issues/1926. // All browsers report it as a button, and it is rendered as a button, so we do "button". if (type === 'file' && !getGlobalOptions().inputFileRoleTextbox) return 'button'; return inputTypeToRole[type] || 'textbox'; }, 'INS': () => 'insertion', 'LI': () => 'listitem', 'MAIN': () => 'main', 'MARK': () => 'mark', 'MATH': () => 'math', 'MENU': () => 'list', 'METER': () => 'meter', 'NAV': () => 'navigation', 'OL': () => 'list', 'OPTGROUP': () => 'group', 'OPTION': () => 'option', 'OUTPUT': () => 'status', 'P': () => 'paragraph', 'PROGRESS': () => 'progressbar', 'SECTION': (e: Element) => hasExplicitAccessibleName(e) ? 'region' : null, 'SELECT': (e: Element) => e.hasAttribute('multiple') || (e as HTMLSelectElement).size > 1 ? 'listbox' : 'combobox', 'STRONG': () => 'strong', 'SUB': () => 'subscript', 'SUP': () => 'superscript', // For we default to Chrome behavior: // - Chrome reports 'img'. // - Firefox reports 'diagram' that is not in official ARIA spec yet. // - Safari reports 'no role', but still computes accessible name. 'SVG': () => 'img', 'TABLE': () => 'table', 'TBODY': () => 'rowgroup', 'TD': (e: Element) => { const table = closestCrossShadow(e, 'table'); const role = table ? getExplicitAriaRole(table) : ''; return (role === 'grid' || role === 'treegrid') ? 'gridcell' : 'cell'; }, 'TEXTAREA': () => 'textbox', 'TFOOT': () => 'rowgroup', 'TH': (e: Element) => { if (e.getAttribute('scope') === 'col') return 'columnheader'; if (e.getAttribute('scope') === 'row') return 'rowheader'; const table = closestCrossShadow(e, 'table'); const role = table ? getExplicitAriaRole(table) : ''; return (role === 'grid' || role === 'treegrid') ? 'gridcell' : 'cell'; }, 'THEAD': () => 'rowgroup', 'TIME': () => 'time', 'TR': () => 'row', 'UL': () => 'list', }; const kPresentationInheritanceParents: { [tagName: string]: string[] } = { 'DD': ['DL', 'DIV'], 'DIV': ['DL'], 'DT': ['DL', 'DIV'], 'LI': ['OL', 'UL'], 'TBODY': ['TABLE'], 'TD': ['TR'], 'TFOOT': ['TABLE'], 'TH': ['TR'], 'THEAD': ['TABLE'], 'TR': ['THEAD', 'TBODY', 'TFOOT', 'TABLE'], }; function getImplicitAriaRole(element: Element): AriaRole | null { const implicitRole = kImplicitRoleByTagName[elementSafeTagName(element)]?.(element) || ''; if (!implicitRole) return null; // Inherit presentation role when required. // https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none let ancestor: Element | null = element; while (ancestor) { const parent = parentElementOrShadowHost(ancestor); const parents = kPresentationInheritanceParents[elementSafeTagName(ancestor)]; if (!parents || !parent || !parents.includes(elementSafeTagName(parent))) break; const parentExplicitRole = getExplicitAriaRole(parent); if ((parentExplicitRole === 'none' || parentExplicitRole === 'presentation') && !hasPresentationConflictResolution(parent, parentExplicitRole)) return parentExplicitRole; ancestor = parent; } return implicitRole; } const validRoles: AriaRole[] = ['alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox', 'complementary', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid', 'gridcell', 'group', 'heading', 'img', 'insertion', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'mark', 'marquee', 'math', 'meter', 'menu', 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'navigation', 'none', 'note', 'option', 'paragraph', 'presentation', 'progressbar', 'radio', 'radiogroup', 'region', 'row', 'rowgroup', 'rowheader', 'scrollbar', 'search', 'searchbox', 'separator', 'slider', 'spinbutton', 'status', 'strong', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer', 'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem']; function getExplicitAriaRole(element: Element): AriaRole | null { // https://www.w3.org/TR/wai-aria-1.2/#document-handling_author-errors_roles const roles = (element.getAttribute('role') || '').split(' ').map(role => role.trim()); return roles.find(role => validRoles.includes(role as any)) as AriaRole || null; } function hasPresentationConflictResolution(element: Element, role: string | null) { // https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none return hasGlobalAriaAttribute(element, role) || isFocusable(element); } export function getAriaRole(element: Element): AriaRole | null { const explicitRole = getExplicitAriaRole(element); if (!explicitRole) return getImplicitAriaRole(element); if (explicitRole === 'none' || explicitRole === 'presentation') { const implicitRole = getImplicitAriaRole(element); if (hasPresentationConflictResolution(element, implicitRole)) return implicitRole; } return explicitRole; } function getAriaBoolean(attr: string | null) { return attr === null ? undefined : attr.toLowerCase() === 'true'; } export function isElementIgnoredForAria(element: Element) { return ['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element)); } // https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion, but including "none" and "presentation" roles // Not implemented: // `Any descendants of elements that have the characteristic "Children Presentational: True"` // https://www.w3.org/TR/wai-aria-1.2/#aria-hidden export function isElementHiddenForAria(element: Element): boolean { if (isElementIgnoredForAria(element)) return true; const style = getElementComputedStyle(element); const isSlot = element.nodeName === 'SLOT'; if (style?.display === 'contents' && !isSlot) { // display:contents is not rendered itself, but its child nodes are. for (let child = element.firstChild; child; child = child.nextSibling) { if (child.nodeType === 1 /* Node.ELEMENT_NODE */ && !isElementHiddenForAria(child as Element)) return false; if (child.nodeType === 3 /* Node.TEXT_NODE */ && isVisibleTextNode(child as Text)) return false; } return true; } // Note: , but all browsers actually support it. const summary = element.getAttribute('summary') || ''; if (summary) return summary; // SPEC DIFFERENCE. // Spec says "if the table element has a title attribute, then use that attribute". // We ignore title to pass "name_from_content-manual.html". } // https://w3c.github.io/html-aam/#area-element if (tagName === 'AREA') { options.visitedElements.add(element); const alt = element.getAttribute('alt') || ''; if (trimFlatString(alt)) return alt; const title = element.getAttribute('title') || ''; return title; } // https://www.w3.org/TR/svg-aam-1.0/#mapping_additional_nd if (tagName === 'SVG' || (element as SVGElement).ownerSVGElement) { options.visitedElements.add(element); for (let child = element.firstElementChild; child; child = child.nextElementSibling) { if (elementSafeTagName(child) === 'TITLE' && (child as SVGElement).ownerSVGElement) { return getTextAlternativeInternal(child, { ...childOptions, embeddedInLabelledBy: { element: child, hidden: isElementHiddenForAria(child) }, }); } } } if ((element as SVGElement).ownerSVGElement && tagName === 'A') { const title = element.getAttribute('xlink:title') || ''; if (trimFlatString(title)) { options.visitedElements.add(element); return title; } } } // See https://w3c.github.io/html-aam/#summary-element-accessible-name-computation for "summary"-specific check. const shouldNameFromContentForSummary = tagName === 'SUMMARY' && !['presentation', 'none'].includes(role); // step 2f + step 2h. if (allowsNameFromContent(role, options.embeddedInTargetElement === 'descendant') || shouldNameFromContentForSummary || !!options.embeddedInLabelledBy || !!options.embeddedInDescribedBy || !!options.embeddedInLabel || !!options.embeddedInNativeTextAlternative) { options.visitedElements.add(element); const accessibleName = innerAccumulatedElementText(element, childOptions); // Spec says "Return the accumulated text if it is not the empty string". However, that is not really // compatible with the real browser behavior and wpt tests, where an element with empty contents will fallback to the title. // So we follow the spec everywhere except for the target element itself. This can probably be improved. const maybeTrimmedAccessibleName = options.embeddedInTargetElement === 'self' ? trimFlatString(accessibleName) : accessibleName; if (maybeTrimmedAccessibleName) return accessibleName; } // step 2i. if (!['presentation', 'none'].includes(role) || tagName === 'IFRAME') { options.visitedElements.add(element); const title = element.getAttribute('title') || ''; if (trimFlatString(title)) return title; } options.visitedElements.add(element); return ''; } function innerAccumulatedElementText(element: Element, options: AccessibleNameOptions): string { const tokens: string[] = []; const visit = (node: Node, skipSlotted: boolean) => { if (skipSlotted && (node as Element | Text).assignedSlot) return; if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { const display = getElementComputedStyle(node as Element)?.display || 'inline'; let token = getTextAlternativeInternal(node as Element, options); // SPEC DIFFERENCE. // Spec says "append the result to the accumulated text", assuming "with space". // However, multiple tests insist that inline elements do not add a space. // Additionally,
insists on a space anyway, see "name_file-label-inline-block-elements-manual.html" if (display !== 'inline' || node.nodeName === 'BR') token = ' ' + token + ' '; tokens.push(token); } else if (node.nodeType === 3 /* Node.TEXT_NODE */) { // step 2g. tokens.push(node.textContent || ''); } }; tokens.push(getCSSContent(element, '::before') || ''); const content = getCSSContent(element); if (content !== undefined) { // `content` CSS property replaces everything inside the element. // I was not able to find any spec or description on how this interacts with accname, // so this is a guess based on what browsers do. tokens.push(content); } else { // step 2h. const assignedNodes = element.nodeName === 'SLOT' ? (element as HTMLSlotElement).assignedNodes() : []; if (assignedNodes.length) { for (const child of assignedNodes) visit(child, false); } else { for (let child = element.firstChild; child; child = child.nextSibling) visit(child, true); if (element.shadowRoot) { for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) visit(child, true); } for (const owned of getIdRefs(element, element.getAttribute('aria-owns'))) visit(owned, true); } } tokens.push(getCSSContent(element, '::after') || ''); return tokens.join(''); } export const kAriaSelectedRoles = ['gridcell', 'option', 'row', 'tab', 'rowheader', 'columnheader', 'treeitem']; export function getAriaSelected(element: Element): boolean { // https://www.w3.org/TR/wai-aria-1.2/#aria-selected // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings if (elementSafeTagName(element) === 'OPTION') return (element as HTMLOptionElement).selected; if (kAriaSelectedRoles.includes(getAriaRole(element) || '')) return getAriaBoolean(element.getAttribute('aria-selected')) === true; return false; } export const kAriaCheckedRoles = ['checkbox', 'menuitemcheckbox', 'option', 'radio', 'switch', 'menuitemradio', 'treeitem']; export function getAriaChecked(element: Element): boolean | 'mixed' { const result = getChecked(element, true); return result === 'error' ? false : result; } export function getCheckedAllowMixed(element: Element): boolean | 'mixed' | 'error' { return getChecked(element, true); } export function getCheckedWithoutMixed(element: Element): boolean | 'error' { const result = getChecked(element, false); return result as boolean | 'error'; } function getChecked(element: Element, allowMixed: boolean): boolean | 'mixed' | 'error' { const tagName = elementSafeTagName(element); // https://www.w3.org/TR/wai-aria-1.2/#aria-checked // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings if (allowMixed && tagName === 'INPUT' && (element as HTMLInputElement).indeterminate) return 'mixed'; if (tagName === 'INPUT' && ['checkbox', 'radio'].includes((element as HTMLInputElement).type)) return (element as HTMLInputElement).checked; if (kAriaCheckedRoles.includes(getAriaRole(element) || '')) { const checked = element.getAttribute('aria-checked'); if (checked === 'true') return true; if (allowMixed && checked === 'mixed') return 'mixed'; return false; } return 'error'; } // https://w3c.github.io/aria/#aria-readonly const kAriaReadonlyRoles = ['checkbox', 'combobox', 'grid', 'gridcell', 'listbox', 'radiogroup', 'slider', 'spinbutton', 'textbox', 'columnheader', 'rowheader', 'searchbox', 'switch', 'treegrid']; export function getReadonly(element: Element): boolean | 'error' { const tagName = elementSafeTagName(element); // https://www.w3.org/TR/wai-aria-1.2/#aria-checked // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tagName)) return element.hasAttribute('readonly'); if (kAriaReadonlyRoles.includes(getAriaRole(element) || '')) return element.getAttribute('aria-readonly') === 'true'; if ((element as HTMLElement).isContentEditable) return false; return 'error'; } export const kAriaPressedRoles = ['button']; export function getAriaPressed(element: Element): boolean | 'mixed' { // https://www.w3.org/TR/wai-aria-1.2/#aria-pressed if (kAriaPressedRoles.includes(getAriaRole(element) || '')) { const pressed = element.getAttribute('aria-pressed'); if (pressed === 'true') return true; if (pressed === 'mixed') return 'mixed'; } return false; } export const kAriaExpandedRoles = ['application', 'button', 'checkbox', 'combobox', 'gridcell', 'link', 'listbox', 'menuitem', 'row', 'rowheader', 'tab', 'treeitem', 'columnheader', 'menuitemcheckbox', 'menuitemradio', 'rowheader', 'switch']; export function getAriaExpanded(element: Element): boolean | undefined { // https://www.w3.org/TR/wai-aria-1.2/#aria-expanded // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings if (elementSafeTagName(element) === 'DETAILS') return (element as HTMLDetailsElement).open; if (kAriaExpandedRoles.includes(getAriaRole(element) || '')) { const expanded = element.getAttribute('aria-expanded'); if (expanded === null) return undefined; if (expanded === 'true') return true; return false; } return undefined; } export const kAriaLevelRoles = ['heading', 'listitem', 'row', 'treeitem']; export function getAriaLevel(element: Element): number { // https://www.w3.org/TR/wai-aria-1.2/#aria-level // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings const native = { 'H1': 1, 'H2': 2, 'H3': 3, 'H4': 4, 'H5': 5, 'H6': 6 }[elementSafeTagName(element)]; if (native) return native; if (kAriaLevelRoles.includes(getAriaRole(element) || '')) { const attr = element.getAttribute('aria-level'); const value = attr === null ? Number.NaN : Number(attr); if (Number.isInteger(value) && value >= 1) return value; } return 0; } export const kAriaDisabledRoles = ['application', 'button', 'composite', 'gridcell', 'group', 'input', 'link', 'menuitem', 'scrollbar', 'separator', 'tab', 'checkbox', 'columnheader', 'combobox', 'grid', 'listbox', 'menu', 'menubar', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'radiogroup', 'row', 'rowheader', 'searchbox', 'select', 'slider', 'spinbutton', 'switch', 'tablist', 'textbox', 'toolbar', 'tree', 'treegrid', 'treeitem']; export function getAriaDisabled(element: Element): boolean { // https://www.w3.org/TR/wai-aria-1.2/#aria-disabled // Note that aria-disabled applies to all descendants, so we look up the hierarchy. return isNativelyDisabled(element) || hasExplicitAriaDisabled(element); } function isNativelyDisabled(element: Element) { // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings const isNativeFormControl = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'].includes(element.tagName); return isNativeFormControl && (element.hasAttribute('disabled') || belongsToDisabledFieldSet(element)); } function belongsToDisabledFieldSet(element: Element): boolean { const fieldSetElement = element?.closest('FIELDSET[DISABLED]'); if (!fieldSetElement) return false; const legendElement = fieldSetElement.querySelector(':scope > LEGEND'); return !legendElement || !legendElement.contains(element); } function hasExplicitAriaDisabled(element: Element | undefined, isAncestor = false): boolean { if (!element) return false; if (isAncestor || kAriaDisabledRoles.includes(getAriaRole(element) || '')) { const attribute = (element.getAttribute('aria-disabled') || '').toLowerCase(); if (attribute === 'true') return true; if (attribute === 'false') return false; // aria-disabled works across shadow boundaries. return hasExplicitAriaDisabled(parentElementOrShadowHost(element), true); } return false; } function getAccessibleNameFromAssociatedLabels(labels: Iterable, options: AccessibleNameOptions) { return [...labels].map(label => getTextAlternativeInternal(label, { ...options, embeddedInLabel: { element: label, hidden: isElementHiddenForAria(label) }, embeddedInNativeTextAlternative: undefined, embeddedInLabelledBy: undefined, embeddedInDescribedBy: undefined, embeddedInTargetElement: undefined, })).filter(accessibleName => !!accessibleName).join(' '); } export function receivesPointerEvents(element: Element): boolean { const cache = cachePointerEvents!; let e: Element | undefined = element; let result: boolean | undefined; const parents: Element[] = []; for (; e; e = parentElementOrShadowHost(e!)) { const cached = cache.get(e); if (cached !== undefined) { result = cached; break; } parents.push(e); const style = getElementComputedStyle(e); if (!style) { result = true; break; } const value = style.pointerEvents; if (value) { result = value !== 'none'; break; } } if (result === undefined) result = true; for (const parent of parents) cache.set(parent, result); return result; } let cacheAccessibleName: Map | undefined; let cacheAccessibleNameHidden: Map | undefined; let cacheAccessibleDescription: Map | undefined; let cacheAccessibleDescriptionHidden: Map | undefined; let cacheAccessibleErrorMessage: Map | undefined; let cacheIsHidden: Map | undefined; let cachePseudoContent: Map | undefined; let cachePseudoContentBefore: Map | undefined; let cachePseudoContentAfter: Map | undefined; let cachePointerEvents: Map | undefined; let cachesCounter = 0; export function beginAriaCaches() { ++cachesCounter; cacheAccessibleName ??= new Map(); cacheAccessibleNameHidden ??= new Map(); cacheAccessibleDescription ??= new Map(); cacheAccessibleDescriptionHidden ??= new Map(); cacheAccessibleErrorMessage ??= new Map(); cacheIsHidden ??= new Map(); cachePseudoContent ??= new Map(); cachePseudoContentBefore ??= new Map(); cachePseudoContentAfter ??= new Map(); cachePointerEvents ??= new Map(); } export function endAriaCaches() { if (!--cachesCounter) { cacheAccessibleName = undefined; cacheAccessibleNameHidden = undefined; cacheAccessibleDescription = undefined; cacheAccessibleDescriptionHidden = undefined; cacheAccessibleErrorMessage = undefined; cacheIsHidden = undefined; cachePseudoContent = undefined; cachePseudoContentBefore = undefined; cachePseudoContentAfter = undefined; cachePointerEvents = undefined; } } const inputTypeToRole: Record = { 'button': 'button', 'checkbox': 'checkbox', 'image': 'button', 'number': 'spinbutton', 'radio': 'radio', 'range': 'slider', 'reset': 'button', 'submit': 'button', };