Skip to content

Commit bf23cea

Browse files
authored
Add modifier keys to useMove (#2794)
* Add modifier keys to useMove * fix types
1 parent 4e977c9 commit bf23cea

File tree

3 files changed

+76
-43
lines changed

3 files changed

+76
-43
lines changed

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

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ interface MoveResult {
2020
moveProps: HTMLAttributes<HTMLElement>
2121
}
2222

23+
interface EventBase {
24+
shiftKey: boolean,
25+
ctrlKey: boolean,
26+
metaKey: boolean,
27+
altKey: boolean
28+
}
29+
2330
/**
2431
* Handles move interactions across mouse, touch, and keyboard, including dragging with
2532
* the mouse or touch, and using the arrow keys. Normalizes behavior across browsers and
@@ -43,7 +50,7 @@ export function useMove(props: MoveEvents): MoveResult {
4350
disableTextSelection();
4451
state.current.didMove = false;
4552
};
46-
let move = (pointerType: PointerType, deltaX: number, deltaY: number) => {
53+
let move = (originalEvent: EventBase, pointerType: PointerType, deltaX: number, deltaY: number) => {
4754
if (deltaX === 0 && deltaY === 0) {
4855
return;
4956
}
@@ -52,36 +59,48 @@ export function useMove(props: MoveEvents): MoveResult {
5259
state.current.didMove = true;
5360
onMoveStart?.({
5461
type: 'movestart',
55-
pointerType
62+
pointerType,
63+
shiftKey: originalEvent.shiftKey,
64+
metaKey: originalEvent.metaKey,
65+
ctrlKey: originalEvent.ctrlKey,
66+
altKey: originalEvent.altKey
5667
});
5768
}
5869
onMove({
5970
type: 'move',
6071
pointerType,
6172
deltaX: deltaX,
62-
deltaY: deltaY
73+
deltaY: deltaY,
74+
shiftKey: originalEvent.shiftKey,
75+
metaKey: originalEvent.metaKey,
76+
ctrlKey: originalEvent.ctrlKey,
77+
altKey: originalEvent.altKey
6378
});
6479
};
65-
let end = (pointerType: PointerType) => {
80+
let end = (originalEvent: EventBase, pointerType: PointerType) => {
6681
restoreTextSelection();
6782
if (state.current.didMove) {
6883
onMoveEnd?.({
6984
type: 'moveend',
70-
pointerType
85+
pointerType,
86+
shiftKey: originalEvent.shiftKey,
87+
metaKey: originalEvent.metaKey,
88+
ctrlKey: originalEvent.ctrlKey,
89+
altKey: originalEvent.altKey
7190
});
7291
}
7392
};
7493

7594
if (typeof PointerEvent === 'undefined') {
7695
let onMouseMove = (e: MouseEvent) => {
7796
if (e.button === 0) {
78-
move('mouse', e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY);
97+
move(e, 'mouse', e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY);
7998
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
8099
}
81100
};
82101
let onMouseUp = (e: MouseEvent) => {
83102
if (e.button === 0) {
84-
end('mouse');
103+
end(e, 'mouse');
85104
removeGlobalListener(window, 'mousemove', onMouseMove, false);
86105
removeGlobalListener(window, 'mouseup', onMouseUp, false);
87106
}
@@ -98,19 +117,17 @@ export function useMove(props: MoveEvents): MoveResult {
98117
};
99118

100119
let onTouchMove = (e: TouchEvent) => {
101-
// @ts-ignore
102120
let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
103121
if (touch >= 0) {
104122
let {pageX, pageY} = e.changedTouches[touch];
105-
move('touch', pageX - state.current.lastPosition.pageX, pageY - state.current.lastPosition.pageY);
123+
move(e, 'touch', pageX - state.current.lastPosition.pageX, pageY - state.current.lastPosition.pageY);
106124
state.current.lastPosition = {pageX, pageY};
107125
}
108126
};
109127
let onTouchEnd = (e: TouchEvent) => {
110-
// @ts-ignore
111128
let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
112129
if (touch >= 0) {
113-
end('touch');
130+
end(e, 'touch');
114131
state.current.id = null;
115132
removeGlobalListener(window, 'touchmove', onTouchMove);
116133
removeGlobalListener(window, 'touchend', onTouchEnd);
@@ -135,22 +152,20 @@ export function useMove(props: MoveEvents): MoveResult {
135152
} else {
136153
let onPointerMove = (e: PointerEvent) => {
137154
if (e.pointerId === state.current.id) {
138-
// @ts-ignore
139-
let pointerType: PointerType = e.pointerType || 'mouse';
155+
let pointerType = (e.pointerType || 'mouse') as PointerType;
140156

141157
// Problems with PointerEvent#movementX/movementY:
142158
// 1. it is always 0 on macOS Safari.
143159
// 2. On Chrome Android, it's scaled by devicePixelRatio, but not on Chrome macOS
144-
move(pointerType, e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY);
160+
move(e, pointerType, e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY);
145161
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
146162
}
147163
};
148164

149165
let onPointerUp = (e: PointerEvent) => {
150166
if (e.pointerId === state.current.id) {
151-
// @ts-ignore
152-
let pointerType: PointerType = e.pointerType || 'mouse';
153-
end(pointerType);
167+
let pointerType = (e.pointerType || 'mouse') as PointerType;
168+
end(e, pointerType);
154169
state.current.id = null;
155170
removeGlobalListener(window, 'pointermove', onPointerMove, false);
156171
removeGlobalListener(window, 'pointerup', onPointerUp, false);
@@ -172,10 +187,10 @@ export function useMove(props: MoveEvents): MoveResult {
172187
};
173188
}
174189

175-
let triggerKeyboardMove = (deltaX: number, deltaY: number) => {
190+
let triggerKeyboardMove = (e: EventBase, deltaX: number, deltaY: number) => {
176191
start();
177-
move('keyboard', deltaX, deltaY);
178-
end('keyboard');
192+
move(e, 'keyboard', deltaX, deltaY);
193+
end(e, 'keyboard');
179194
};
180195

181196
moveProps.onKeyDown = (e) => {
@@ -184,25 +199,25 @@ export function useMove(props: MoveEvents): MoveResult {
184199
case 'ArrowLeft':
185200
e.preventDefault();
186201
e.stopPropagation();
187-
triggerKeyboardMove(-1, 0);
202+
triggerKeyboardMove(e, -1, 0);
188203
break;
189204
case 'Right':
190205
case 'ArrowRight':
191206
e.preventDefault();
192207
e.stopPropagation();
193-
triggerKeyboardMove(1, 0);
208+
triggerKeyboardMove(e, 1, 0);
194209
break;
195210
case 'Up':
196211
case 'ArrowUp':
197212
e.preventDefault();
198213
e.stopPropagation();
199-
triggerKeyboardMove(0, -1);
214+
triggerKeyboardMove(e, 0, -1);
200215
break;
201216
case 'Down':
202217
case 'ArrowDown':
203218
e.preventDefault();
204219
e.stopPropagation();
205-
triggerKeyboardMove(0, 1);
220+
triggerKeyboardMove(e, 0, 1);
206221
break;
207222
}
208223
};

packages/@react-aria/interactions/test/useMove.test.js

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ describe('useMove', function () {
3333
jest.runAllTimers();
3434
});
3535

36+
afterEach(() => {
37+
// for restoreTextSelection
38+
jest.runAllTimers();
39+
});
40+
let altKey = false;
41+
let ctrlKey = false;
42+
let metaKey = false;
43+
let shiftKey = false;
44+
let defaultModifiers = {altKey, ctrlKey, metaKey, shiftKey};
45+
3646
describe('mouse events', function () {
3747
installMouseEvent();
3848

@@ -51,9 +61,9 @@ describe('useMove', function () {
5161
fireEvent.mouseDown(el, {button: 0, pageX: 1, pageY: 30});
5262
expect(events).toStrictEqual([]);
5363
fireEvent.mouseMove(el, {button: 0, pageX: 10, pageY: 25});
54-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'mouse'}, {type: 'move', pointerType: 'mouse', deltaX: 9, deltaY: -5}]);
64+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'mouse', ...defaultModifiers}, {type: 'move', pointerType: 'mouse', deltaX: 9, deltaY: -5, ...defaultModifiers}]);
5565
fireEvent.mouseUp(el);
56-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'mouse'}, {type: 'move', pointerType: 'mouse', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'mouse'}]);
66+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'mouse', ...defaultModifiers}, {type: 'move', pointerType: 'mouse', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'mouse', ...defaultModifiers}]);
5767
});
5868

5969
it('doesn\'t respond to right click', function () {
@@ -110,9 +120,9 @@ describe('useMove', function () {
110120
fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX: 1, pageY: 30}]});
111121
expect(events).toStrictEqual([]);
112122
fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]});
113-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}]);
123+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}]);
114124
fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]});
115-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'touch'}]);
125+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'touch', ...defaultModifiers}]);
116126
});
117127

118128
it('ends with touchcancel', function () {
@@ -130,9 +140,9 @@ describe('useMove', function () {
130140
fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX: 1, pageY: 30}]});
131141
expect(events).toStrictEqual([]);
132142
fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]});
133-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}]);
143+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}]);
134144
fireEvent.touchCancel(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]});
135-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'touch'}]);
145+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'touch', ...defaultModifiers}]);
136146
});
137147

138148
it('doesn\'t fire anything when tapping', function () {
@@ -170,9 +180,9 @@ describe('useMove', function () {
170180
fireEvent.touchEnd(el, {changedTouches: [{identifier: 2, pageX: 10, pageY: 40}]});
171181
expect(events).toStrictEqual([]);
172182
fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]});
173-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}]);
183+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}]);
174184
fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]});
175-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'touch'}]);
185+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'touch', ...defaultModifiers}]);
176186
});
177187
});
178188

@@ -214,7 +224,6 @@ describe('useMove', function () {
214224
expect(document.documentElement.style.webkitUserSelect).toBe('none');
215225
fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]});
216226
expect(document.documentElement.style.webkitUserSelect).toBe('none');
217-
// advance by the setTimeout in textSelection, then an additional 16ms for the requestAnimationFrame in runAfterTransition
218227
act(() => {jest.advanceTimersByTime(316);});
219228
expect(document.documentElement.style.webkitUserSelect).toBe(mockUserSelect);
220229
});
@@ -262,9 +271,9 @@ describe('useMove', function () {
262271
fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX: 1, pageY: 30}]});
263272
expect(eventsChild).toStrictEqual([]);
264273
fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]});
265-
expect(eventsChild).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}]);
274+
expect(eventsChild).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}]);
266275
fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]});
267-
expect(eventsChild).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'touch'}]);
276+
expect(eventsChild).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'touch', ...defaultModifiers}]);
268277
expect(eventsParent).toStrictEqual([]);
269278

270279
});
@@ -289,7 +298,7 @@ describe('useMove', function () {
289298
let el = tree.getByTestId(EXAMPLE_ELEMENT_TESTID);
290299

291300
fireEvent.keyDown(el, {key: Key});
292-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'keyboard'}, {type: 'move', pointerType: 'keyboard', ...Result}, {type: 'moveend', pointerType: 'keyboard'}]);
301+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'keyboard', ...defaultModifiers}, {type: 'move', pointerType: 'keyboard', ...defaultModifiers, ...Result}, {type: 'moveend', pointerType: 'keyboard', ...defaultModifiers}]);
293302
});
294303

295304
it('allows handling other key events', function () {
@@ -330,9 +339,9 @@ describe('useMove', function () {
330339
fireEvent.pointerDown(el, {pointerType: 'pen', pointerId: 1, pageX: 1, pageY: 30});
331340
expect(events).toStrictEqual([]);
332341
fireEvent.pointerMove(el, {pointerType: 'pen', pointerId: 1, pageX: 10, pageY: 25});
333-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen'}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5}]);
342+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen', ...defaultModifiers}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5, ...defaultModifiers}]);
334343
fireEvent.pointerUp(el, {pointerType: 'pen', pointerId: 1});
335-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen'}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'pen'}]);
344+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen', ...defaultModifiers}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'pen', ...defaultModifiers}]);
336345
});
337346

338347
it('doesn\'t respond to right click', function () {
@@ -370,9 +379,9 @@ describe('useMove', function () {
370379
fireEvent.pointerDown(el, {pointerType: 'pen', pointerId: 1, pageX: 1, pageY: 30});
371380
expect(events).toStrictEqual([]);
372381
fireEvent.pointerMove(el, {pointerType: 'pen', pointerId: 1, pageX: 10, pageY: 25});
373-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen'}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5}]);
382+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen', ...defaultModifiers}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5, ...defaultModifiers}]);
374383
fireEvent.pointerCancel(el, {pointerType: 'pen', pointerId: 1});
375-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen'}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'pen'}]);
384+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen', ...defaultModifiers}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'pen', ...defaultModifiers}]);
376385
});
377386

378387
it('doesn\'t fire anything when tapping', function () {
@@ -412,9 +421,10 @@ describe('useMove', function () {
412421

413422
expect(events).toStrictEqual([]);
414423
fireEvent.pointerMove(el, {pointerType: 'pen', pointerId: 1, pageX: 10, pageY: 25});
415-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen'}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5}]);
424+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen', ...defaultModifiers}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5, ...defaultModifiers}]);
416425
fireEvent.pointerUp(el, {pointerType: 'pen', pointerId: 1});
417-
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen'}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'pen'}]);
426+
expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen', ...defaultModifiers}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'pen', ...defaultModifiers}]);
418427
});
419428
});
420429
});
430+

packages/@react-types/shared/src/events.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,15 @@ export interface FocusableProps extends FocusEvents, KeyboardEvents {
110110

111111
interface BaseMoveEvent {
112112
/** The pointer type that triggered the move event. */
113-
pointerType: PointerType
113+
pointerType: PointerType,
114+
/** Whether the shift keyboard modifier was held during the move event. */
115+
shiftKey: boolean,
116+
/** Whether the ctrl keyboard modifier was held during the move event. */
117+
ctrlKey: boolean,
118+
/** Whether the meta keyboard modifier was held during the move event. */
119+
metaKey: boolean,
120+
/** Whether the alt keyboard modifier was held during the move event. */
121+
altKey: boolean
114122
}
115123

116124
export interface MoveStartEvent extends BaseMoveEvent {

0 commit comments

Comments
 (0)