mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 09:22:24 +00:00
Refactor video modal accessibility wiring
This commit is contained in:
1 parent
395962c7cc
commit
3dc84dfd44
2 files changed
+232
-34
No files matched your search
@@ -0,0 +1,192 @@
|
||||
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 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 = () => {
|
||||
if (active) {
|
||||
return;
|
||||
}
|
||||
active = true;
|
||||
const activeElement = doc.activeElement;
|
||||
previousFocus = activeElement instanceof Element ? 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user