evOo-local/apps/web/src/pages/shell/use-mobile-sidebar.ts
Cursor Agent c5b2ebc2b3
Fix German signup E2E, Escape menu stacking, and Heute rhythm click
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-06 17:00:11 +00:00

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,
};
}