extract auth to lib
This commit is contained in:
174
src/panels/useSidePanelMachine.ts
Normal file
174
src/panels/useSidePanelMachine.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { useCallback, useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react';
|
||||
|
||||
const DEFAULT_DESKTOP_BREAKPOINT = 1024;
|
||||
|
||||
type ResizeAxis = 'from-left' | 'from-right';
|
||||
|
||||
export type SidePanelMachineOptions = {
|
||||
storageKey: string;
|
||||
defaultWidth: number;
|
||||
minWidth: number;
|
||||
maxWidth: number;
|
||||
resizeAxis: ResizeAxis;
|
||||
resizingBodyClass: string;
|
||||
isOpen: boolean;
|
||||
canResize: boolean;
|
||||
shouldPersistWidth: boolean;
|
||||
closeOnPathname?: string;
|
||||
onCloseOnPathname?: () => void;
|
||||
onEscape?: () => void;
|
||||
desktopBreakpoint?: number;
|
||||
};
|
||||
|
||||
export type SidePanelMachineResult = {
|
||||
width: number;
|
||||
isDesktop: boolean;
|
||||
startResize: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
};
|
||||
|
||||
export function isDesktopViewport(breakpoint = DEFAULT_DESKTOP_BREAKPOINT): boolean {
|
||||
if (!globalThis.window) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof globalThis.window.matchMedia === 'function') {
|
||||
return globalThis.window.matchMedia(`(min-width: ${breakpoint}px)`).matches;
|
||||
}
|
||||
|
||||
return window.innerWidth >= breakpoint;
|
||||
}
|
||||
|
||||
export function useSidePanelMachine({
|
||||
storageKey,
|
||||
defaultWidth,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
resizeAxis,
|
||||
resizingBodyClass,
|
||||
isOpen,
|
||||
canResize,
|
||||
shouldPersistWidth,
|
||||
closeOnPathname,
|
||||
onCloseOnPathname,
|
||||
onEscape,
|
||||
desktopBreakpoint = DEFAULT_DESKTOP_BREAKPOINT
|
||||
}: SidePanelMachineOptions): SidePanelMachineResult {
|
||||
const isResizingRef = useRef(false);
|
||||
const resizeStartXRef = useRef(0);
|
||||
const resizeStartWidthRef = useRef(0);
|
||||
|
||||
const clampWidth = useCallback((value: number) => {
|
||||
return Math.min(maxWidth, Math.max(minWidth, value));
|
||||
}, [maxWidth, minWidth]);
|
||||
|
||||
const readStoredWidth = useCallback(() => {
|
||||
if (!globalThis.window) {
|
||||
return defaultWidth;
|
||||
}
|
||||
|
||||
const storedValue = localStorage.getItem(storageKey);
|
||||
const parsed = Number(storedValue);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return defaultWidth;
|
||||
}
|
||||
|
||||
return clampWidth(parsed);
|
||||
}, [defaultWidth, storageKey, clampWidth]);
|
||||
|
||||
const [width, setWidth] = useState<number>(() => readStoredWidth());
|
||||
|
||||
useEffect(() => {
|
||||
if (closeOnPathname == null || !onCloseOnPathname) {
|
||||
return;
|
||||
}
|
||||
|
||||
onCloseOnPathname();
|
||||
}, [closeOnPathname, onCloseOnPathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldPersistWidth || !globalThis.window) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(storageKey, String(width));
|
||||
}, [shouldPersistWidth, storageKey, width]);
|
||||
|
||||
useEffect(() => {
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
if (!isResizingRef.current || !canResize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = event.clientX - resizeStartXRef.current;
|
||||
const delta = resizeAxis === 'from-left' ? deltaX : -deltaX;
|
||||
const nextWidth = clampWidth(resizeStartWidthRef.current + delta);
|
||||
setWidth(nextWidth);
|
||||
}
|
||||
|
||||
function stopResizing() {
|
||||
if (!isResizingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isResizingRef.current = false;
|
||||
document.body.classList.remove(resizingBodyClass);
|
||||
}
|
||||
|
||||
globalThis.addEventListener('pointermove', handlePointerMove);
|
||||
globalThis.addEventListener('pointerup', stopResizing);
|
||||
|
||||
return () => {
|
||||
globalThis.removeEventListener('pointermove', handlePointerMove);
|
||||
globalThis.removeEventListener('pointerup', stopResizing);
|
||||
document.body.classList.remove(resizingBodyClass);
|
||||
};
|
||||
}, [canResize, clampWidth, resizeAxis, resizingBodyClass]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || isDesktopViewport(desktopBreakpoint)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
};
|
||||
}, [isOpen, desktopBreakpoint]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !onEscape) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onEscape();
|
||||
}
|
||||
};
|
||||
|
||||
globalThis.addEventListener('keydown', handleEscape);
|
||||
return () => {
|
||||
globalThis.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [isOpen, onEscape]);
|
||||
|
||||
const startResize = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (!canResize || !isDesktopViewport(desktopBreakpoint)) {
|
||||
return;
|
||||
}
|
||||
|
||||
isResizingRef.current = true;
|
||||
resizeStartXRef.current = event.clientX;
|
||||
resizeStartWidthRef.current = width;
|
||||
document.body.classList.add(resizingBodyClass);
|
||||
event.preventDefault();
|
||||
}, [canResize, desktopBreakpoint, resizingBodyClass, width]);
|
||||
|
||||
return {
|
||||
width,
|
||||
isDesktop: isDesktopViewport(desktopBreakpoint),
|
||||
startResize
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user