useFloatingToolbarDrag.ts97 lines · main
1import { useCallback, useRef, useState } from 'react'
2
3import {
4 DRAG_THRESHOLD_PX,
5 getNextPosition,
6 type NavSize,
7 type Position,
8 type Viewport,
9} from './FloatingMobileToolbar.utils'
10
11type DragStartState = {
12 x: number
13 y: number
14 startX: number
15 startY: number
16 pointerId: number
17}
18
19export function useFloatingToolbarDrag(navRef: React.RefObject<HTMLElement | null>) {
20 const [position, setPosition] = useState<Position | null>(null)
21 const dragStartRef = useRef<DragStartState | null>(null)
22
23 const applyMove = useCallback(
24 (clientX: number, clientY: number) => {
25 const state = dragStartRef.current
26 if (!state) return
27 const rect = navRef.current?.getBoundingClientRect()
28 const viewport: Viewport = {
29 width: typeof window !== 'undefined' ? window.innerWidth : 0,
30 height: typeof window !== 'undefined' ? window.innerHeight : 0,
31 }
32 const navSize: NavSize = {
33 width: rect?.width ?? 200,
34 height: rect?.height ?? 48,
35 }
36 const next = getNextPosition(state, clientX, clientY, viewport, navSize, DRAG_THRESHOLD_PX)
37 if (next) setPosition(next)
38 },
39 [navRef]
40 )
41
42 const handlePointerDown = useCallback(
43 (e: React.PointerEvent) => {
44 e.preventDefault()
45 e.stopPropagation()
46 const rect = navRef.current?.getBoundingClientRect()
47 if (!rect) return
48 const currentX = position?.x ?? rect.left
49 const currentY = position?.y ?? rect.top
50 dragStartRef.current = {
51 x: currentX,
52 y: currentY,
53 startX: e.clientX,
54 startY: e.clientY,
55 pointerId: e.pointerId,
56 }
57
58 const onMove = (moveEvent: PointerEvent) => {
59 if (dragStartRef.current?.pointerId !== moveEvent.pointerId) return
60 const dist = Math.hypot(
61 moveEvent.clientX - dragStartRef.current.startX,
62 moveEvent.clientY - dragStartRef.current.startY
63 )
64 if (dist >= DRAG_THRESHOLD_PX) {
65 navRef.current?.setPointerCapture?.(moveEvent.pointerId)
66 }
67 applyMove(moveEvent.clientX, moveEvent.clientY)
68 }
69 const onUpOrCancel = (upEvent: PointerEvent) => {
70 if (dragStartRef.current?.pointerId !== upEvent.pointerId) return
71 ;(upEvent.target as HTMLElement)?.releasePointerCapture?.(upEvent.pointerId)
72 window.removeEventListener('pointermove', onMove)
73 window.removeEventListener('pointerup', onUpOrCancel)
74 window.removeEventListener('pointercancel', onUpOrCancel)
75 dragStartRef.current = null
76 }
77 window.addEventListener('pointermove', onMove)
78 window.addEventListener('pointerup', onUpOrCancel)
79 window.addEventListener('pointercancel', onUpOrCancel)
80 },
81 [navRef, position, applyMove]
82 )
83
84 const handlePointerMove = useCallback(
85 (e: React.PointerEvent) => {
86 applyMove(e.clientX, e.clientY)
87 },
88 [applyMove]
89 )
90
91 return {
92 position,
93 dragStartRef,
94 handlePointerDown,
95 handlePointerMove,
96 }
97}