useFloatingToolbarDrag.ts 2.9 KB

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