@react-aria/interactions 3.5.1 → 3.8.1
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 +1252 -1661
- package/dist/main.js.map +1 -1
- package/dist/module.js +1239 -1612
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +45 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/textSelection.ts +59 -28
- package/src/useFocusVisible.ts +4 -2
- package/src/useHover.ts +3 -3
- package/src/useLongPress.ts +128 -0
- package/src/useMove.ts +39 -24
- package/src/usePress.ts +102 -31
- package/src/useScrollWheel.ts +3 -12
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
|
-
ref: _, // Removing `ref` from `domProps` because TypeScript is dumb
|
|
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,14 +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
231
|
if (isValidKeyboardEvent(e.nativeEvent) && e.currentTarget.contains(e.target as HTMLElement)) {
|
|
214
|
-
e.
|
|
232
|
+
if (shouldPreventDefaultKeyboard(e.target as Element)) {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
}
|
|
215
235
|
e.stopPropagation();
|
|
216
236
|
|
|
217
237
|
// If the event is repeating, it may have started on a different element
|
|
@@ -246,7 +266,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
246
266
|
|
|
247
267
|
// If triggered from a screen reader or by using element.click(),
|
|
248
268
|
// trigger as if it were a keyboard click.
|
|
249
|
-
if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && isVirtualClick(e.nativeEvent)) {
|
|
269
|
+
if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && (state.pointerType === 'virtual' || isVirtualClick(e.nativeEvent))) {
|
|
250
270
|
// Ensure the element receives focus (VoiceOver on iOS does not do this)
|
|
251
271
|
if (!isDisabled && !preventFocusOnPress) {
|
|
252
272
|
focusWithoutScrolling(e.currentTarget);
|
|
@@ -265,16 +285,19 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
265
285
|
|
|
266
286
|
let onKeyUp = (e: KeyboardEvent) => {
|
|
267
287
|
if (state.isPressed && isValidKeyboardEvent(e)) {
|
|
268
|
-
e.
|
|
288
|
+
if (shouldPreventDefaultKeyboard(e.target as Element)) {
|
|
289
|
+
e.preventDefault();
|
|
290
|
+
}
|
|
269
291
|
e.stopPropagation();
|
|
270
292
|
|
|
271
293
|
state.isPressed = false;
|
|
272
|
-
|
|
294
|
+
let target = e.target as HTMLElement;
|
|
295
|
+
triggerPressEnd(createEvent(state.target, e), 'keyboard', state.target.contains(target));
|
|
273
296
|
removeAllGlobalListeners();
|
|
274
297
|
|
|
275
298
|
// If the target is a link, trigger the click method to open the URL,
|
|
276
299
|
// but defer triggering pressEnd until onClick event handler.
|
|
277
|
-
if (
|
|
300
|
+
if (state.target.contains(target) && isHTMLAnchorLink(state.target) || state.target.getAttribute('role') === 'link') {
|
|
278
301
|
state.target.click();
|
|
279
302
|
}
|
|
280
303
|
}
|
|
@@ -287,15 +310,22 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
287
310
|
return;
|
|
288
311
|
}
|
|
289
312
|
|
|
313
|
+
// iOS safari fires pointer events from VoiceOver with incorrect coordinates/target.
|
|
314
|
+
// Ignore and let the onClick handler take care of it instead.
|
|
315
|
+
// https://bugs.webkit.org/show_bug.cgi?id=222627
|
|
316
|
+
// https://bugs.webkit.org/show_bug.cgi?id=223202
|
|
317
|
+
if (isVirtualPointerEvent(e.nativeEvent)) {
|
|
318
|
+
state.pointerType = 'virtual';
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
290
322
|
// Due to browser inconsistencies, especially on mobile browsers, we prevent
|
|
291
323
|
// default on pointer down and handle focusing the pressable element ourselves.
|
|
292
324
|
if (shouldPreventDefault(e.target as Element)) {
|
|
293
325
|
e.preventDefault();
|
|
294
326
|
}
|
|
295
327
|
|
|
296
|
-
|
|
297
|
-
// https://bugs.webkit.org/show_bug.cgi?id=222627
|
|
298
|
-
state.pointerType = isVirtualPointerEvent(e.nativeEvent) ? 'virtual' : e.pointerType;
|
|
328
|
+
state.pointerType = e.pointerType;
|
|
299
329
|
|
|
300
330
|
e.stopPropagation();
|
|
301
331
|
if (!state.isPressed) {
|
|
@@ -308,7 +338,10 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
308
338
|
focusWithoutScrolling(e.currentTarget);
|
|
309
339
|
}
|
|
310
340
|
|
|
311
|
-
|
|
341
|
+
if (!allowTextSelectionOnPress) {
|
|
342
|
+
disableTextSelection(state.target);
|
|
343
|
+
}
|
|
344
|
+
|
|
312
345
|
triggerPressStart(e, state.pointerType);
|
|
313
346
|
|
|
314
347
|
addGlobalListener(document, 'pointermove', onPointerMove, false);
|
|
@@ -335,7 +368,8 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
335
368
|
};
|
|
336
369
|
|
|
337
370
|
pressProps.onPointerUp = (e) => {
|
|
338
|
-
|
|
371
|
+
// iOS fires pointerup with zero width and height, so check the pointerType recorded during pointerdown.
|
|
372
|
+
if (!e.currentTarget.contains(e.target as HTMLElement) || state.pointerType === 'virtual') {
|
|
339
373
|
return;
|
|
340
374
|
}
|
|
341
375
|
|
|
@@ -343,7 +377,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
343
377
|
// Safari on iOS sometimes fires pointerup events, even
|
|
344
378
|
// when the touch isn't over the target, so double check.
|
|
345
379
|
if (e.button === 0 && isOverTarget(e, e.currentTarget)) {
|
|
346
|
-
triggerPressUp(e, state.pointerType);
|
|
380
|
+
triggerPressUp(e, state.pointerType || e.pointerType);
|
|
347
381
|
}
|
|
348
382
|
};
|
|
349
383
|
|
|
@@ -363,6 +397,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
363
397
|
} else if (state.isOverTarget) {
|
|
364
398
|
state.isOverTarget = false;
|
|
365
399
|
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
|
400
|
+
if (propsRef.current.shouldCancelOnPointerExit) {
|
|
401
|
+
cancel(e);
|
|
402
|
+
}
|
|
366
403
|
}
|
|
367
404
|
};
|
|
368
405
|
|
|
@@ -379,7 +416,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
379
416
|
state.activePointerId = null;
|
|
380
417
|
state.pointerType = null;
|
|
381
418
|
removeAllGlobalListeners();
|
|
382
|
-
|
|
419
|
+
if (!allowTextSelectionOnPress) {
|
|
420
|
+
restoreTextSelection(state.target);
|
|
421
|
+
}
|
|
383
422
|
}
|
|
384
423
|
};
|
|
385
424
|
|
|
@@ -448,6 +487,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
448
487
|
if (state.isPressed && !state.ignoreEmulatedMouseEvents) {
|
|
449
488
|
state.isOverTarget = false;
|
|
450
489
|
triggerPressEnd(e, state.pointerType, false);
|
|
490
|
+
if (propsRef.current.shouldCancelOnPointerExit) {
|
|
491
|
+
cancel(e);
|
|
492
|
+
}
|
|
451
493
|
}
|
|
452
494
|
};
|
|
453
495
|
|
|
@@ -507,7 +549,10 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
507
549
|
focusWithoutScrolling(e.currentTarget);
|
|
508
550
|
}
|
|
509
551
|
|
|
510
|
-
|
|
552
|
+
if (!allowTextSelectionOnPress) {
|
|
553
|
+
disableTextSelection(state.target);
|
|
554
|
+
}
|
|
555
|
+
|
|
511
556
|
triggerPressStart(e, state.pointerType);
|
|
512
557
|
|
|
513
558
|
addGlobalListener(window, 'scroll', onScroll, true);
|
|
@@ -532,6 +577,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
532
577
|
} else if (state.isOverTarget) {
|
|
533
578
|
state.isOverTarget = false;
|
|
534
579
|
triggerPressEnd(e, state.pointerType, false);
|
|
580
|
+
if (propsRef.current.shouldCancelOnPointerExit) {
|
|
581
|
+
cancel(e);
|
|
582
|
+
}
|
|
535
583
|
}
|
|
536
584
|
};
|
|
537
585
|
|
|
@@ -557,7 +605,9 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
557
605
|
state.activePointerId = null;
|
|
558
606
|
state.isOverTarget = false;
|
|
559
607
|
state.ignoreEmulatedMouseEvents = true;
|
|
560
|
-
|
|
608
|
+
if (!allowTextSelectionOnPress) {
|
|
609
|
+
restoreTextSelection(state.target);
|
|
610
|
+
}
|
|
561
611
|
removeAllGlobalListeners();
|
|
562
612
|
};
|
|
563
613
|
|
|
@@ -578,7 +628,8 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
578
628
|
currentTarget: state.target,
|
|
579
629
|
shiftKey: false,
|
|
580
630
|
ctrlKey: false,
|
|
581
|
-
metaKey: false
|
|
631
|
+
metaKey: false,
|
|
632
|
+
altKey: false
|
|
582
633
|
});
|
|
583
634
|
}
|
|
584
635
|
};
|
|
@@ -593,13 +644,17 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
593
644
|
}
|
|
594
645
|
|
|
595
646
|
return pressProps;
|
|
596
|
-
}, [addGlobalListener, isDisabled, preventFocusOnPress, removeAllGlobalListeners]);
|
|
647
|
+
}, [addGlobalListener, isDisabled, preventFocusOnPress, removeAllGlobalListeners, allowTextSelectionOnPress]);
|
|
597
648
|
|
|
598
649
|
// Remove user-select: none in case component unmounts immediately after pressStart
|
|
599
650
|
// eslint-disable-next-line arrow-body-style
|
|
600
651
|
useEffect(() => {
|
|
601
|
-
return () =>
|
|
602
|
-
|
|
652
|
+
return () => {
|
|
653
|
+
if (!allowTextSelectionOnPress) {
|
|
654
|
+
restoreTextSelection(ref.current.target);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
}, [allowTextSelectionOnPress]);
|
|
603
658
|
|
|
604
659
|
return {
|
|
605
660
|
isPressed: isPressedProp || isPressed,
|
|
@@ -612,14 +667,14 @@ function isHTMLAnchorLink(target: HTMLElement): boolean {
|
|
|
612
667
|
}
|
|
613
668
|
|
|
614
669
|
function isValidKeyboardEvent(event: KeyboardEvent): boolean {
|
|
615
|
-
const {key, target} = event;
|
|
670
|
+
const {key, code, target} = event;
|
|
616
671
|
const element = target as HTMLElement;
|
|
617
672
|
const {tagName, isContentEditable} = element;
|
|
618
673
|
const role = element.getAttribute('role');
|
|
619
674
|
// Accessibility for keyboards. Space and Enter only.
|
|
620
675
|
// "Spacebar" is for IE 11
|
|
621
676
|
return (
|
|
622
|
-
(key === 'Enter' || key === ' ' || key === 'Spacebar') &&
|
|
677
|
+
(key === 'Enter' || key === ' ' || key === 'Spacebar' || code === 'Space') &&
|
|
623
678
|
(tagName !== 'INPUT' &&
|
|
624
679
|
tagName !== 'TEXTAREA' &&
|
|
625
680
|
isContentEditable !== true) &&
|
|
@@ -658,7 +713,8 @@ function createEvent(target: HTMLElement, e: EventBase): EventBase {
|
|
|
658
713
|
currentTarget: target,
|
|
659
714
|
shiftKey: e.shiftKey,
|
|
660
715
|
ctrlKey: e.ctrlKey,
|
|
661
|
-
metaKey: e.metaKey
|
|
716
|
+
metaKey: e.metaKey,
|
|
717
|
+
altKey: e.altKey
|
|
662
718
|
};
|
|
663
719
|
}
|
|
664
720
|
|
|
@@ -713,7 +769,22 @@ function shouldPreventDefault(target: Element) {
|
|
|
713
769
|
return !target.closest('[draggable="true"]');
|
|
714
770
|
}
|
|
715
771
|
|
|
772
|
+
function shouldPreventDefaultKeyboard(target: Element) {
|
|
773
|
+
return !((target.tagName === 'INPUT' || target.tagName === 'BUTTON') && (target as HTMLButtonElement | HTMLInputElement).type === 'submit');
|
|
774
|
+
}
|
|
775
|
+
|
|
716
776
|
function isVirtualPointerEvent(event: PointerEvent) {
|
|
717
777
|
// If the pointer size is zero, then we assume it's from a screen reader.
|
|
718
|
-
return event
|
|
778
|
+
// Android TalkBack double tap will sometimes return a event with width and height of 1
|
|
779
|
+
// and pointerType === 'mouse' so we need to check for a specific combination of event attributes.
|
|
780
|
+
// Cannot use "event.pressure === 0" as the sole check due to Safari pointer events always returning pressure === 0
|
|
781
|
+
// instead of .5, see https://bugs.webkit.org/show_bug.cgi?id=206216
|
|
782
|
+
return (
|
|
783
|
+
(event.width === 0 && event.height === 0) ||
|
|
784
|
+
(event.width === 1 &&
|
|
785
|
+
event.height === 1 &&
|
|
786
|
+
event.pressure === 0 &&
|
|
787
|
+
event.detail === 0
|
|
788
|
+
)
|
|
789
|
+
);
|
|
719
790
|
}
|
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
|
}
|