mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 04:42:57 +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
@@ -1,3 +1,5 @@
|
|||||||
|
import { createModalAccessibility } from "./modalAccessibility.js";
|
||||||
|
|
||||||
export class VideoModal {
|
export class VideoModal {
|
||||||
constructor({
|
constructor({
|
||||||
removeTrackingScripts,
|
removeTrackingScripts,
|
||||||
@@ -25,6 +27,8 @@ export class VideoModal {
|
|||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
|
|
||||||
this.playerModal = null;
|
this.playerModal = null;
|
||||||
|
this.modalPanel = null;
|
||||||
|
this.modalBackdrop = null;
|
||||||
this.scrollRegion = null;
|
this.scrollRegion = null;
|
||||||
this.modalVideo = null;
|
this.modalVideo = null;
|
||||||
this.modalStatus = null;
|
this.modalStatus = null;
|
||||||
@@ -46,6 +50,9 @@ export class VideoModal {
|
|||||||
this.modalMoreBtn = null;
|
this.modalMoreBtn = null;
|
||||||
this.modalMoreMenu = null;
|
this.modalMoreMenu = null;
|
||||||
|
|
||||||
|
this.modalAccessibility = null;
|
||||||
|
this.modalNavScrollHandler = null;
|
||||||
|
|
||||||
this.modalZapDialog = null;
|
this.modalZapDialog = null;
|
||||||
this.modalZapForm = null;
|
this.modalZapForm = null;
|
||||||
this.modalZapAmountInput = null;
|
this.modalZapAmountInput = null;
|
||||||
@@ -190,9 +197,23 @@ export class VideoModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hydrate(playerModal) {
|
hydrate(playerModal) {
|
||||||
|
if (this.modalAccessibility?.destroy) {
|
||||||
|
this.modalAccessibility.destroy();
|
||||||
|
}
|
||||||
|
this.modalAccessibility = null;
|
||||||
|
|
||||||
|
const previousScrollRegion = this.scrollRegion;
|
||||||
|
if (previousScrollRegion && this.modalNavScrollHandler) {
|
||||||
|
previousScrollRegion.removeEventListener(
|
||||||
|
"scroll",
|
||||||
|
this.modalNavScrollHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.playerModal = playerModal;
|
this.playerModal = playerModal;
|
||||||
this.scrollRegion =
|
this.modalPanel = playerModal.querySelector(".bv-modal__panel") || null;
|
||||||
playerModal.querySelector(".player-modal__content") || playerModal;
|
this.modalBackdrop = playerModal.querySelector("[data-dismiss]") || null;
|
||||||
|
this.scrollRegion = this.modalPanel || playerModal;
|
||||||
|
|
||||||
this.modalVideo = playerModal.querySelector("#modalVideo") || null;
|
this.modalVideo = playerModal.querySelector("#modalVideo") || null;
|
||||||
this.modalStatus = playerModal.querySelector("#modalStatus") || null;
|
this.modalStatus = playerModal.querySelector("#modalStatus") || null;
|
||||||
@@ -253,7 +274,7 @@ export class VideoModal {
|
|||||||
const modalNav = playerModal.querySelector("#modalNav");
|
const modalNav = playerModal.querySelector("#modalNav");
|
||||||
if (modalNav && this.scrollRegion) {
|
if (modalNav && this.scrollRegion) {
|
||||||
let lastScrollY = 0;
|
let lastScrollY = 0;
|
||||||
this.scrollRegion.addEventListener("scroll", () => {
|
this.modalNavScrollHandler = () => {
|
||||||
const currentScrollY = this.scrollRegion.scrollTop;
|
const currentScrollY = this.scrollRegion.scrollTop;
|
||||||
const shouldShowNav =
|
const shouldShowNav =
|
||||||
currentScrollY <= lastScrollY || currentScrollY < 50;
|
currentScrollY <= lastScrollY || currentScrollY < 50;
|
||||||
@@ -261,9 +282,22 @@ export class VideoModal {
|
|||||||
? "translateY(0)"
|
? "translateY(0)"
|
||||||
: "translateY(-100%)";
|
: "translateY(-100%)";
|
||||||
lastScrollY = currentScrollY;
|
lastScrollY = currentScrollY;
|
||||||
});
|
};
|
||||||
|
this.scrollRegion.addEventListener("scroll", this.modalNavScrollHandler);
|
||||||
|
} else {
|
||||||
|
this.modalNavScrollHandler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.modalAccessibility = createModalAccessibility({
|
||||||
|
root: this.playerModal,
|
||||||
|
panel: this.modalPanel,
|
||||||
|
backdrop: this.modalBackdrop,
|
||||||
|
document: this.document,
|
||||||
|
onRequestClose: () => {
|
||||||
|
this.dispatch("modal:close", { video: this.activeVideo });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.bindVideoEvents();
|
this.bindVideoEvents();
|
||||||
this.bindActionButtons();
|
this.bindActionButtons();
|
||||||
this.setZapVisibility(false);
|
this.setZapVisibility(false);
|
||||||
@@ -427,34 +461,6 @@ export class VideoModal {
|
|||||||
this.dispatch("creator:navigate", { video: this.activeVideo });
|
this.dispatch("creator:navigate", { video: this.activeVideo });
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleBackgroundInert(enable) {
|
|
||||||
const ids = ["sidebar", "sidebarOverlay"];
|
|
||||||
for (const id of ids) {
|
|
||||||
const element = this.document.getElementById(id);
|
|
||||||
if (!element) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enable) {
|
|
||||||
if (!element.hasAttribute("inert")) {
|
|
||||||
element.setAttribute("inert", "");
|
|
||||||
}
|
|
||||||
element.dataset.modalInert = "true";
|
|
||||||
element.setAttribute("aria-hidden", "true");
|
|
||||||
element.classList.add("is-modal-hidden");
|
|
||||||
} else {
|
|
||||||
if (element.dataset.modalInert === "true") {
|
|
||||||
element.removeAttribute("inert");
|
|
||||||
delete element.dataset.modalInert;
|
|
||||||
}
|
|
||||||
if (element.getAttribute("aria-hidden") === "true") {
|
|
||||||
element.removeAttribute("aria-hidden");
|
|
||||||
}
|
|
||||||
element.classList.remove("is-modal-hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open(video) {
|
open(video) {
|
||||||
this.activeVideo = video || null;
|
this.activeVideo = video || null;
|
||||||
if (!this.playerModal) {
|
if (!this.playerModal) {
|
||||||
@@ -465,7 +471,7 @@ export class VideoModal {
|
|||||||
this.playerModal.classList.remove("hidden");
|
this.playerModal.classList.remove("hidden");
|
||||||
this.document.body.classList.add("modal-open");
|
this.document.body.classList.add("modal-open");
|
||||||
this.document.documentElement.classList.add("modal-open");
|
this.document.documentElement.classList.add("modal-open");
|
||||||
this.toggleBackgroundInert(true);
|
this.modalAccessibility?.activate();
|
||||||
if (this.scrollRegion) {
|
if (this.scrollRegion) {
|
||||||
this.scrollRegion.scrollTop = 0;
|
this.scrollRegion.scrollTop = 0;
|
||||||
}
|
}
|
||||||
@@ -481,7 +487,7 @@ export class VideoModal {
|
|||||||
}
|
}
|
||||||
this.document.body.classList.remove("modal-open");
|
this.document.body.classList.remove("modal-open");
|
||||||
this.document.documentElement.classList.remove("modal-open");
|
this.document.documentElement.classList.remove("modal-open");
|
||||||
this.toggleBackgroundInert(false);
|
this.modalAccessibility?.deactivate();
|
||||||
this.setGlobalModalState("player", false);
|
this.setGlobalModalState("player", false);
|
||||||
this.forceRemovePoster("close");
|
this.forceRemovePoster("close");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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