@zag-js/aria-hidden
Advanced tools
Comparing version 0.65.1 to 0.66.0
@@ -1,10 +0,9 @@ | ||
interface AriaHiddenOptions { | ||
rootEl?: HTMLElement; | ||
defer?: boolean; | ||
} | ||
type MaybeElement = HTMLElement | null; | ||
type Targets = Array<MaybeElement>; | ||
type TargetsOrFn = Targets | (() => Targets); | ||
declare function ariaHidden(targetsOrFn: TargetsOrFn, options?: AriaHiddenOptions): () => void; | ||
type Options = { | ||
defer?: boolean; | ||
}; | ||
declare function ariaHidden(targetsOrFn: TargetsOrFn, options?: Options): () => void; | ||
export { type AriaHiddenOptions, ariaHidden }; | ||
export { ariaHidden }; |
@@ -26,113 +26,10 @@ "use strict"; | ||
module.exports = __toCommonJS(src_exports); | ||
var import_dom_query = require("@zag-js/dom-query"); | ||
var refCountMap = /* @__PURE__ */ new WeakMap(); | ||
var observerStack = []; | ||
function ariaHiddenImpl(targets, options = {}) { | ||
const { rootEl } = options; | ||
const exclude = targets.filter(Boolean); | ||
if (exclude.length === 0) return; | ||
const doc = exclude[0].ownerDocument || document; | ||
const win = doc.defaultView ?? window; | ||
const visibleNodes = new Set(exclude); | ||
const hiddenNodes = /* @__PURE__ */ new Set(); | ||
const root = rootEl ?? doc.body; | ||
let walk = (root2) => { | ||
for (let element of root2.querySelectorAll("[data-live-announcer], [data-zag-top-layer]")) { | ||
visibleNodes.add(element); | ||
} | ||
let acceptNode = (node) => { | ||
if (visibleNodes.has(node) || hiddenNodes.has(node.parentElement) && node.parentElement.getAttribute("role") !== "row") { | ||
return NodeFilter.FILTER_REJECT; | ||
} | ||
for (let target of visibleNodes) { | ||
if (node.contains(target)) { | ||
return NodeFilter.FILTER_SKIP; | ||
} | ||
} | ||
return NodeFilter.FILTER_ACCEPT; | ||
}; | ||
let walker = doc.createTreeWalker(root2, NodeFilter.SHOW_ELEMENT, { acceptNode }); | ||
let acceptRoot = acceptNode(root2); | ||
if (acceptRoot === NodeFilter.FILTER_ACCEPT) { | ||
hide(root2); | ||
} | ||
if (acceptRoot !== NodeFilter.FILTER_REJECT) { | ||
let node = walker.nextNode(); | ||
while (node != null) { | ||
hide(node); | ||
node = walker.nextNode(); | ||
} | ||
} | ||
}; | ||
let hide = (node) => { | ||
let refCount = refCountMap.get(node) ?? 0; | ||
if (node.getAttribute("aria-hidden") === "true" && refCount === 0) { | ||
return; | ||
} | ||
if (refCount === 0) { | ||
node.setAttribute("aria-hidden", "true"); | ||
} | ||
hiddenNodes.add(node); | ||
refCountMap.set(node, refCount + 1); | ||
}; | ||
if (observerStack.length) { | ||
observerStack[observerStack.length - 1].disconnect(); | ||
} | ||
walk(root); | ||
const observer = new win.MutationObserver((changes) => { | ||
for (let change of changes) { | ||
if (change.type !== "childList" || change.addedNodes.length === 0) { | ||
continue; | ||
} | ||
if (![...visibleNodes, ...hiddenNodes].some((node) => node.contains(change.target))) { | ||
for (let node of change.removedNodes) { | ||
if (node instanceof win.Element) { | ||
visibleNodes.delete(node); | ||
hiddenNodes.delete(node); | ||
} | ||
} | ||
for (let node of change.addedNodes) { | ||
if ((node instanceof win.HTMLElement || node instanceof win.SVGElement) && (node.dataset.liveAnnouncer === "true" || node.dataset.zagTopLayer === "true")) { | ||
visibleNodes.add(node); | ||
} else if (node instanceof win.Element) { | ||
walk(node); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
observer.observe(root, { childList: true, subtree: true }); | ||
let observerWrapper = { | ||
observe() { | ||
observer.observe(root, { childList: true, subtree: true }); | ||
}, | ||
disconnect() { | ||
observer.disconnect(); | ||
} | ||
}; | ||
observerStack.push(observerWrapper); | ||
return () => { | ||
observer.disconnect(); | ||
for (let node of hiddenNodes) { | ||
let count = refCountMap.get(node); | ||
if (count === 1) { | ||
node.removeAttribute("aria-hidden"); | ||
refCountMap.delete(node); | ||
} else { | ||
refCountMap.set(node, count - 1); | ||
} | ||
} | ||
if (observerWrapper === observerStack[observerStack.length - 1]) { | ||
observerStack.pop(); | ||
if (observerStack.length) { | ||
observerStack[observerStack.length - 1].observe(); | ||
} | ||
} else { | ||
observerStack.splice(observerStack.indexOf(observerWrapper), 1); | ||
} | ||
}; | ||
} | ||
var import_aria_hidden = require("aria-hidden"); | ||
var raf = (fn) => { | ||
const frameId = requestAnimationFrame(() => fn()); | ||
return () => cancelAnimationFrame(frameId); | ||
}; | ||
function ariaHidden(targetsOrFn, options = {}) { | ||
const { defer } = options; | ||
const func = defer ? import_dom_query.raf : (v) => v(); | ||
const { defer = true } = options; | ||
const func = defer ? raf : (v) => v(); | ||
const cleanups = []; | ||
@@ -142,3 +39,4 @@ cleanups.push( | ||
const targets = typeof targetsOrFn === "function" ? targetsOrFn() : targetsOrFn; | ||
cleanups.push(ariaHiddenImpl(targets, options)); | ||
const elements = targets.filter(Boolean); | ||
cleanups.push((0, import_aria_hidden.hideOthers)(elements)); | ||
}) | ||
@@ -145,0 +43,0 @@ ); |
{ | ||
"name": "@zag-js/aria-hidden", | ||
"version": "0.65.1", | ||
"version": "0.66.0", | ||
"description": "Hide targets from screen readers", | ||
@@ -28,3 +28,3 @@ "keywords": [ | ||
"dependencies": { | ||
"@zag-js/dom-query": "0.65.1" | ||
"aria-hidden": "1.2.4" | ||
}, | ||
@@ -31,0 +31,0 @@ "devDependencies": { |
169
src/index.ts
@@ -1,10 +0,6 @@ | ||
// Credits: https://github.com/adobe/react-spectrum/blob/main/packages/@react-aria/overlays/src/ariaHideOutside.ts | ||
import { raf } from "@zag-js/dom-query" | ||
import { hideOthers } from "aria-hidden" | ||
const refCountMap = new WeakMap<Element, number>() | ||
const observerStack: any[] = [] | ||
export interface AriaHiddenOptions { | ||
rootEl?: HTMLElement | ||
defer?: boolean | ||
const raf = (fn: VoidFunction) => { | ||
const frameId = requestAnimationFrame(() => fn()) | ||
return () => cancelAnimationFrame(frameId) | ||
} | ||
@@ -16,154 +12,8 @@ | ||
function ariaHiddenImpl(targets: Targets, options: AriaHiddenOptions = {}) { | ||
const { rootEl } = options | ||
const exclude = targets.filter(Boolean) as HTMLElement[] | ||
if (exclude.length === 0) return | ||
const doc = exclude[0].ownerDocument || document | ||
const win = doc.defaultView ?? window | ||
const visibleNodes = new Set<Element>(exclude) | ||
const hiddenNodes = new Set<Element>() | ||
const root = rootEl ?? doc.body | ||
let walk = (root: Element) => { | ||
// Keep live announcer and top layer elements (e.g. toasts) visible. | ||
for (let element of root.querySelectorAll("[data-live-announcer], [data-zag-top-layer]")) { | ||
visibleNodes.add(element) | ||
} | ||
let acceptNode = (node: Element) => { | ||
// Skip this node and its children if it is one of the target nodes, or a live announcer. | ||
// Also skip children of already hidden nodes, as aria-hidden is recursive. An exception is | ||
// made for elements with role="row" since VoiceOver on iOS has issues hiding elements with role="row". | ||
// For that case we want to hide the cells inside as well (https://bugs.webkit.org/show_bug.cgi?id=222623). | ||
if ( | ||
visibleNodes.has(node) || | ||
(hiddenNodes.has(node.parentElement!) && node.parentElement!.getAttribute("role") !== "row") | ||
) { | ||
return NodeFilter.FILTER_REJECT | ||
} | ||
// Skip this node but continue to children if one of the targets is inside the node. | ||
for (let target of visibleNodes) { | ||
if (node.contains(target)) { | ||
return NodeFilter.FILTER_SKIP | ||
} | ||
} | ||
return NodeFilter.FILTER_ACCEPT | ||
} | ||
let walker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, { acceptNode }) | ||
// TreeWalker does not include the root. | ||
let acceptRoot = acceptNode(root) | ||
if (acceptRoot === NodeFilter.FILTER_ACCEPT) { | ||
hide(root) | ||
} | ||
if (acceptRoot !== NodeFilter.FILTER_REJECT) { | ||
let node = walker.nextNode() as Element | ||
while (node != null) { | ||
hide(node) | ||
node = walker.nextNode() as Element | ||
} | ||
} | ||
} | ||
let hide = (node: Element) => { | ||
let refCount = refCountMap.get(node) ?? 0 | ||
// If already aria-hidden, and the ref count is zero, then this element | ||
// was already hidden and there's nothing for us to do. | ||
if (node.getAttribute("aria-hidden") === "true" && refCount === 0) { | ||
return | ||
} | ||
if (refCount === 0) { | ||
node.setAttribute("aria-hidden", "true") | ||
} | ||
hiddenNodes.add(node) | ||
refCountMap.set(node, refCount + 1) | ||
} | ||
if (observerStack.length) { | ||
observerStack[observerStack.length - 1].disconnect() | ||
} | ||
walk(root) | ||
const observer = new win.MutationObserver((changes) => { | ||
for (let change of changes) { | ||
if (change.type !== "childList" || change.addedNodes.length === 0) { | ||
continue | ||
} | ||
// If the parent element of the added nodes is not within one of the targets, | ||
// and not already inside a hidden node, hide all of the new children. | ||
if (![...visibleNodes, ...hiddenNodes].some((node) => node.contains(change.target))) { | ||
for (let node of change.removedNodes) { | ||
if (node instanceof win.Element) { | ||
visibleNodes.delete(node) | ||
hiddenNodes.delete(node) | ||
} | ||
} | ||
for (let node of change.addedNodes) { | ||
if ( | ||
(node instanceof win.HTMLElement || node instanceof win.SVGElement) && | ||
(node.dataset.liveAnnouncer === "true" || node.dataset.zagTopLayer === "true") | ||
) { | ||
visibleNodes.add(node) | ||
} else if (node instanceof win.Element) { | ||
walk(node) | ||
} | ||
} | ||
} | ||
} | ||
}) | ||
observer.observe(root, { childList: true, subtree: true }) | ||
let observerWrapper = { | ||
observe() { | ||
observer.observe(root, { childList: true, subtree: true }) | ||
}, | ||
disconnect() { | ||
observer.disconnect() | ||
}, | ||
} | ||
observerStack.push(observerWrapper) | ||
return () => { | ||
observer.disconnect() | ||
for (let node of hiddenNodes) { | ||
let count = refCountMap.get(node) | ||
if (count === 1) { | ||
node.removeAttribute("aria-hidden") | ||
refCountMap.delete(node) | ||
} else { | ||
refCountMap.set(node, count! - 1) | ||
} | ||
} | ||
// Remove this observer from the stack, and start the previous one. | ||
if (observerWrapper === observerStack[observerStack.length - 1]) { | ||
observerStack.pop() | ||
if (observerStack.length) { | ||
observerStack[observerStack.length - 1].observe() | ||
} | ||
} else { | ||
observerStack.splice(observerStack.indexOf(observerWrapper), 1) | ||
} | ||
} | ||
type Options = { | ||
defer?: boolean | ||
} | ||
export function ariaHidden(targetsOrFn: TargetsOrFn, options: AriaHiddenOptions = {}) { | ||
const { defer } = options | ||
export function ariaHidden(targetsOrFn: TargetsOrFn, options: Options = {}) { | ||
const { defer = true } = options | ||
const func = defer ? raf : (v: any) => v() | ||
@@ -174,3 +24,4 @@ const cleanups: (VoidFunction | undefined)[] = [] | ||
const targets = typeof targetsOrFn === "function" ? targetsOrFn() : targetsOrFn | ||
cleanups.push(ariaHiddenImpl(targets, options)) | ||
const elements = targets.filter(Boolean) as HTMLElement[] | ||
cleanups.push(hideOthers(elements)) | ||
}), | ||
@@ -177,0 +28,0 @@ ) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
9414
106
1
+ Addedaria-hidden@1.2.4
+ Addedaria-hidden@1.2.4(transitive)
+ Addedtslib@2.7.0(transitive)
- Removed@zag-js/dom-query@0.65.1
- Removed@zag-js/dom-query@0.65.1(transitive)