@zag-js/aria-hidden
Version:
Hide targets from screen readers
134 lines (130 loc) • 4.61 kB
JavaScript
// 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 };