@react-aria/interactions 3.4.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +440 -152
- package/dist/main.js.map +1 -1
- package/dist/module.js +429 -149
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +53 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/PressResponder.tsx +4 -2
- package/src/index.ts +1 -0
- package/src/textSelection.ts +59 -28
- package/src/useFocusVisible.ts +33 -16
- package/src/useHover.ts +28 -9
- package/src/useInteractOutside.ts +46 -44
- package/src/useLongPress.ts +128 -0
- package/src/usePress.ts +181 -38
- package/src/useScrollWheel.ts +3 -12
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {HTMLAttributes, useRef} from 'react';
|
|
14
|
+
import {LongPressEvent} from '@react-types/shared';
|
|
15
|
+
import {mergeProps, useDescription, useGlobalListeners} from '@react-aria/utils';
|
|
16
|
+
import {usePress} from './usePress';
|
|
17
|
+
|
|
18
|
+
interface LongPressProps {
|
|
19
|
+
/** Whether long press events should be disabled. */
|
|
20
|
+
isDisabled?: boolean,
|
|
21
|
+
/** Handler that is called when a long press interaction starts. */
|
|
22
|
+
onLongPressStart?: (e: LongPressEvent) => void,
|
|
23
|
+
/**
|
|
24
|
+
* Handler that is called when a long press interaction ends, either
|
|
25
|
+
* over the target or when the pointer leaves the target.
|
|
26
|
+
*/
|
|
27
|
+
onLongPressEnd?: (e: LongPressEvent) => void,
|
|
28
|
+
/**
|
|
29
|
+
* Handler that is called when the threshold time is met while
|
|
30
|
+
* the press is over the target.
|
|
31
|
+
*/
|
|
32
|
+
onLongPress?: (e: LongPressEvent) => void,
|
|
33
|
+
/**
|
|
34
|
+
* The amount of time in milliseconds to wait before triggering a long press.
|
|
35
|
+
* @default 500ms
|
|
36
|
+
*/
|
|
37
|
+
threshold?: number,
|
|
38
|
+
/**
|
|
39
|
+
* A description for assistive techology users indicating that a long press
|
|
40
|
+
* action is available, e.g. "Long press to open menu".
|
|
41
|
+
*/
|
|
42
|
+
accessibilityDescription?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface LongPressResult {
|
|
46
|
+
/** Props to spread on the target element. */
|
|
47
|
+
longPressProps: HTMLAttributes<HTMLElement>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DEFAULT_THRESHOLD = 500;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Handles long press interactions across mouse and touch devices. Supports a customizable time threshold,
|
|
54
|
+
* accessibility description, and normalizes behavior across browsers and devices.
|
|
55
|
+
*/
|
|
56
|
+
export function useLongPress(props: LongPressProps): LongPressResult {
|
|
57
|
+
let {
|
|
58
|
+
isDisabled,
|
|
59
|
+
onLongPressStart,
|
|
60
|
+
onLongPressEnd,
|
|
61
|
+
onLongPress,
|
|
62
|
+
threshold = DEFAULT_THRESHOLD,
|
|
63
|
+
accessibilityDescription
|
|
64
|
+
} = props;
|
|
65
|
+
|
|
66
|
+
const timeRef = useRef(null);
|
|
67
|
+
let {addGlobalListener, removeGlobalListener} = useGlobalListeners();
|
|
68
|
+
|
|
69
|
+
let {pressProps} = usePress({
|
|
70
|
+
isDisabled,
|
|
71
|
+
onPressStart(e) {
|
|
72
|
+
if (e.pointerType === 'mouse' || e.pointerType === 'touch') {
|
|
73
|
+
if (onLongPressStart) {
|
|
74
|
+
onLongPressStart({
|
|
75
|
+
...e,
|
|
76
|
+
type: 'longpressstart'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
timeRef.current = setTimeout(() => {
|
|
81
|
+
// Prevent other usePress handlers from also handling this event.
|
|
82
|
+
e.target.dispatchEvent(new PointerEvent('pointercancel', {bubbles: true}));
|
|
83
|
+
if (onLongPress) {
|
|
84
|
+
onLongPress({
|
|
85
|
+
...e,
|
|
86
|
+
type: 'longpress'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
timeRef.current = null;
|
|
90
|
+
}, threshold);
|
|
91
|
+
|
|
92
|
+
// Prevent context menu, which may be opened on long press on touch devices
|
|
93
|
+
if (e.pointerType === 'touch') {
|
|
94
|
+
let onContextMenu = e => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
addGlobalListener(e.target, 'contextmenu', onContextMenu, {once: true});
|
|
99
|
+
addGlobalListener(window, 'pointerup', () => {
|
|
100
|
+
// If no contextmenu event is fired quickly after pointerup, remove the handler
|
|
101
|
+
// so future context menu events outside a long press are not prevented.
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
removeGlobalListener(e.target, 'contextmenu', onContextMenu);
|
|
104
|
+
}, 30);
|
|
105
|
+
}, {once: true});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
onPressEnd(e) {
|
|
110
|
+
if (timeRef.current) {
|
|
111
|
+
clearTimeout(timeRef.current);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (onLongPressEnd && (e.pointerType === 'mouse' || e.pointerType === 'touch')) {
|
|
115
|
+
onLongPressEnd({
|
|
116
|
+
...e,
|
|
117
|
+
type: 'longpressend'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
let descriptionProps = useDescription(onLongPress && !isDisabled ? accessibilityDescription : null);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
longPressProps: mergeProps(pressProps, descriptionProps)
|
|
127
|
+
};
|
|
128
|
+
}
|
package/src/usePress.ts
CHANGED
|
@@ -28,7 +28,16 @@ export interface PressProps extends PressEvents {
|
|
|
28
28
|
/** Whether the press events should be disabled. */
|
|
29
29
|
isDisabled?: boolean,
|
|
30
30
|
/** Whether the target should not receive focus on press. */
|
|
31
|
-
preventFocusOnPress?: boolean
|
|
31
|
+
preventFocusOnPress?: boolean,
|
|
32
|
+
/**
|
|
33
|
+
* Whether press events should be canceled when the pointer leaves the target while pressed.
|
|
34
|
+
* By default, this is `false`, which means if the pointer returns back over the target while
|
|
35
|
+
* still pressed, onPressStart will be fired again. If set to `true`, the press is canceled
|
|
36
|
+
* when the pointer leaves the target and onPressStart will not be fired if the pointer returns.
|
|
37
|
+
*/
|
|
38
|
+
shouldCancelOnPointerExit?: boolean,
|
|
39
|
+
/** Whether text selection should be enabled on the pressable element. */
|
|
40
|
+
allowTextSelectionOnPress?: boolean
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
export interface PressHookProps extends PressProps {
|
|
@@ -52,7 +61,8 @@ interface EventBase {
|
|
|
52
61
|
currentTarget: EventTarget,
|
|
53
62
|
shiftKey: boolean,
|
|
54
63
|
ctrlKey: boolean,
|
|
55
|
-
metaKey: boolean
|
|
64
|
+
metaKey: boolean,
|
|
65
|
+
altKey: boolean
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
export interface PressResult {
|
|
@@ -90,12 +100,14 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
90
100
|
isDisabled,
|
|
91
101
|
isPressed: isPressedProp,
|
|
92
102
|
preventFocusOnPress,
|
|
103
|
+
shouldCancelOnPointerExit,
|
|
104
|
+
allowTextSelectionOnPress,
|
|
93
105
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
94
106
|
ref: _, // Removing `ref` from `domProps` because TypeScript is dumb,
|
|
95
107
|
...domProps
|
|
96
108
|
} = usePressResponderContext(props);
|
|
97
109
|
let propsRef = useRef<PressHookProps>(null);
|
|
98
|
-
propsRef.current = {onPress, onPressChange, onPressStart, onPressEnd, onPressUp, isDisabled};
|
|
110
|
+
propsRef.current = {onPress, onPressChange, onPressStart, onPressEnd, onPressUp, isDisabled, shouldCancelOnPointerExit};
|
|
99
111
|
|
|
100
112
|
let [isPressed, setPressed] = useState(false);
|
|
101
113
|
let ref = useRef<PressState>({
|
|
@@ -126,7 +138,8 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
126
138
|
target: originalEvent.currentTarget as HTMLElement,
|
|
127
139
|
shiftKey: originalEvent.shiftKey,
|
|
128
140
|
metaKey: originalEvent.metaKey,
|
|
129
|
-
ctrlKey: originalEvent.ctrlKey
|
|
141
|
+
ctrlKey: originalEvent.ctrlKey,
|
|
142
|
+
altKey: originalEvent.altKey
|
|
130
143
|
});
|
|
131
144
|
}
|
|
132
145
|
|
|
@@ -154,7 +167,8 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
154
167
|
target: originalEvent.currentTarget as HTMLElement,
|
|
155
168
|
shiftKey: originalEvent.shiftKey,
|
|
156
169
|
metaKey: originalEvent.metaKey,
|
|
157
|
-
ctrlKey: originalEvent.ctrlKey
|
|
170
|
+
ctrlKey: originalEvent.ctrlKey,
|
|
171
|
+
altKey: originalEvent.altKey
|
|
158
172
|
});
|
|
159
173
|
}
|
|
160
174
|
|
|
@@ -171,7 +185,8 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
171
185
|
target: originalEvent.currentTarget as HTMLElement,
|
|
172
186
|
shiftKey: originalEvent.shiftKey,
|
|
173
187
|
metaKey: originalEvent.metaKey,
|
|
174
|
-
ctrlKey: originalEvent.ctrlKey
|
|
188
|
+
ctrlKey: originalEvent.ctrlKey,
|
|
189
|
+
altKey: originalEvent.altKey
|
|
175
190
|
});
|
|
176
191
|
}
|
|
177
192
|
};
|
|
@@ -189,7 +204,8 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
189
204
|
target: originalEvent.currentTarget as HTMLElement,
|
|
190
205
|
shiftKey: originalEvent.shiftKey,
|
|
191
206
|
metaKey: originalEvent.metaKey,
|
|
192
|
-
ctrlKey: originalEvent.ctrlKey
|
|
207
|
+
ctrlKey: originalEvent.ctrlKey,
|
|
208
|
+
altKey: originalEvent.altKey
|
|
193
209
|
});
|
|
194
210
|
}
|
|
195
211
|
};
|
|
@@ -204,17 +220,18 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
204
220
|
state.activePointerId = null;
|
|
205
221
|
state.pointerType = null;
|
|
206
222
|
removeAllGlobalListeners();
|
|
207
|
-
|
|
223
|
+
if (!allowTextSelectionOnPress) {
|
|
224
|
+
restoreTextSelection(state.target);
|
|
225
|
+
}
|
|
208
226
|
}
|
|
209
227
|
};
|
|
210
228
|
|
|
211
229
|
let pressProps: HTMLAttributes<HTMLElement> = {
|
|
212
230
|
onKeyDown(e) {
|
|
213
|
-
if (isValidKeyboardEvent(e.nativeEvent)) {
|
|
231
|
+
if (isValidKeyboardEvent(e.nativeEvent) && e.currentTarget.contains(e.target as HTMLElement)) {
|
|
214
232
|
e.preventDefault();
|
|
215
233
|
e.stopPropagation();
|
|
216
234
|
|
|
217
|
-
|
|
218
235
|
// If the event is repeating, it may have started on a different element
|
|
219
236
|
// after which focus moved to the current element. Ignore these events and
|
|
220
237
|
// only handle the first key down event.
|
|
@@ -230,11 +247,15 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
230
247
|
}
|
|
231
248
|
},
|
|
232
249
|
onKeyUp(e) {
|
|
233
|
-
if (isValidKeyboardEvent(e.nativeEvent) && !e.repeat) {
|
|
250
|
+
if (isValidKeyboardEvent(e.nativeEvent) && !e.repeat && e.currentTarget.contains(e.target as HTMLElement)) {
|
|
234
251
|
triggerPressUp(createEvent(state.target, e), 'keyboard');
|
|
235
252
|
}
|
|
236
253
|
},
|
|
237
254
|
onClick(e) {
|
|
255
|
+
if (e && !e.currentTarget.contains(e.target as HTMLElement)) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
238
259
|
if (e && e.button === 0) {
|
|
239
260
|
e.stopPropagation();
|
|
240
261
|
if (isDisabled) {
|
|
@@ -243,7 +264,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
243
264
|
|
|
244
265
|
// If triggered from a screen reader or by using element.click(),
|
|
245
266
|
// trigger as if it were a keyboard click.
|
|
246
|
-
if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && isVirtualClick(e.nativeEvent)) {
|
|
267
|
+
if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && (state.pointerType === 'virtual' || isVirtualClick(e.nativeEvent))) {
|
|
247
268
|
// Ensure the element receives focus (VoiceOver on iOS does not do this)
|
|
248
269
|
if (!isDisabled && !preventFocusOnPress) {
|
|
249
270
|
focusWithoutScrolling(e.currentTarget);
|
|
@@ -266,12 +287,13 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
266
287
|
e.stopPropagation();
|
|
267
288
|
|
|
268
289
|
state.isPressed = false;
|
|
269
|
-
|
|
290
|
+
let target = e.target as HTMLElement;
|
|
291
|
+
triggerPressEnd(createEvent(state.target, e), 'keyboard', state.target.contains(target));
|
|
270
292
|
removeAllGlobalListeners();
|
|
271
293
|
|
|
272
294
|
// If the target is a link, trigger the click method to open the URL,
|
|
273
295
|
// but defer triggering pressEnd until onClick event handler.
|
|
274
|
-
if (
|
|
296
|
+
if (state.target.contains(target) && isHTMLAnchorLink(state.target) || state.target.getAttribute('role') === 'link') {
|
|
275
297
|
state.target.click();
|
|
276
298
|
}
|
|
277
299
|
}
|
|
@@ -279,8 +301,17 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
279
301
|
|
|
280
302
|
if (typeof PointerEvent !== 'undefined') {
|
|
281
303
|
pressProps.onPointerDown = (e) => {
|
|
282
|
-
// Only handle left clicks
|
|
283
|
-
if (e.button !== 0) {
|
|
304
|
+
// Only handle left clicks, and ignore events that bubbled through portals.
|
|
305
|
+
if (e.button !== 0 || !e.currentTarget.contains(e.target as HTMLElement)) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// iOS safari fires pointer events from VoiceOver with incorrect coordinates/target.
|
|
310
|
+
// Ignore and let the onClick handler take care of it instead.
|
|
311
|
+
// https://bugs.webkit.org/show_bug.cgi?id=222627
|
|
312
|
+
// https://bugs.webkit.org/show_bug.cgi?id=223202
|
|
313
|
+
if (isVirtualPointerEvent(e.nativeEvent)) {
|
|
314
|
+
state.pointerType = 'virtual';
|
|
284
315
|
return;
|
|
285
316
|
}
|
|
286
317
|
|
|
@@ -290,9 +321,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
290
321
|
e.preventDefault();
|
|
291
322
|
}
|
|
292
323
|
|
|
293
|
-
|
|
294
|
-
// https://bugs.webkit.org/show_bug.cgi?id=222627
|
|
295
|
-
state.pointerType = isVirtualPointerEvent(e.nativeEvent) ? 'virtual' : e.pointerType;
|
|
324
|
+
state.pointerType = e.pointerType;
|
|
296
325
|
|
|
297
326
|
e.stopPropagation();
|
|
298
327
|
if (!state.isPressed) {
|
|
@@ -305,7 +334,10 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
305
334
|
focusWithoutScrolling(e.currentTarget);
|
|
306
335
|
}
|
|
307
336
|
|
|
308
|
-
|
|
337
|
+
if (!allowTextSelectionOnPress) {
|
|
338
|
+
disableTextSelection(state.target);
|
|
339
|
+
}
|
|
340
|
+
|
|
309
341
|
triggerPressStart(e, state.pointerType);
|
|
310
342
|
|
|
311
343
|
addGlobalListener(document, 'pointermove', onPointerMove, false);
|
|
@@ -315,6 +347,10 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
315
347
|
};
|
|
316
348
|
|
|
317
349
|
pressProps.onMouseDown = (e) => {
|
|
350
|
+
if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
318
354
|
if (e.button === 0) {
|
|
319
355
|
// Chrome and Firefox on touch Windows devices require mouse down events
|
|
320
356
|
// to be canceled in addition to pointer events, or an extra asynchronous
|
|
@@ -328,11 +364,16 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
328
364
|
};
|
|
329
365
|
|
|
330
366
|
pressProps.onPointerUp = (e) => {
|
|
367
|
+
// iOS fires pointerup with zero width and height, so check the pointerType recorded during pointerdown.
|
|
368
|
+
if (!e.currentTarget.contains(e.target as HTMLElement) || state.pointerType === 'virtual') {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
331
372
|
// Only handle left clicks
|
|
332
373
|
// Safari on iOS sometimes fires pointerup events, even
|
|
333
374
|
// when the touch isn't over the target, so double check.
|
|
334
375
|
if (e.button === 0 && isOverTarget(e, e.currentTarget)) {
|
|
335
|
-
triggerPressUp(e, state.pointerType);
|
|
376
|
+
triggerPressUp(e, state.pointerType || e.pointerType);
|
|
336
377
|
}
|
|
337
378
|
};
|
|
338
379
|
|
|
@@ -352,6 +393,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
352
393
|
} else if (state.isOverTarget) {
|
|
353
394
|
state.isOverTarget = false;
|
|
354
395
|
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
|
396
|
+
if (propsRef.current.shouldCancelOnPointerExit) {
|
|
397
|
+
cancel(e);
|
|
398
|
+
}
|
|
355
399
|
}
|
|
356
400
|
};
|
|
357
401
|
|
|
@@ -368,7 +412,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
368
412
|
state.activePointerId = null;
|
|
369
413
|
state.pointerType = null;
|
|
370
414
|
removeAllGlobalListeners();
|
|
371
|
-
|
|
415
|
+
if (!allowTextSelectionOnPress) {
|
|
416
|
+
restoreTextSelection(state.target);
|
|
417
|
+
}
|
|
372
418
|
}
|
|
373
419
|
};
|
|
374
420
|
|
|
@@ -377,13 +423,17 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
377
423
|
};
|
|
378
424
|
|
|
379
425
|
pressProps.onDragStart = (e) => {
|
|
426
|
+
if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
380
430
|
// Safari does not call onPointerCancel when a drag starts, whereas Chrome and Firefox do.
|
|
381
431
|
cancel(e);
|
|
382
432
|
};
|
|
383
433
|
} else {
|
|
384
434
|
pressProps.onMouseDown = (e) => {
|
|
385
435
|
// Only handle left clicks
|
|
386
|
-
if (e.button !== 0) {
|
|
436
|
+
if (e.button !== 0 || !e.currentTarget.contains(e.target as HTMLElement)) {
|
|
387
437
|
return;
|
|
388
438
|
}
|
|
389
439
|
|
|
@@ -413,6 +463,10 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
413
463
|
};
|
|
414
464
|
|
|
415
465
|
pressProps.onMouseEnter = (e) => {
|
|
466
|
+
if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
416
470
|
e.stopPropagation();
|
|
417
471
|
if (state.isPressed && !state.ignoreEmulatedMouseEvents) {
|
|
418
472
|
state.isOverTarget = true;
|
|
@@ -421,14 +475,25 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
421
475
|
};
|
|
422
476
|
|
|
423
477
|
pressProps.onMouseLeave = (e) => {
|
|
478
|
+
if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
424
482
|
e.stopPropagation();
|
|
425
483
|
if (state.isPressed && !state.ignoreEmulatedMouseEvents) {
|
|
426
484
|
state.isOverTarget = false;
|
|
427
485
|
triggerPressEnd(e, state.pointerType, false);
|
|
486
|
+
if (propsRef.current.shouldCancelOnPointerExit) {
|
|
487
|
+
cancel(e);
|
|
488
|
+
}
|
|
428
489
|
}
|
|
429
490
|
};
|
|
430
491
|
|
|
431
492
|
pressProps.onMouseUp = (e) => {
|
|
493
|
+
if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
432
497
|
if (!state.ignoreEmulatedMouseEvents && e.button === 0) {
|
|
433
498
|
triggerPressUp(e, state.pointerType);
|
|
434
499
|
}
|
|
@@ -458,6 +523,10 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
458
523
|
};
|
|
459
524
|
|
|
460
525
|
pressProps.onTouchStart = (e) => {
|
|
526
|
+
if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
461
530
|
e.stopPropagation();
|
|
462
531
|
let touch = getTouchFromEvent(e.nativeEvent);
|
|
463
532
|
if (!touch) {
|
|
@@ -476,13 +545,20 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
476
545
|
focusWithoutScrolling(e.currentTarget);
|
|
477
546
|
}
|
|
478
547
|
|
|
479
|
-
|
|
548
|
+
if (!allowTextSelectionOnPress) {
|
|
549
|
+
disableTextSelection(state.target);
|
|
550
|
+
}
|
|
551
|
+
|
|
480
552
|
triggerPressStart(e, state.pointerType);
|
|
481
553
|
|
|
482
554
|
addGlobalListener(window, 'scroll', onScroll, true);
|
|
483
555
|
};
|
|
484
556
|
|
|
485
557
|
pressProps.onTouchMove = (e) => {
|
|
558
|
+
if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
486
562
|
e.stopPropagation();
|
|
487
563
|
if (!state.isPressed) {
|
|
488
564
|
return;
|
|
@@ -497,10 +573,17 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
497
573
|
} else if (state.isOverTarget) {
|
|
498
574
|
state.isOverTarget = false;
|
|
499
575
|
triggerPressEnd(e, state.pointerType, false);
|
|
576
|
+
if (propsRef.current.shouldCancelOnPointerExit) {
|
|
577
|
+
cancel(e);
|
|
578
|
+
}
|
|
500
579
|
}
|
|
501
580
|
};
|
|
502
581
|
|
|
503
582
|
pressProps.onTouchEnd = (e) => {
|
|
583
|
+
if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
504
587
|
e.stopPropagation();
|
|
505
588
|
if (!state.isPressed) {
|
|
506
589
|
return;
|
|
@@ -518,11 +601,17 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
518
601
|
state.activePointerId = null;
|
|
519
602
|
state.isOverTarget = false;
|
|
520
603
|
state.ignoreEmulatedMouseEvents = true;
|
|
521
|
-
|
|
604
|
+
if (!allowTextSelectionOnPress) {
|
|
605
|
+
restoreTextSelection(state.target);
|
|
606
|
+
}
|
|
522
607
|
removeAllGlobalListeners();
|
|
523
608
|
};
|
|
524
609
|
|
|
525
610
|
pressProps.onTouchCancel = (e) => {
|
|
611
|
+
if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
526
615
|
e.stopPropagation();
|
|
527
616
|
if (state.isPressed) {
|
|
528
617
|
cancel(e);
|
|
@@ -535,24 +624,33 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
535
624
|
currentTarget: state.target,
|
|
536
625
|
shiftKey: false,
|
|
537
626
|
ctrlKey: false,
|
|
538
|
-
metaKey: false
|
|
627
|
+
metaKey: false,
|
|
628
|
+
altKey: false
|
|
539
629
|
});
|
|
540
630
|
}
|
|
541
631
|
};
|
|
542
632
|
|
|
543
633
|
pressProps.onDragStart = (e) => {
|
|
634
|
+
if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
544
638
|
cancel(e);
|
|
545
639
|
};
|
|
546
640
|
}
|
|
547
641
|
|
|
548
642
|
return pressProps;
|
|
549
|
-
}, [addGlobalListener, isDisabled, preventFocusOnPress, removeAllGlobalListeners]);
|
|
643
|
+
}, [addGlobalListener, isDisabled, preventFocusOnPress, removeAllGlobalListeners, allowTextSelectionOnPress]);
|
|
550
644
|
|
|
551
645
|
// Remove user-select: none in case component unmounts immediately after pressStart
|
|
552
646
|
// eslint-disable-next-line arrow-body-style
|
|
553
647
|
useEffect(() => {
|
|
554
|
-
return () =>
|
|
555
|
-
|
|
648
|
+
return () => {
|
|
649
|
+
if (!allowTextSelectionOnPress) {
|
|
650
|
+
restoreTextSelection(ref.current.target);
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
}, [allowTextSelectionOnPress]);
|
|
556
654
|
|
|
557
655
|
return {
|
|
558
656
|
isPressed: isPressedProp || isPressed,
|
|
@@ -565,14 +663,14 @@ function isHTMLAnchorLink(target: HTMLElement): boolean {
|
|
|
565
663
|
}
|
|
566
664
|
|
|
567
665
|
function isValidKeyboardEvent(event: KeyboardEvent): boolean {
|
|
568
|
-
const {key, target} = event;
|
|
666
|
+
const {key, code, target} = event;
|
|
569
667
|
const element = target as HTMLElement;
|
|
570
668
|
const {tagName, isContentEditable} = element;
|
|
571
669
|
const role = element.getAttribute('role');
|
|
572
670
|
// Accessibility for keyboards. Space and Enter only.
|
|
573
671
|
// "Spacebar" is for IE 11
|
|
574
672
|
return (
|
|
575
|
-
(key === 'Enter' || key === ' ' || key === 'Spacebar') &&
|
|
673
|
+
(key === 'Enter' || key === ' ' || key === 'Spacebar' || code === 'Space') &&
|
|
576
674
|
(tagName !== 'INPUT' &&
|
|
577
675
|
tagName !== 'TEXTAREA' &&
|
|
578
676
|
isContentEditable !== true) &&
|
|
@@ -611,21 +709,55 @@ function createEvent(target: HTMLElement, e: EventBase): EventBase {
|
|
|
611
709
|
currentTarget: target,
|
|
612
710
|
shiftKey: e.shiftKey,
|
|
613
711
|
ctrlKey: e.ctrlKey,
|
|
614
|
-
metaKey: e.metaKey
|
|
712
|
+
metaKey: e.metaKey,
|
|
713
|
+
altKey: e.altKey
|
|
615
714
|
};
|
|
616
715
|
}
|
|
617
716
|
|
|
717
|
+
interface Rect {
|
|
718
|
+
top: number,
|
|
719
|
+
right: number,
|
|
720
|
+
bottom: number,
|
|
721
|
+
left: number
|
|
722
|
+
}
|
|
723
|
+
|
|
618
724
|
interface EventPoint {
|
|
619
725
|
clientX: number,
|
|
620
|
-
clientY: number
|
|
726
|
+
clientY: number,
|
|
727
|
+
width?: number,
|
|
728
|
+
height?: number,
|
|
729
|
+
radiusX?: number,
|
|
730
|
+
radiusY?: number
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function getPointClientRect(point: EventPoint): Rect {
|
|
734
|
+
let offsetX = (point.width / 2) || point.radiusX || 0;
|
|
735
|
+
let offsetY = (point.height / 2) || point.radiusY || 0;
|
|
736
|
+
|
|
737
|
+
return {
|
|
738
|
+
top: point.clientY - offsetY,
|
|
739
|
+
right: point.clientX + offsetX,
|
|
740
|
+
bottom: point.clientY + offsetY,
|
|
741
|
+
left: point.clientX - offsetX
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function areRectanglesOverlapping(a: Rect, b: Rect) {
|
|
746
|
+
// check if they cannot overlap on x axis
|
|
747
|
+
if (a.left > b.right || b.left > a.right) {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
// check if they cannot overlap on y axis
|
|
751
|
+
if (a.top > b.bottom || b.top > a.bottom) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return true;
|
|
621
755
|
}
|
|
622
756
|
|
|
623
757
|
function isOverTarget(point: EventPoint, target: HTMLElement) {
|
|
624
758
|
let rect = target.getBoundingClientRect();
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
(point.clientY || 0) >= (rect.top || 0) &&
|
|
628
|
-
(point.clientY || 0) <= (rect.bottom || 0);
|
|
759
|
+
let pointRect = getPointClientRect(point);
|
|
760
|
+
return areRectanglesOverlapping(rect, pointRect);
|
|
629
761
|
}
|
|
630
762
|
|
|
631
763
|
function shouldPreventDefault(target: Element) {
|
|
@@ -635,5 +767,16 @@ function shouldPreventDefault(target: Element) {
|
|
|
635
767
|
|
|
636
768
|
function isVirtualPointerEvent(event: PointerEvent) {
|
|
637
769
|
// If the pointer size is zero, then we assume it's from a screen reader.
|
|
638
|
-
return event
|
|
770
|
+
// Android TalkBack double tap will sometimes return a event with width and height of 1
|
|
771
|
+
// and pointerType === 'mouse' so we need to check for a specific combination of event attributes.
|
|
772
|
+
// Cannot use "event.pressure === 0" as the sole check due to Safari pointer events always returning pressure === 0
|
|
773
|
+
// instead of .5, see https://bugs.webkit.org/show_bug.cgi?id=206216
|
|
774
|
+
return (
|
|
775
|
+
(event.width === 0 && event.height === 0) ||
|
|
776
|
+
(event.width === 1 &&
|
|
777
|
+
event.height === 1 &&
|
|
778
|
+
event.pressure === 0 &&
|
|
779
|
+
event.detail === 0
|
|
780
|
+
)
|
|
781
|
+
);
|
|
639
782
|
}
|
package/src/useScrollWheel.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {RefObject, useCallback
|
|
13
|
+
import {RefObject, useCallback} from 'react';
|
|
14
14
|
import {ScrollEvents} from '@react-types/shared';
|
|
15
|
+
import {useEvent} from '@react-aria/utils';
|
|
15
16
|
|
|
16
17
|
export interface ScrollWheelProps extends ScrollEvents {
|
|
17
18
|
/** Whether the scroll listener should be disabled. */
|
|
@@ -36,15 +37,5 @@ export function useScrollWheel(props: ScrollWheelProps, ref: RefObject<HTMLEleme
|
|
|
36
37
|
}
|
|
37
38
|
}, [onScroll]);
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
let elem = ref.current;
|
|
41
|
-
if (isDisabled) {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
elem.addEventListener('wheel', onScrollHandler);
|
|
45
|
-
|
|
46
|
-
return () => {
|
|
47
|
-
elem.removeEventListener('wheel', onScrollHandler);
|
|
48
|
-
};
|
|
49
|
-
}, [onScrollHandler, ref, isDisabled]);
|
|
40
|
+
useEvent(ref, 'wheel', isDisabled ? null : onScrollHandler);
|
|
50
41
|
}
|