Skip to content

Commit 7902702

Browse files
authored
fix: usePress and useMove global event attachment timing (#9749)
* fix: 9744 compiler lint issue * missed eslint * fix lint * same operation applied to usePress
1 parent d74ddc0 commit 7902702

2 files changed

Lines changed: 230 additions & 263 deletions

File tree

packages/@react-aria/interactions/src/useMove.ts

Lines changed: 62 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
import {disableTextSelection, restoreTextSelection} from './textSelection';
1414
import {DOMAttributes, MoveEvents, PointerType} from '@react-types/shared';
15-
import React, {useCallback, useMemo, useRef, useState} from 'react';
16-
import {useEffectEvent, useGlobalListeners, useLayoutEffect} from '@react-aria/utils';
15+
import React, {useCallback, useMemo, useRef} from 'react';
16+
import {useEffectEvent, useGlobalListeners} from '@react-aria/utils';
1717

1818
export interface MoveResult {
1919
/** Props to spread on the target element. */
@@ -87,108 +87,62 @@ export function useMove(props: MoveEvents): MoveResult {
8787
}, [onMoveEnd, state]);
8888
let endEvent = useEffectEvent(end);
8989

90-
let [pointerDown, setPointerDown] = useState<'pointer' | 'mouse' | 'touch' | null>(null);
91-
useLayoutEffect(() => {
92-
if (pointerDown === 'pointer') {
93-
let onPointerMove = (e: PointerEvent) => {
94-
if (e.pointerId === state.current.id) {
95-
let pointerType = (e.pointerType || 'mouse') as PointerType;
90+
let moveProps = useMemo(() => {
91+
let moveProps: DOMAttributes = {};
9692

97-
// Problems with PointerEvent#movementX/movementY:
98-
// 1. it is always 0 on macOS Safari.
99-
// 2. On Chrome Android, it's scaled by devicePixelRatio, but not on Chrome macOS
100-
moveEvent(e, pointerType, e.pageX - (state.current.lastPosition?.pageX ?? 0), e.pageY - (state.current.lastPosition?.pageY ?? 0));
101-
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
102-
}
103-
};
93+
let start = () => {
94+
disableTextSelection();
95+
state.current.didMove = false;
96+
};
10497

105-
let onPointerUp = (e: PointerEvent) => {
106-
if (e.pointerId === state.current.id) {
107-
let pointerType = (e.pointerType || 'mouse') as PointerType;
108-
endEvent(e, pointerType);
109-
state.current.id = null;
110-
removeGlobalListener(window, 'pointermove', onPointerMove, false);
111-
removeGlobalListener(window, 'pointerup', onPointerUp, false);
112-
removeGlobalListener(window, 'pointercancel', onPointerUp, false);
113-
setPointerDown(null);
114-
}
115-
};
116-
addGlobalListener(window, 'pointermove', onPointerMove, false);
117-
addGlobalListener(window, 'pointerup', onPointerUp, false);
118-
addGlobalListener(window, 'pointercancel', onPointerUp, false);
119-
return () => {
120-
removeGlobalListener(window, 'pointermove', onPointerMove, false);
121-
removeGlobalListener(window, 'pointerup', onPointerUp, false);
122-
removeGlobalListener(window, 'pointercancel', onPointerUp, false);
123-
};
124-
} else if (pointerDown === 'mouse' && process.env.NODE_ENV === 'test') {
98+
if (typeof PointerEvent === 'undefined' && process.env.NODE_ENV === 'test') {
12599
let onMouseMove = (e: MouseEvent) => {
126100
if (e.button === 0) {
101+
// Should be safe to use the useEffectEvent because these are equivalent https://github.com/reactjs/react.dev/issues/8075#issuecomment-3400179389
102+
// However, the compiler is not smart enough to know that. As such, this whole file must be manually optimised as the compiler will bail.
103+
//
104+
// eslint-disable-next-line react-hooks/rules-of-hooks
127105
moveEvent(e, 'mouse', e.pageX - (state.current.lastPosition?.pageX ?? 0), e.pageY - (state.current.lastPosition?.pageY ?? 0));
128106
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
129107
}
130108
};
131109
let onMouseUp = (e: MouseEvent) => {
132110
if (e.button === 0) {
111+
// eslint-disable-next-line react-hooks/rules-of-hooks
133112
endEvent(e, 'mouse');
134113
removeGlobalListener(window, 'mousemove', onMouseMove, false);
135114
removeGlobalListener(window, 'mouseup', onMouseUp, false);
136-
setPointerDown(null);
137115
}
138116
};
139-
addGlobalListener(window, 'mousemove', onMouseMove, false);
140-
addGlobalListener(window, 'mouseup', onMouseUp, false);
141-
return () => {
142-
removeGlobalListener(window, 'mousemove', onMouseMove, false);
143-
removeGlobalListener(window, 'mouseup', onMouseUp, false);
117+
moveProps.onMouseDown = (e: React.MouseEvent) => {
118+
if (e.button === 0) {
119+
start();
120+
e.stopPropagation();
121+
e.preventDefault();
122+
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
123+
addGlobalListener(window, 'mousemove', onMouseMove, false);
124+
addGlobalListener(window, 'mouseup', onMouseUp, false);
125+
}
144126
};
145-
} else if (pointerDown === 'touch' && process.env.NODE_ENV === 'test') {
127+
146128
let onTouchMove = (e: TouchEvent) => {
147129
let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
148130
if (touch >= 0) {
149131
let {pageX, pageY} = e.changedTouches[touch];
132+
// eslint-disable-next-line react-hooks/rules-of-hooks
150133
moveEvent(e, 'touch', pageX - (state.current.lastPosition?.pageX ?? 0), pageY - (state.current.lastPosition?.pageY ?? 0));
151134
state.current.lastPosition = {pageX, pageY};
152135
}
153136
};
154137
let onTouchEnd = (e: TouchEvent) => {
155138
let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
156139
if (touch >= 0) {
140+
// eslint-disable-next-line react-hooks/rules-of-hooks
157141
endEvent(e, 'touch');
158142
state.current.id = null;
159143
removeGlobalListener(window, 'touchmove', onTouchMove);
160144
removeGlobalListener(window, 'touchend', onTouchEnd);
161145
removeGlobalListener(window, 'touchcancel', onTouchEnd);
162-
setPointerDown(null);
163-
}
164-
};
165-
addGlobalListener(window, 'touchmove', onTouchMove, false);
166-
addGlobalListener(window, 'touchend', onTouchEnd, false);
167-
addGlobalListener(window, 'touchcancel', onTouchEnd, false);
168-
return () => {
169-
removeGlobalListener(window, 'touchmove', onTouchMove, false);
170-
removeGlobalListener(window, 'touchend', onTouchEnd, false);
171-
removeGlobalListener(window, 'touchcancel', onTouchEnd, false);
172-
};
173-
}
174-
}, [pointerDown, addGlobalListener, removeGlobalListener]);
175-
176-
let moveProps = useMemo(() => {
177-
let moveProps: DOMAttributes = {};
178-
179-
let start = () => {
180-
disableTextSelection();
181-
state.current.didMove = false;
182-
};
183-
184-
if (typeof PointerEvent === 'undefined' && process.env.NODE_ENV === 'test') {
185-
moveProps.onMouseDown = (e: React.MouseEvent) => {
186-
if (e.button === 0) {
187-
start();
188-
e.stopPropagation();
189-
e.preventDefault();
190-
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
191-
setPointerDown('mouse');
192146
}
193147
};
194148
moveProps.onTouchStart = (e: React.TouchEvent) => {
@@ -202,25 +156,56 @@ export function useMove(props: MoveEvents): MoveResult {
202156
e.preventDefault();
203157
state.current.lastPosition = {pageX, pageY};
204158
state.current.id = identifier;
205-
setPointerDown('touch');
159+
addGlobalListener(window, 'touchmove', onTouchMove, false);
160+
addGlobalListener(window, 'touchend', onTouchEnd, false);
161+
addGlobalListener(window, 'touchcancel', onTouchEnd, false);
206162
};
207163
} else {
164+
let onPointerMove = (e: PointerEvent) => {
165+
if (e.pointerId === state.current.id) {
166+
let pointerType = (e.pointerType || 'mouse') as PointerType;
167+
168+
// Problems with PointerEvent#movementX/movementY:
169+
// 1. it is always 0 on macOS Safari.
170+
// 2. On Chrome Android, it's scaled by devicePixelRatio, but not on Chrome macOS
171+
// eslint-disable-next-line react-hooks/rules-of-hooks
172+
moveEvent(e, pointerType, e.pageX - (state.current.lastPosition?.pageX ?? 0), e.pageY - (state.current.lastPosition?.pageY ?? 0));
173+
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
174+
}
175+
};
176+
177+
let onPointerUp = (e: PointerEvent) => {
178+
if (e.pointerId === state.current.id) {
179+
let pointerType = (e.pointerType || 'mouse') as PointerType;
180+
// eslint-disable-next-line react-hooks/rules-of-hooks
181+
endEvent(e, pointerType);
182+
state.current.id = null;
183+
removeGlobalListener(window, 'pointermove', onPointerMove, false);
184+
removeGlobalListener(window, 'pointerup', onPointerUp, false);
185+
removeGlobalListener(window, 'pointercancel', onPointerUp, false);
186+
}
187+
};
188+
208189
moveProps.onPointerDown = (e: React.PointerEvent) => {
209190
if (e.button === 0 && state.current.id == null) {
210191
start();
211192
e.stopPropagation();
212193
e.preventDefault();
213194
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
214195
state.current.id = e.pointerId;
215-
setPointerDown('pointer');
196+
addGlobalListener(window, 'pointermove', onPointerMove, false);
197+
addGlobalListener(window, 'pointerup', onPointerUp, false);
198+
addGlobalListener(window, 'pointercancel', onPointerUp, false);
216199
}
217200
};
218201
}
219202

220203
let triggerKeyboardMove = (e: EventBase, deltaX: number, deltaY: number) => {
221204
start();
222-
move(e, 'keyboard', deltaX, deltaY);
223-
end(e, 'keyboard');
205+
// eslint-disable-next-line react-hooks/rules-of-hooks
206+
moveEvent(e, 'keyboard', deltaX, deltaY);
207+
// eslint-disable-next-line react-hooks/rules-of-hooks
208+
endEvent(e, 'keyboard');
224209
};
225210

226211
moveProps.onKeyDown = (e) => {
@@ -253,7 +238,7 @@ export function useMove(props: MoveEvents): MoveResult {
253238
};
254239

255240
return moveProps;
256-
}, [state, move, end]);
241+
}, [addGlobalListener, removeGlobalListener, state]);
257242

258243
return {moveProps};
259244
}

0 commit comments

Comments
 (0)