UNPKG

@zag-js/aria-hidden

Version:

Hide targets from screen readers

134 lines (130 loc) 4.61 kB
// src/walk-tree-outside.ts var counterMap = /* @__PURE__ */ new WeakMap(); var uncontrolledNodes = /* @__PURE__ */ new WeakMap(); var markerMap = {}; var lockCount = 0; var unwrapHost = (node) => node && (node.host || unwrapHost(node.parentNode)); var correctTargets = (parent, targets) => targets.map((target) => { if (parent.contains(target)) return target; const correctedTarget = unwrapHost(target); if (correctedTarget && parent.contains(correctedTarget)) { return correctedTarget; } console.error("[zag-js > ariaHidden] target", target, "in not contained inside", parent, ". Doing nothing"); return null; }).filter((x) => Boolean(x)); var ignoreableNodes = /* @__PURE__ */ new Set(["script", "output", "status", "next-route-announcer"]); var isIgnoredNode = (node) => { if (ignoreableNodes.has(node.localName)) return true; if (node.role === "status") return true; if (node.hasAttribute("aria-live")) return true; return node.matches("[data-live-announcer]"); }; var walkTreeOutside = (originalTarget, props) => { const { parentNode, markerName, controlAttribute} = props; const targets = correctTargets(parentNode, Array.isArray(originalTarget) ? originalTarget : [originalTarget]); markerMap[markerName] || (markerMap[markerName] = /* @__PURE__ */ new WeakMap()); const markerCounter = markerMap[markerName]; const hiddenNodes = []; const elementsToKeep = /* @__PURE__ */ new Set(); const elementsToStop = new Set(targets); const keep = (el) => { if (!el || elementsToKeep.has(el)) return; elementsToKeep.add(el); keep(el.parentNode); }; targets.forEach(keep); const deep = (parent) => { if (!parent || elementsToStop.has(parent)) { return; } Array.prototype.forEach.call(parent.children, (node) => { if (elementsToKeep.has(node)) { deep(node); } else { try { if (isIgnoredNode(node)) return; const attr = node.getAttribute(controlAttribute); const alreadyHidden = attr === "true" ; const counterValue = (counterMap.get(node) || 0) + 1; const markerValue = (markerCounter.get(node) || 0) + 1; counterMap.set(node, counterValue); markerCounter.set(node, markerValue); hiddenNodes.push(node); if (counterValue === 1 && alreadyHidden) { uncontrolledNodes.set(node, true); } if (markerValue === 1) { node.setAttribute(markerName, ""); } if (!alreadyHidden) { node.setAttribute(controlAttribute, "true" ); } } catch (e) { console.error("[zag-js > ariaHidden] cannot operate on ", node, e); } } }); }; deep(parentNode); elementsToKeep.clear(); lockCount++; return () => { hiddenNodes.forEach((node) => { const counterValue = counterMap.get(node) - 1; const markerValue = markerCounter.get(node) - 1; counterMap.set(node, counterValue); markerCounter.set(node, markerValue); if (!counterValue) { if (!uncontrolledNodes.has(node)) { node.removeAttribute(controlAttribute); } uncontrolledNodes.delete(node); } if (!markerValue) { node.removeAttribute(markerName); } }); lockCount--; if (!lockCount) { counterMap = /* @__PURE__ */ new WeakMap(); counterMap = /* @__PURE__ */ new WeakMap(); uncontrolledNodes = /* @__PURE__ */ new WeakMap(); markerMap = {}; } }; }; // src/aria-hidden.ts var getParentNode = (originalTarget) => { const target = Array.isArray(originalTarget) ? originalTarget[0] : originalTarget; return target.ownerDocument.body; }; var hideOthers = (originalTarget, parentNode = getParentNode(originalTarget), markerName = "data-aria-hidden") => { if (!parentNode) return; return walkTreeOutside(originalTarget, { parentNode, markerName, controlAttribute: "aria-hidden"}); }; // src/index.ts var raf = (fn) => { const frameId = requestAnimationFrame(() => fn()); return () => cancelAnimationFrame(frameId); }; function ariaHidden(targetsOrFn, options = {}) { const { defer = true } = options; const func = defer ? raf : (v) => v(); const cleanups = []; cleanups.push( func(() => { const targets = typeof targetsOrFn === "function" ? targetsOrFn() : targetsOrFn; const elements = targets.filter(Boolean); if (elements.length === 0) return; cleanups.push(hideOthers(elements)); }) ); return () => { cleanups.forEach((fn) => fn?.()); }; } export { ariaHidden };