@servicetitan/assist-ui
Version:
ServiceTitan Atlas UI Components
128 lines (109 loc) • 4.13 kB
text/typescript
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,
};
};