Files
driver.js/src/events.ts
T

162 lines
4.7 KiB
TypeScript

import { Context } from "./context";
import { refreshActiveHighlight } from "./highlight";
import { getFocusableElements } from "./utils";
export function requireRefresh(ctx: Context) {
const resizeTimeout = ctx.getState("__resizeTimeout");
if (resizeTimeout) {
window.cancelAnimationFrame(resizeTimeout);
}
ctx.setState(
"__resizeTimeout",
window.requestAnimationFrame(() => refreshActiveHighlight(ctx))
);
}
function trapFocus(ctx: Context, e: KeyboardEvent) {
const isActivated = ctx.getState("isInitialized");
if (!isActivated) {
return;
}
const isTabKey = e.key === "Tab" || e.keyCode === 9;
if (!isTabKey) {
return;
}
const activeElement = ctx.getState("__activeElement");
const popoverEl = ctx.getState("popover")?.wrapper;
const focusableEls = getFocusableElements([
...(popoverEl ? [popoverEl] : []),
...(activeElement ? [activeElement] : []),
]);
const firstFocusableEl = focusableEls[0];
const lastFocusableEl = focusableEls[focusableEls.length - 1];
e.preventDefault();
if (e.shiftKey) {
const previousFocusableEl =
focusableEls[focusableEls.indexOf(document.activeElement as HTMLElement) - 1] || lastFocusableEl;
previousFocusableEl?.focus();
} else {
const nextFocusableEl =
focusableEls[focusableEls.indexOf(document.activeElement as HTMLElement) + 1] || firstFocusableEl;
nextFocusableEl?.focus();
}
}
function onKeyup(ctx: Context, e: KeyboardEvent) {
const allowKeyboardControl = ctx.getConfig("allowKeyboardControl") ?? true;
if (!allowKeyboardControl) {
return;
}
if (e.key === "Escape") {
ctx.emit("escapePress");
} else if (e.key === "ArrowRight") {
ctx.emit("arrowRightPress");
} else if (e.key === "ArrowLeft") {
ctx.emit("arrowLeftPress");
}
}
const DRIVER_CLICK_EVENTS = ["pointerdown", "mousedown", "pointerup", "mouseup", "click"] as const;
const driverClickHandlers = new WeakMap<Element, (e: MouseEvent | PointerEvent) => void>();
/**
* Attaches click handler to the elements created by driver.js. It makes
* sure to give the listener the first chance to handle the event, and
* prevents all other pointer-events to make sure no external-library
* ever knows the click happened.
*
* @param {Element} element Element to listen for click events
* @param {(pointer: MouseEvent | PointerEvent) => void} listener Click handler
* @param {(target: HTMLElement) => boolean} shouldPreventDefault Whether to prevent default action i.e. link clicks etc
*/
export function onDriverClick(
element: Element,
listener: (pointer: MouseEvent | PointerEvent) => void,
shouldPreventDefault?: (target: HTMLElement) => boolean
) {
destroyDriverClick(element);
const handler = (e: MouseEvent | PointerEvent) => {
const target = e.target as HTMLElement;
if (!element.contains(target)) {
return;
}
if (!shouldPreventDefault || shouldPreventDefault(target)) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
if (e.type === "click") {
listener?.(e);
}
};
// We want to be the absolute first one to hear about the event
const useCapture = true;
for (const type of DRIVER_CLICK_EVENTS) {
document.addEventListener(type, handler, useCapture);
}
driverClickHandlers.set(element, handler);
}
export function destroyDriverClick(element: Element) {
const handler = driverClickHandlers.get(element);
if (!handler) {
return;
}
for (const type of DRIVER_CLICK_EVENTS) {
document.removeEventListener(type, handler, true);
}
driverClickHandlers.delete(element);
}
export function initEvents(ctx: Context) {
// The handlers close over this instance's context and are stashed in state so
// destroyEvents can detach the very same references, keeping instances from
// removing each other's listeners.
const onWindowKeyup = (e: KeyboardEvent) => onKeyup(ctx, e);
const onWindowKeydown = (e: KeyboardEvent) => trapFocus(ctx, e);
const onWindowResize = () => requireRefresh(ctx);
const onWindowScroll = () => requireRefresh(ctx);
ctx.setState("__events", {
onKeyup: onWindowKeyup,
onKeydown: onWindowKeydown,
onResize: onWindowResize,
onScroll: onWindowScroll,
});
window.addEventListener("keyup", onWindowKeyup, false);
window.addEventListener("keydown", onWindowKeydown, false);
window.addEventListener("resize", onWindowResize);
window.addEventListener("scroll", onWindowScroll);
}
export function destroyEvents(ctx: Context) {
const events = ctx.getState("__events");
if (!events) {
return;
}
window.removeEventListener("keyup", events.onKeyup);
window.removeEventListener("keydown", events.onKeydown);
window.removeEventListener("resize", events.onResize);
window.removeEventListener("scroll", events.onScroll);
}