UNPKG

@base-ui/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

159 lines 5.42 kB
// Modified to add conditional `aria-hidden` support: // https://github.com/theKashey/aria-hidden/blob/9220c8f4a4fd35f63bee5510a9f41a37264382d4/src/index.ts import { getNodeName, isShadowRoot } from '@floating-ui/utils/dom'; import { ownerDocument } from '@base-ui/utils/owner'; const counters = { inert: new WeakMap(), 'aria-hidden': new WeakMap() }; const markerName = 'data-base-ui-inert'; const uncontrolledElementsSets = { inert: new WeakSet(), 'aria-hidden': new WeakSet() }; let markerCounterMap = new WeakMap(); let lockCount = 0; function getUncontrolledElementsSet(controlAttribute) { return uncontrolledElementsSets[controlAttribute]; } export const supportsInert = () => typeof HTMLElement !== 'undefined' && 'inert' in HTMLElement.prototype; function unwrapHost(node) { if (!node) { return null; } return isShadowRoot(node) ? node.host : unwrapHost(node.parentNode); } const correctElements = (parent, targets) => targets.map(target => { if (parent.contains(target)) { return target; } const correctedTarget = unwrapHost(target); if (parent.contains(correctedTarget)) { return correctedTarget; } return null; }).filter(x => x != null); const buildKeepSet = targets => { const keep = new Set(); targets.forEach(target => { let node = target; while (node && !keep.has(node)) { keep.add(node); node = node.parentNode; } }); return keep; }; const collectOutsideElements = (root, keepElements, stopElements) => { const outside = []; const walk = parent => { if (!parent || stopElements.has(parent)) { return; } Array.from(parent.children).forEach(node => { if (getNodeName(node) === 'script') { return; } if (keepElements.has(node)) { walk(node); } else { outside.push(node); } }); }; walk(root); return outside; }; function applyAttributeToOthers(uncorrectedAvoidElements, body, ariaHidden, inert, { mark = true, markerIgnoreElements = [] }) { // eslint-disable-next-line no-nested-ternary const controlAttribute = inert ? 'inert' : ariaHidden ? 'aria-hidden' : null; let counterMap = null; let uncontrolledElementsSet = null; const avoidElements = correctElements(body, uncorrectedAvoidElements); const markerIgnoreTargets = mark ? correctElements(body, markerIgnoreElements) : []; const markerIgnoreSet = new Set(markerIgnoreTargets); const markerTargets = mark ? collectOutsideElements(body, buildKeepSet(avoidElements), new Set(avoidElements)).filter(target => !markerIgnoreSet.has(target)) : []; const hiddenElements = []; const markedElements = []; if (controlAttribute) { const map = counters[controlAttribute]; const currentUncontrolledElementsSet = getUncontrolledElementsSet(controlAttribute); uncontrolledElementsSet = currentUncontrolledElementsSet; counterMap = map; const ariaLiveElements = correctElements(body, Array.from(body.querySelectorAll('[aria-live]'))); const controlElements = avoidElements.concat(ariaLiveElements); const controlTargets = collectOutsideElements(body, buildKeepSet(controlElements), new Set(controlElements)); controlTargets.forEach(node => { const attr = node.getAttribute(controlAttribute); const alreadyHidden = attr !== null && attr !== 'false'; const counterValue = (map.get(node) || 0) + 1; map.set(node, counterValue); hiddenElements.push(node); if (counterValue === 1 && alreadyHidden) { currentUncontrolledElementsSet.add(node); } if (!alreadyHidden) { node.setAttribute(controlAttribute, controlAttribute === 'inert' ? '' : 'true'); } }); } if (mark) { markerTargets.forEach(node => { const markerValue = (markerCounterMap.get(node) || 0) + 1; markerCounterMap.set(node, markerValue); markedElements.push(node); if (markerValue === 1) { node.setAttribute(markerName, ''); } }); } lockCount += 1; return () => { if (counterMap) { hiddenElements.forEach(element => { const currentCounterValue = counterMap.get(element) || 0; const counterValue = currentCounterValue - 1; counterMap.set(element, counterValue); if (!counterValue) { if (!uncontrolledElementsSet?.has(element) && controlAttribute) { element.removeAttribute(controlAttribute); } uncontrolledElementsSet?.delete(element); } }); } if (mark) { markedElements.forEach(element => { const markerValue = (markerCounterMap.get(element) || 0) - 1; markerCounterMap.set(element, markerValue); if (!markerValue) { element.removeAttribute(markerName); } }); } lockCount -= 1; if (!lockCount) { counters.inert = new WeakMap(); counters['aria-hidden'] = new WeakMap(); uncontrolledElementsSets.inert = new WeakSet(); uncontrolledElementsSets['aria-hidden'] = new WeakSet(); markerCounterMap = new WeakMap(); } }; } export function markOthers(avoidElements, options = {}) { const { ariaHidden = false, inert = false, mark = true, markerIgnoreElements = [] } = options; const body = ownerDocument(avoidElements[0]).body; return applyAttributeToOthers(avoidElements, body, ariaHidden, inert, { mark, markerIgnoreElements }); }