mirror of
https://github.com/kamranahmedse/driver.js.git
synced 2026-10-31 03:52:13 +08:00
162 lines
4.7 KiB
TypeScript
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);
|
|
}
|