Skip to content

Commit ce1cef6

Browse files
committed
Multitouch
1 parent a951c0c commit ce1cef6

File tree

6 files changed

+182
-129
lines changed

6 files changed

+182
-129
lines changed

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

Lines changed: 74 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -39,36 +39,25 @@ export interface MoveProps {
3939
onMoveEnd?: (e: MoveEndEvent) => void
4040
}
4141

42-
// const currentTargets: Set<HTMLElement> = new Set();
43-
let currentTarget: HTMLElement | null = null;
44-
4542
export function useMove(props: MoveProps): HTMLAttributes<HTMLElement> {
4643
let {onMoveStart, onMove, onMoveEnd} = props;
4744

48-
let state = useRef({movedAfterDown: false, previousPosition: null});
45+
let state = useRef<{
46+
didMove: boolean,
47+
lastPosition: {pageX: number, pageY: number} | null,
48+
id: number | null
49+
}>({didMove: false, lastPosition: null, id: null});
4950

5051
let moveProps = useMemo(() => {
5152
let moveProps: HTMLAttributes<HTMLElement> = {};
5253

53-
let start = (target?: any) => {
54-
// Only move innermost element that is using useMove, not potential parents.
55-
if (target) {
56-
if (currentTarget) {
57-
return false;
58-
}
59-
// if ([...currentTargets].some(e => e.contains(target))) {
60-
// return false;
61-
// }
62-
// currentTargets.add(target);
63-
currentTarget = target;
64-
}
54+
let start = () => {
6555
disableTextSelection();
66-
state.current.movedAfterDown = false;
67-
return true;
56+
state.current.didMove = false;
6857
};
6958
let move = (pointerType: BaseMoveEvent['pointerType'], deltaX: number, deltaY: number) => {
70-
if (!state.current.movedAfterDown) {
71-
state.current.movedAfterDown = true;
59+
if (!state.current.didMove) {
60+
state.current.didMove = true;
7261
onMoveStart?.({
7362
type: 'movestart',
7463
pointerType
@@ -81,18 +70,9 @@ export function useMove(props: MoveProps): HTMLAttributes<HTMLElement> {
8170
deltaY: deltaY
8271
});
8372
};
84-
let end = (pointerType: BaseMoveEvent['pointerType']/* , target?: any */) => {
85-
currentTarget = null;
86-
// if (target) {
87-
// for (let e of currentTargets) {
88-
// // The cursor might be let go on some parent element.
89-
// if (target.contains(e) || e.contains(target)) {
90-
// currentTargets.delete(e);
91-
// }
92-
// }
93-
// }
73+
let end = (pointerType: BaseMoveEvent['pointerType']) => {
9474
restoreTextSelection();
95-
if (state.current.movedAfterDown) {
75+
if (state.current.didMove) {
9676
onMoveEnd?.({
9777
type: 'moveend',
9878
pointerType
@@ -102,73 +82,95 @@ export function useMove(props: MoveProps): HTMLAttributes<HTMLElement> {
10282

10383
if (typeof PointerEvent === 'undefined') {
10484
let onMouseMove = (e: MouseEvent) => {
105-
move('mouse', e.pageX - state.current.previousPosition.pageX, e.pageY - state.current.previousPosition.pageY);
106-
state.current.previousPosition = {pageX: e.pageX, pageY: e.pageY};
85+
if (e.button === 0) {
86+
move('mouse', e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY);
87+
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
88+
}
10789
};
108-
let onMouseUp = (/* e: MouseEvent */) => {
109-
end('mouse'/* , e.target */);
110-
window.removeEventListener('mousemove', onMouseMove, false);
111-
window.removeEventListener('mouseup', onMouseUp, false);
90+
let onMouseUp = (e: MouseEvent) => {
91+
if (e.button === 0) {
92+
end('mouse');
93+
window.removeEventListener('mousemove', onMouseMove, false);
94+
window.removeEventListener('mouseup', onMouseUp, false);
95+
}
11296
};
11397
moveProps.onMouseDown = (e: React.MouseEvent) => {
114-
if (e.button === 0 && start(e.target)) {
98+
if (e.button === 0) {
99+
start();
115100
e.stopPropagation();
116101
e.preventDefault();
117-
state.current.previousPosition = {pageX: e.pageX, pageY: e.pageY};
102+
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
118103
window.addEventListener('mousemove', onMouseMove, false);
119104
window.addEventListener('mouseup', onMouseUp, false);
120105
}
121106
};
122107

123108
let onTouchMove = (e: TouchEvent) => {
124-
// TODO which touch?
125-
let {pageX, pageY} = e.targetTouches[0];
126-
move('touch', pageX - state.current.previousPosition.pageX, pageY - state.current.previousPosition.pageY);
127-
state.current.previousPosition = {pageX, pageY};
109+
// @ts-ignore
110+
let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
111+
if (touch >= 0) {
112+
let {pageX, pageY} = e.changedTouches[touch];
113+
move('touch', pageX - state.current.lastPosition.pageX, pageY - state.current.lastPosition.pageY);
114+
state.current.lastPosition = {pageX, pageY};
115+
}
128116
};
129-
let onTouchEnd = (/* e: TouchEvent */) => {
130-
end('touch'/* , e.target */);
131-
window.removeEventListener('touchmove', onTouchMove);
132-
window.removeEventListener('touchend', onTouchEnd);
133-
window.removeEventListener('touchcancel', onTouchEnd);
117+
let onTouchEnd = (e: TouchEvent) => {
118+
// @ts-ignore
119+
let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
120+
if (touch >= 0) {
121+
end('touch');
122+
window.removeEventListener('touchmove', onTouchMove);
123+
window.removeEventListener('touchend', onTouchEnd);
124+
window.removeEventListener('touchcancel', onTouchEnd);
125+
}
134126
};
135127
moveProps.onTouchStart = (e: React.TouchEvent) => {
136-
if (start(e.target)) {
137-
e.stopPropagation();
138-
e.preventDefault();
139-
let {pageX, pageY} = e.targetTouches[0];
140-
state.current.previousPosition = {pageX, pageY};
141-
window.addEventListener('touchmove', onTouchMove, false);
142-
window.addEventListener('touchend', onTouchEnd, false);
143-
window.addEventListener('touchcancel', onTouchEnd, false);
128+
if (e.targetTouches.length === 0) {
129+
return;
144130
}
131+
132+
let {pageX, pageY, identifier} = e.targetTouches[0];
133+
start();
134+
e.stopPropagation();
135+
e.preventDefault();
136+
state.current.lastPosition = {pageX, pageY};
137+
state.current.id = identifier;
138+
window.addEventListener('touchmove', onTouchMove, false);
139+
window.addEventListener('touchend', onTouchEnd, false);
140+
window.addEventListener('touchcancel', onTouchEnd, false);
145141
};
146142
} else {
147143
let onPointerMove = (e: PointerEvent) => {
148-
// @ts-ignore
149-
let pointerType: BaseMoveEvent['pointerType'] = e.pointerType || 'mouse';
150-
151-
// Problems with PointerEvent#movementX/movementY:
152-
// 1. it is always 0 on macOS Safari.
153-
// 2. On Chrome Android, it's scaled by devicePixelRatio, but not on Chrome macOS
154-
move(pointerType, e.pageX - state.current.previousPosition.pageX, e.pageY - state.current.previousPosition.pageY);
155-
state.current.previousPosition = {pageX: e.pageX, pageY: e.pageY};
144+
if (e.pointerId === state.current.id) {
145+
// @ts-ignore
146+
let pointerType: BaseMoveEvent['pointerType'] = e.pointerType || 'mouse';
147+
148+
// Problems with PointerEvent#movementX/movementY:
149+
// 1. it is always 0 on macOS Safari.
150+
// 2. On Chrome Android, it's scaled by devicePixelRatio, but not on Chrome macOS
151+
move(pointerType, e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY);
152+
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
153+
}
156154
};
157155

158156
let onPointerUp = (e: PointerEvent) => {
159-
// @ts-ignore
160-
let pointerType: BaseMoveEvent['pointerType'] = e.pointerType || 'mouse';
161-
end(pointerType/* , e.target */);
162-
window.removeEventListener('pointermove', onPointerMove, false);
163-
window.removeEventListener('pointerup', onPointerUp, false);
164-
window.removeEventListener('pointercancel', onPointerUp, false);
157+
if (e.pointerId === state.current.id) {
158+
// @ts-ignore
159+
let pointerType: BaseMoveEvent['pointerType'] = e.pointerType || 'mouse';
160+
end(pointerType/* , e.target */);
161+
window.removeEventListener('pointermove', onPointerMove, false);
162+
window.removeEventListener('pointerup', onPointerUp, false);
163+
window.removeEventListener('pointercancel', onPointerUp, false);
164+
}
165165
};
166166

167167
moveProps.onPointerDown = (e: React.PointerEvent) => {
168-
if (e.button === 0 && start(e.target)) {
168+
if (e.button === 0) {
169+
start();
169170
e.stopPropagation();
170171
e.preventDefault();
171-
state.current.previousPosition = {pageX: e.pageX, pageY: e.pageY};
172+
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
173+
state.current.id = e.pointerId;
172174
window.addEventListener('pointermove', onPointerMove, false);
173175
window.addEventListener('pointerup', onPointerUp, false);
174176
window.addEventListener('pointercancel', onPointerUp, false);

packages/@react-aria/interactions/stories/useMove.stories.tsx

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414

1515
import {action} from '@storybook/addon-actions';
1616
import {clamp} from '@react-aria/utils';
17+
import {Flex} from '@react-spectrum/layout';
1718
import React, {useRef, useState} from 'react';
1819
import {storiesOf} from '@storybook/react';
1920
import {useMove} from '../';
2021

21-
export function useClampedMove(props) {
22+
function useClampedMove(props) {
2223
let currentPosition = useRef<{x?: number, y?: number}>();
2324

2425
let {getCurrentState, onMoveTo, onMoveStart, onMoveEnd, reverseX = false, reverseY = false} = props;
@@ -43,6 +44,25 @@ export function useClampedMove(props) {
4344
return moveProps;
4445
}
4546

47+
function Ball1D() {
48+
let [state, setState] = useState({x: 0, color: 'black'});
49+
50+
let props = useClampedMove({
51+
linear: 'horizontal',
52+
reverseY: true,
53+
onMoveStart() { setState((state) => ({...state, color: 'red'})); },
54+
onMoveTo({x}) {
55+
setState((state) => ({...state, x: clamp(x, 0, 200 - 30), y: 0}));
56+
},
57+
getCurrentState() { return {x: state.x, y: 0}; },
58+
onMoveEnd() { setState((state) => ({...state, color: 'black'})); }
59+
});
60+
61+
return (<div style={{width: '200px', height: '30px', background: 'white', border: '1px solid black', position: 'relative', touchAction: 'none'}}>
62+
<div tabIndex={0} {...props} style={{width: '30px', height: '30px', borderRadius: '100%', position: 'absolute', left: state.x + 'px', background: state.color}} />
63+
</div>);
64+
}
65+
4666
storiesOf('useMove', module)
4767
.add(
4868
'Log',
@@ -58,24 +78,10 @@ storiesOf('useMove', module)
5878
)
5979
.add(
6080
'Ball 1D',
61-
() => {
62-
let [state, setState] = useState({x: 0, color: 'black'});
63-
64-
let props = useClampedMove({
65-
linear: 'horizontal',
66-
reverseY: true,
67-
onMoveStart() { setState((state) => ({...state, color: 'red'})); },
68-
onMoveTo({x}) {
69-
setState((state) => ({...state, x: clamp(x, 0, 200 - 30), y: 0}));
70-
},
71-
getCurrentState() { return {x: state.x, y: 0}; },
72-
onMoveEnd() { setState((state) => ({...state, color: 'black'})); }
73-
});
74-
75-
return (<div style={{width: '200px', height: '30px', background: 'white', border: '1px solid black', position: 'relative', touchAction: 'none'}}>
76-
<div tabIndex={0} {...props} style={{width: '30px', height: '30px', borderRadius: '100%', position: 'absolute', left: state.x + 'px', background: state.color}} />
77-
</div>);
78-
}
81+
() => (<Flex direction="column" gap="size-1000">
82+
<Ball1D />
83+
<Ball1D />
84+
</Flex>)
7985
)
8086
.add(
8187
'Ball 2D',

0 commit comments

Comments
 (0)