Skip to content

Commit 7d49f0f

Browse files
Combine the useMove frequently appears with useKeyboard (#2508)
* Combine the useMove frequently appears with useKeyboard * fix merge mistake * Remove isPage from useMove, add modifier keys * continue propagation for keys we don't handle * have aria pass through the step size Co-authored-by: Rob Snow <[email protected]>
1 parent 9c2769f commit 7d49f0f

File tree

9 files changed

+200
-144
lines changed

9 files changed

+200
-144
lines changed

packages/@react-aria/color/src/useColorArea.ts

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -68,44 +68,34 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i
6868
let {keyboardProps} = useKeyboard({
6969
onKeyDown(e) {
7070
// these are the cases that useMove doesn't handle
71-
if (!((e.shiftKey && /^Arrow(?:Right|Left|Up|Down)$/.test(e.key)) || /^(PageUp|PageDown|Home|End)$/.test(e.key))) {
71+
if (!/^(PageUp|PageDown|Home|End)$/.test(e.key)) {
72+
e.continuePropagation();
7273
return;
7374
}
75+
// same handling as useMove, don't need to stop propagation, useKeyboard will do that for us
76+
e.preventDefault();
77+
// remember to set this and unset it so that onChangeEnd is fired
7478
stateRef.current.setDragging(true);
75-
let isPage = e.shiftKey;
7679
switch (e.key) {
7780
case 'PageUp':
78-
isPage = true;
79-
case 'ArrowUp':
80-
case 'Up':
81-
stateRef.current.incrementY(isPage);
81+
stateRef.current.incrementY(stateRef.current.yChannelPageStep);
8282
focusedInputRef.current = inputYRef.current;
8383
break;
8484
case 'PageDown':
85-
isPage = true;
86-
case 'ArrowDown':
87-
case 'Down':
88-
stateRef.current.decrementY(isPage);
85+
stateRef.current.decrementY(stateRef.current.yChannelPageStep);
8986
focusedInputRef.current = inputYRef.current;
9087
break;
9188
case 'Home':
92-
isPage = true;
93-
case 'ArrowLeft':
94-
case 'Left':
95-
direction === 'rtl' ? stateRef.current.incrementX(isPage) : stateRef.current.decrementX(isPage);
89+
direction === 'rtl' ? stateRef.current.incrementX(stateRef.current.xChannelPageStep) : stateRef.current.decrementX(stateRef.current.xChannelPageStep);
9690
focusedInputRef.current = inputXRef.current;
9791
break;
9892
case 'End':
99-
isPage = true;
100-
case 'ArrowRight':
101-
case 'Right':
102-
direction === 'rtl' ? stateRef.current.decrementX(isPage) : stateRef.current.incrementX(isPage);
93+
direction === 'rtl' ? stateRef.current.decrementX(stateRef.current.xChannelPageStep) : stateRef.current.incrementX(stateRef.current.xChannelPageStep);
10394
focusedInputRef.current = inputXRef.current;
10495
break;
10596
}
10697
stateRef.current.setDragging(false);
10798
if (focusedInputRef.current) {
108-
e.preventDefault();
10999
focusInput(focusedInputRef.current ? focusedInputRef : inputXRef);
110100
focusedInputRef.current = undefined;
111101
}
@@ -117,24 +107,26 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i
117107
currentPosition.current = null;
118108
stateRef.current.setDragging(true);
119109
},
120-
onMove({deltaX, deltaY, pointerType}) {
110+
onMove({deltaX, deltaY, pointerType, shiftKey}) {
121111
if (currentPosition.current == null) {
122112
currentPosition.current = stateRef.current.getThumbPosition();
123113
}
124114
let {width, height} = containerRef.current.getBoundingClientRect();
125115
if (pointerType === 'keyboard') {
126-
if (deltaX > 0 || deltaX < 0) {
127-
stateRef.current[`${deltaX > 0 ? 'increment' : 'decrement'}X`]();
128-
}
129-
if (deltaY > 0 || deltaY < 0) {
130-
stateRef.current[`${deltaY < 0 ? 'increment' : 'decrement'}Y`]();
116+
if (deltaX > 0) {
117+
stateRef.current.incrementX(shiftKey ? stateRef.current.xChannelPageStep : stateRef.current.xChannelStep);
118+
} else if (deltaX < 0) {
119+
stateRef.current.decrementX(shiftKey ? stateRef.current.xChannelPageStep : stateRef.current.xChannelStep);
120+
} else if (deltaY > 0) {
121+
stateRef.current.decrementY(shiftKey ? stateRef.current.yChannelPageStep : stateRef.current.yChannelStep);
122+
} else if (deltaY < 0) {
123+
stateRef.current.incrementY(shiftKey ? stateRef.current.yChannelPageStep : stateRef.current.yChannelStep);
131124
}
132125
// set the focused input based on which axis has the greater delta
133126
focusedInputRef.current = (deltaX !== 0 || deltaY !== 0) && Math.abs(deltaY) > Math.abs(deltaX) ? inputYRef.current : inputXRef.current;
134-
}
135-
currentPosition.current.x += (direction === 'rtl' ? -1 : 1) * deltaX / width ;
136-
currentPosition.current.y += deltaY / height;
137-
if (pointerType !== 'keyboard') {
127+
} else {
128+
currentPosition.current.x += (direction === 'rtl' ? -1 : 1) * deltaX / width ;
129+
currentPosition.current.y += deltaY / height;
138130
stateRef.current.setColorFromPoint(currentPosition.current.x, currentPosition.current.y);
139131
}
140132
},
@@ -279,8 +271,6 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i
279271
}
280272
})
281273
}, keyboardProps, movePropsThumb);
282-
// order matters, keyboard need to finish before move so that onChangeEnd is fired last
283-
// after valueRef in stately has been updated
284274

285275

286276
let xInputLabellingProps = useLabels({

packages/@react-aria/color/src/useColorWheel.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ interface ColorWheelAria {
3333
inputProps: InputHTMLAttributes<HTMLInputElement>
3434
}
3535

36-
const PAGE_MIN_STEP_SIZE = 6;
37-
3836
/**
3937
* Provides the behavior and accessibility implementation for a color wheel component.
4038
* Color wheels allow users to adjust the hue of an HSL or HSB color value on a circular track.
@@ -62,22 +60,48 @@ export function useColorWheel(props: ColorWheelAriaProps, state: ColorWheelState
6260
stateRef.current = state;
6361

6462
let currentPosition = useRef<{x: number, y: number}>(null);
63+
64+
let {keyboardProps} = useKeyboard({
65+
onKeyDown(e) {
66+
// these are the cases that useMove doesn't handle
67+
if (!/^(PageUp|PageDown)$/.test(e.key)) {
68+
e.continuePropagation();
69+
return;
70+
}
71+
// same handling as useMove, don't need to stop propagation, useKeyboard will do that for us
72+
e.preventDefault();
73+
// remember to set this and unset it so that onChangeEnd is fired
74+
stateRef.current.setDragging(true);
75+
switch (e.key) {
76+
case 'PageUp':
77+
e.preventDefault();
78+
state.increment(stateRef.current.pageStep);
79+
break;
80+
case 'PageDown':
81+
e.preventDefault();
82+
state.decrement(stateRef.current.pageStep);
83+
break;
84+
}
85+
stateRef.current.setDragging(false);
86+
}
87+
});
88+
6589
let moveHandler = {
6690
onMoveStart() {
6791
currentPosition.current = null;
6892
state.setDragging(true);
6993
},
70-
onMove({deltaX, deltaY, pointerType}) {
94+
onMove({deltaX, deltaY, pointerType, shiftKey}) {
7195
if (currentPosition.current == null) {
7296
currentPosition.current = stateRef.current.getThumbPosition(thumbRadius);
7397
}
7498
currentPosition.current.x += deltaX;
7599
currentPosition.current.y += deltaY;
76100
if (pointerType === 'keyboard') {
77101
if (deltaX > 0 || deltaY < 0) {
78-
state.increment();
102+
state.increment(shiftKey ? stateRef.current.pageStep : stateRef.current.step);
79103
} else if (deltaX < 0 || deltaY > 0) {
80-
state.decrement();
104+
state.decrement(shiftKey ? stateRef.current.pageStep : stateRef.current.step);
81105
}
82106
} else {
83107
stateRef.current.setHueFromPoint(currentPosition.current.x, currentPosition.current.y, thumbRadius);
@@ -169,21 +193,6 @@ export function useColorWheel(props: ColorWheelAriaProps, state: ColorWheelState
169193
}
170194
};
171195

172-
let {keyboardProps} = useKeyboard({
173-
onKeyDown(e) {
174-
switch (e.key) {
175-
case 'PageUp':
176-
e.preventDefault();
177-
state.increment(PAGE_MIN_STEP_SIZE);
178-
break;
179-
case 'PageDown':
180-
e.preventDefault();
181-
state.decrement(PAGE_MIN_STEP_SIZE);
182-
break;
183-
}
184-
}
185-
});
186-
187196
let trackInteractions = isDisabled ? {} : mergeProps({
188197
onMouseDown: (e: React.MouseEvent) => {
189198
if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) {
@@ -218,7 +227,7 @@ export function useColorWheel(props: ColorWheelAriaProps, state: ColorWheelState
218227
onTouchStart: (e: React.TouchEvent) => {
219228
onThumbDown(e.changedTouches[0].identifier);
220229
}
221-
}, movePropsThumb, keyboardProps);
230+
}, keyboardProps, movePropsThumb);
222231
let {x, y} = state.getThumbPosition(thumbRadius);
223232

224233
// Provide a default aria-label if none is given

packages/@react-aria/color/test/useColorWheel.test.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,24 +52,18 @@ function ColorWheel(props: ColorWheelProps) {
5252
describe('useColorWheel', () => {
5353
let onChangeSpy = jest.fn();
5454

55-
afterEach(() => {
56-
onChangeSpy.mockClear();
57-
});
58-
5955
beforeAll(() => {
6056
// @ts-ignore
61-
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb());
62-
jest.useFakeTimers();
57+
jest.useFakeTimers('modern');
6358
});
6459
afterAll(() => {
6560
jest.useRealTimers();
66-
// @ts-ignore
67-
window.requestAnimationFrame.mockRestore();
6861
});
6962

7063
afterEach(() => {
7164
// for restoreTextSelection
7265
jest.runAllTimers();
66+
onChangeSpy.mockClear();
7367
});
7468

7569
it('sets input props', () => {

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

Lines changed: 39 additions & 28 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,41 +187,37 @@ 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) => {
182-
// don't want useMove to handle shift key + arrow events because it doesn't do anything
183-
if (e.shiftKey) {
184-
return;
185-
}
186197
switch (e.key) {
187198
case 'Left':
188199
case 'ArrowLeft':
189200
e.preventDefault();
190201
e.stopPropagation();
191-
triggerKeyboardMove(-1, 0);
202+
triggerKeyboardMove(e, -1, 0);
192203
break;
193204
case 'Right':
194205
case 'ArrowRight':
195206
e.preventDefault();
196207
e.stopPropagation();
197-
triggerKeyboardMove(1, 0);
208+
triggerKeyboardMove(e, 1, 0);
198209
break;
199210
case 'Up':
200211
case 'ArrowUp':
201212
e.preventDefault();
202213
e.stopPropagation();
203-
triggerKeyboardMove(0, -1);
214+
triggerKeyboardMove(e, 0, -1);
204215
break;
205216
case 'Down':
206217
case 'ArrowDown':
207218
e.preventDefault();
208219
e.stopPropagation();
209-
triggerKeyboardMove(0, 1);
220+
triggerKeyboardMove(e, 0, 1);
210221
break;
211222
}
212223
};

0 commit comments

Comments
 (0)