81 lines
2.8 KiB
TypeScript
81 lines
2.8 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
|
|
const MOBILE_SIDEBAR_SWIPE_EDGE_PX = 32;
|
|
const MOBILE_SIDEBAR_SWIPE_DISTANCE_PX = 56;
|
|
|
|
export function useMobileSidebar() {
|
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
const mobileSidebarSwipeRef = useRef<{ startX: number; startY: number } | null>(null);
|
|
|
|
useEffect(() => {
|
|
const desktop = window.matchMedia("(min-width: 768px)");
|
|
function closeMobileSidebar() {
|
|
if (desktop.matches) setMobileSidebarOpen(false);
|
|
}
|
|
closeMobileSidebar();
|
|
desktop.addEventListener("change", closeMobileSidebar);
|
|
return () => desktop.removeEventListener("change", closeMobileSidebar);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!mobileSidebarOpen) return;
|
|
function onKeyDown(event: KeyboardEvent) {
|
|
if (event.key !== "Escape" || window.matchMedia("(min-width: 768px)").matches) return;
|
|
event.preventDefault();
|
|
setMobileSidebarOpen(false);
|
|
queueMicrotask(() => {
|
|
document.querySelector<HTMLElement>('[data-testid="open-navigation"]')?.focus();
|
|
});
|
|
}
|
|
window.addEventListener("keydown", onKeyDown, true);
|
|
return () => window.removeEventListener("keydown", onKeyDown, true);
|
|
}, [mobileSidebarOpen]);
|
|
|
|
function onMobileSidebarTouchStart(event: React.TouchEvent<HTMLDivElement>) {
|
|
if (
|
|
mobileSidebarOpen ||
|
|
event.touches.length !== 1 ||
|
|
window.matchMedia("(min-width: 768px)").matches
|
|
) {
|
|
mobileSidebarSwipeRef.current = null;
|
|
return;
|
|
}
|
|
const touch = event.touches[0];
|
|
if (!touch) return;
|
|
const rtl = document.documentElement.getAttribute("dir") === "rtl";
|
|
const startsAtEdge = rtl
|
|
? touch.clientX >= window.innerWidth - MOBILE_SIDEBAR_SWIPE_EDGE_PX
|
|
: touch.clientX <= MOBILE_SIDEBAR_SWIPE_EDGE_PX;
|
|
mobileSidebarSwipeRef.current = startsAtEdge
|
|
? { startX: touch.clientX, startY: touch.clientY }
|
|
: null;
|
|
}
|
|
|
|
function onMobileSidebarTouchEnd(event: React.TouchEvent<HTMLDivElement>) {
|
|
const swipe = mobileSidebarSwipeRef.current;
|
|
mobileSidebarSwipeRef.current = null;
|
|
const touch = event.changedTouches[0];
|
|
if (!swipe || !touch || mobileSidebarOpen || window.matchMedia("(min-width: 768px)").matches) {
|
|
return;
|
|
}
|
|
const rtl = document.documentElement.getAttribute("dir") === "rtl";
|
|
const horizontal = rtl ? swipe.startX - touch.clientX : touch.clientX - swipe.startX;
|
|
const vertical = Math.abs(touch.clientY - swipe.startY);
|
|
if (horizontal >= MOBILE_SIDEBAR_SWIPE_DISTANCE_PX && horizontal > vertical * 1.25) {
|
|
setMobileSidebarOpen(true);
|
|
}
|
|
}
|
|
|
|
function onMobileSidebarTouchCancel() {
|
|
mobileSidebarSwipeRef.current = null;
|
|
}
|
|
|
|
return {
|
|
mobileSidebarOpen,
|
|
setMobileSidebarOpen,
|
|
onMobileSidebarTouchStart,
|
|
onMobileSidebarTouchEnd,
|
|
onMobileSidebarTouchCancel,
|
|
};
|
|
}
|