UNPKG

@servicetitan/assist-ui

Version:

ServiceTitan Atlas UI Components

128 lines (109 loc) 4.13 kB
import { MouseEvent, useCallback, useEffect, useState } from 'react'; export interface UseDraggableProps { initialX?: number; initialY?: number; minVisibleHorizontal?: number; minVisibleVertical?: number; width?: number; } export interface Position { x: number; y: number; } export const useDraggable = ({ initialX = window.innerWidth - 390, initialY = 64, minVisibleHorizontal = 390, minVisibleVertical = 56, width = 390, }: UseDraggableProps = {}) => { const [position, setPosition] = useState<Position>({ x: initialX, y: initialY }); const [isDragging, setIsDragging] = useState(false); const [dragOffset, setDragOffset] = useState<Position>({ x: 0, y: 0 }); // Function to ensure position is within viewport boundaries const ensurePositionInViewport = useCallback( (pos: Position): Position => { const maxX = window.innerWidth - minVisibleHorizontal; const minX = -width + minVisibleHorizontal; const maxY = window.innerHeight - minVisibleVertical; return { x: Math.min(Math.max(pos.x, minX), maxX), y: Math.min(Math.max(pos.y, minVisibleVertical), maxY), }; }, [minVisibleHorizontal, minVisibleVertical, width] ); // Function to recalculate initial position based on current viewport const recalculateInitialPosition = useCallback((): Position => { const newInitialX = window.innerWidth - width; return { x: newInitialX, y: initialY }; }, [width, initialY]); const resetPosition = useCallback(() => { const newInitialPos = recalculateInitialPosition(); setPosition(newInitialPos); }, [recalculateInitialPosition]); const handleMouseDown = useCallback( (e: MouseEvent) => { e.preventDefault(); setIsDragging(true); setDragOffset({ x: e.clientX - position.x, y: e.clientY - position.y, }); }, [position] ); const handleMouseMove = useCallback( (e: MouseEvent) => { if (!isDragging) { return; } const newX = e.clientX - dragOffset.x; const newY = e.clientY - dragOffset.y; const constrainedPosition = ensurePositionInViewport({ x: newX, y: newY }); setPosition(constrainedPosition); }, [isDragging, dragOffset, ensurePositionInViewport] ); const handleMouseUp = useCallback(() => { setIsDragging(false); }, []); // Handle viewport resize const handleResize = useCallback(() => { const constrainedPosition = ensurePositionInViewport(position); setPosition(constrainedPosition); }, [position, ensurePositionInViewport]); useEffect(() => { if (isDragging) { // Prevent text selection while dragging document.body.style.userSelect = 'none'; document.body.style.webkitUserSelect = 'none'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); } return () => { // Restore text selection document.body.style.userSelect = ''; document.body.style.webkitUserSelect = ''; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; }, [isDragging, handleMouseMove, handleMouseUp]); // Add resize listener useEffect(() => { window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, [handleResize]); return { position, isDragging, handleMouseDown, resetPosition, }; };