@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
JavaScript
// 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
});
}