11import React , {
2+ MutableRefObject ,
23 RefCallback ,
34 useCallback ,
45 useEffect ,
56 useRef ,
67 useState ,
78} from "react" ;
9+ import { Rect } from "./useDragToResize" ;
810
911// https://stackoverflow.com/a/39192992/6236042
1012
@@ -16,52 +18,28 @@ function hasStyle(value: unknown): value is StyledElement {
1618 return ! ! value && typeof value === "object" && "style" in value ;
1719}
1820
19- const id = ( x : { x : number ; y : number } ) => x ;
20-
2121interface UseDraggableProps {
22- onDrag ?: ( x : { x : number ; y : number } ) => {
23- x : number ;
24- y : number ;
25- } ;
2622 /**
2723 * A reference to the element that can be used to move the draggable element.
2824 * If omitted, clicking anywhere on the entire draggable element will
2925 * allow you to move it.
3026 */
3127 moveRef ?: React . RefObject < HTMLElement > | null ;
28+
29+ elementRect : MutableRefObject < Partial < Rect > > ;
3230}
3331
3432// complex logic should be a hook, not a component
3533const useDragToMove = ( {
36- onDrag = id ,
3734 moveRef = null ,
35+ elementRect,
3836} : UseDraggableProps ) : [ React . RefCallback < HTMLElement > , boolean ] => {
39- // this state doesn't change often, so it's fine
4037 const [ pressed , setPressed ] = useState ( false ) ;
41-
42- // do not store position in useState! even if you useEffect on
43- // it and update `transform` CSS property, React still rerenders
44- // on every state change, and it LAGS
45- const position = useRef < { x : number ; y : number } > ( { x : 0 , y : 0 } ) ;
4638 const ref = useRef < HTMLElement | null > ( null ) ;
4739
48- // we've moved the code into the hook, and it would be weird to
49- // return `ref` and `handleMouseDown` to be set on the same element
50- // why not just do the job on our own here and use a function-ref
51- // to subscribe to `mousedown` too? it would go like this:
5240 const unsubscribe = useRef < VoidFunction > ( ) ;
5341 const legacyRef : RefCallback < HTMLElement > = useCallback (
5442 ( elem ) => {
55- // in a production version of this code I'd use a
56- // `useComposeRef` hook to compose function-ref and object-ref
57- // into one ref, and then would return it. combining
58- // hooks in this way by hand is error-prone
59-
60- // then I'd also split out the rest of this function into a
61- // separate hook to be called like this:
62- // const legacyRef = useDomEvent('mousedown');
63- // const combinedRef = useCombinedRef(ref, legacyRef);
64- // return [combinedRef, pressed];
6543 ref . current = elem ;
6644 if ( unsubscribe . current ) {
6745 unsubscribe . current ;
@@ -85,37 +63,26 @@ const useDragToMove = ({
8563 ) ;
8664
8765 useEffect ( ( ) => {
88- // why subscribe in a `useEffect`? because we want to subscribe
89- // to mousemove only when pressed, otherwise it will lag even
90- // when you're not dragging
9166 if ( ! pressed ) {
9267 return ;
9368 }
9469
95- // updating the page without any throttling is a bad idea
96- // requestAnimationFrame-based throttle would probably be fine,
97- // but be aware that naive implementation might make element
98- // lag 1 frame behind cursor, and it will appear to be lagging
99- // even at 60 FPS
10070 const handleMouseMove = throttle (
10171 ( event : { movementX : number ; movementY : number } ) => {
102- // needed for TypeScript anyway
103- if ( ! ref . current || ! position . current ) {
72+ if ( ! ref . current || ! elementRect . current ) {
10473 return ;
10574 }
106- const pos = position . current ;
107- // it's important to save it into variable here,
108- // otherwise we might capture reference to an element
109- // that was long gone. not really sure what's correct
110- // behavior for a case when you've been scrolling, and
111- // the target element was replaced. probably some formulae
112- // needed to handle that case. TODO
75+
76+ elementRect . current . left =
77+ ( elementRect . current . left ?? 0 ) + event . movementX ;
78+
79+ elementRect . current . top =
80+ ( elementRect . current . top ?? 0 ) + event . movementY ;
81+
11382 const elem = moveRef ?. current ?? ref . current ;
114- position . current = onDrag ( {
115- x : pos . x + event . movementX ,
116- y : pos . y + event . movementY ,
117- } ) ;
118- elem . style . transform = `translate(${ position . current . x } px, ${ position . current . y } px)` ;
83+
84+ elem . style . left = `${ elementRect . current . left } px` ;
85+ elem . style . top = `${ elementRect . current . top } px` ;
11986 }
12087 ) ;
12188 const handleMouseUp = ( e : MouseEvent ) => {
@@ -138,7 +105,7 @@ const useDragToMove = ({
138105 // if `onDrag` wasn't defined with `useCallback`, we'd have to
139106 // resubscribe to 2 DOM events here, not to say it would mess
140107 // with `throttle` and reset its internal timer
141- } , [ pressed , onDrag , moveRef ] ) ;
108+ } , [ pressed , moveRef , elementRect ] ) ;
142109
143110 // actually it makes sense to return an array only when
144111 // you expect that on the caller side all of the fields
0 commit comments