mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
211 lines
5.0 KiB
JavaScript
211 lines
5.0 KiB
JavaScript
const FOCUSABLE_SELECTORS = [
|
|
"a[href]",
|
|
"area[href]",
|
|
"button:not([disabled])",
|
|
'input:not([type="hidden"]):not([disabled])',
|
|
"select:not([disabled])",
|
|
"textarea:not([disabled])",
|
|
"iframe",
|
|
"object",
|
|
"embed",
|
|
'[contenteditable="true"]',
|
|
"[tabindex]",
|
|
"audio[controls]",
|
|
"video[controls]",
|
|
].join(",");
|
|
|
|
const scheduleMicrotask =
|
|
typeof queueMicrotask === "function"
|
|
? queueMicrotask
|
|
: (callback) => Promise.resolve().then(callback);
|
|
|
|
function isElement(node) {
|
|
return (
|
|
!!node &&
|
|
typeof node === "object" &&
|
|
typeof node.nodeType === "number" &&
|
|
node.nodeType === 1
|
|
);
|
|
}
|
|
|
|
function isFocusable(element) {
|
|
if (!element) {
|
|
return false;
|
|
}
|
|
if (element.hasAttribute('disabled')) {
|
|
return false;
|
|
}
|
|
const tabindex = element.getAttribute('tabindex');
|
|
if (tabindex && Number(tabindex) < 0) {
|
|
return false;
|
|
}
|
|
if (element.getAttribute('aria-hidden') === 'true') {
|
|
return false;
|
|
}
|
|
const rects = element.getClientRects();
|
|
if (rects.length === 0) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function createModalAccessibility({ root, panel, backdrop, document: providedDocument, onRequestClose } = {}) {
|
|
const doc = providedDocument || root?.ownerDocument || (typeof document !== 'undefined' ? document : null);
|
|
if (!root || !panel || !doc) {
|
|
return {
|
|
activate() {},
|
|
deactivate() {},
|
|
destroy() {},
|
|
isActive() {
|
|
return false;
|
|
},
|
|
};
|
|
}
|
|
|
|
let active = false;
|
|
let previousFocus = null;
|
|
|
|
const resolveDismissTarget = (target) => {
|
|
if (!target) {
|
|
return null;
|
|
}
|
|
if (
|
|
backdrop &&
|
|
(target === backdrop || backdrop.contains?.(target)) &&
|
|
!backdrop.hasAttribute("data-dismiss")
|
|
) {
|
|
return backdrop;
|
|
}
|
|
return target.closest?.("[data-dismiss]") || null;
|
|
};
|
|
|
|
const keydownListener = (event) => {
|
|
if (!active) {
|
|
return;
|
|
}
|
|
if (event.key === "Escape") {
|
|
if (root.contains(event.target)) {
|
|
event.preventDefault();
|
|
}
|
|
onRequestClose?.(event);
|
|
return;
|
|
}
|
|
if (event.key !== "Tab" || !root.contains(event.target)) {
|
|
return;
|
|
}
|
|
|
|
const focusable = Array.from(panel.querySelectorAll(FOCUSABLE_SELECTORS)).filter(isFocusable);
|
|
const activeElement = doc.activeElement;
|
|
const isPanelFocused = activeElement === panel;
|
|
|
|
if (focusable.length === 0) {
|
|
event.preventDefault();
|
|
if (panel !== activeElement) {
|
|
panel.focus({ preventScroll: true });
|
|
}
|
|
return;
|
|
}
|
|
|
|
const first = focusable[0];
|
|
const last = focusable[focusable.length - 1];
|
|
|
|
if (event.shiftKey) {
|
|
if (activeElement === first || !panel.contains(activeElement) || isPanelFocused) {
|
|
event.preventDefault();
|
|
last.focus({ preventScroll: true });
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (activeElement === last || isPanelFocused) {
|
|
event.preventDefault();
|
|
first.focus({ preventScroll: true });
|
|
}
|
|
};
|
|
|
|
const clickListener = (event) => {
|
|
if (!active) {
|
|
return;
|
|
}
|
|
const dismissTarget = resolveDismissTarget(event.target);
|
|
if (!dismissTarget) {
|
|
return;
|
|
}
|
|
if (!root.contains(dismissTarget)) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
onRequestClose?.(event);
|
|
};
|
|
|
|
const activate = ({ triggerElement } = {}) => {
|
|
if (active) {
|
|
return;
|
|
}
|
|
active = true;
|
|
const activeElement = doc.activeElement;
|
|
const hasDocContains = typeof doc.contains === "function";
|
|
const normalizedTrigger =
|
|
isElement(triggerElement) && (!hasDocContains || doc.contains(triggerElement))
|
|
? triggerElement
|
|
: null;
|
|
if (normalizedTrigger) {
|
|
previousFocus = normalizedTrigger;
|
|
} else {
|
|
previousFocus = isElement(activeElement) ? activeElement : null;
|
|
}
|
|
doc.addEventListener("keydown", keydownListener, true);
|
|
root.addEventListener("click", clickListener);
|
|
if (backdrop && backdrop !== root) {
|
|
backdrop.addEventListener("click", clickListener);
|
|
}
|
|
|
|
scheduleMicrotask(() => {
|
|
if (!active) {
|
|
return;
|
|
}
|
|
const focusTarget = Array.from(panel.querySelectorAll(FOCUSABLE_SELECTORS)).find(isFocusable) || panel;
|
|
if (typeof focusTarget.focus === "function") {
|
|
focusTarget.focus({ preventScroll: true });
|
|
}
|
|
});
|
|
};
|
|
|
|
const deactivate = () => {
|
|
if (!active) {
|
|
return;
|
|
}
|
|
doc.removeEventListener("keydown", keydownListener, true);
|
|
root.removeEventListener("click", clickListener);
|
|
if (backdrop && backdrop !== root) {
|
|
backdrop.removeEventListener("click", clickListener);
|
|
}
|
|
active = false;
|
|
|
|
if (previousFocus && typeof previousFocus.focus === "function") {
|
|
if (
|
|
!root.contains(previousFocus) &&
|
|
typeof doc.contains === "function" &&
|
|
doc.contains(previousFocus)
|
|
) {
|
|
previousFocus.focus({ preventScroll: true });
|
|
}
|
|
}
|
|
previousFocus = null;
|
|
};
|
|
|
|
const destroy = () => {
|
|
deactivate();
|
|
};
|
|
|
|
return {
|
|
activate,
|
|
deactivate,
|
|
destroy,
|
|
isActive() {
|
|
return active;
|
|
},
|
|
};
|
|
}
|