@react-aria/interactions 3.22.5 → 3.24.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/Pressable.main.js +28 -4
- package/dist/Pressable.main.js.map +1 -1
- package/dist/Pressable.mjs +30 -6
- package/dist/Pressable.module.js +30 -6
- package/dist/Pressable.module.js.map +1 -1
- package/dist/createEventHandler.main.js +5 -1
- package/dist/createEventHandler.main.js.map +1 -1
- package/dist/createEventHandler.mjs +5 -1
- package/dist/createEventHandler.module.js +5 -1
- package/dist/createEventHandler.module.js.map +1 -1
- package/dist/focusSafely.main.js +40 -0
- package/dist/focusSafely.main.js.map +1 -0
- package/dist/focusSafely.mjs +35 -0
- package/dist/focusSafely.module.js +35 -0
- package/dist/focusSafely.module.js.map +1 -0
- package/dist/import.mjs +5 -1
- package/dist/main.js +9 -0
- package/dist/main.js.map +1 -1
- package/dist/module.js +5 -1
- package/dist/module.js.map +1 -1
- package/dist/textSelection.main.js +5 -3
- package/dist/textSelection.main.js.map +1 -1
- package/dist/textSelection.mjs +5 -3
- package/dist/textSelection.module.js +5 -3
- package/dist/textSelection.module.js.map +1 -1
- package/dist/types.d.ts +59 -25
- package/dist/types.d.ts.map +1 -1
- package/dist/useFocus.main.js +2 -1
- package/dist/useFocus.main.js.map +1 -1
- package/dist/useFocus.mjs +3 -2
- package/dist/useFocus.module.js +3 -2
- package/dist/useFocus.module.js.map +1 -1
- package/dist/useFocusVisible.main.js +9 -3
- package/dist/useFocusVisible.main.js.map +1 -1
- package/dist/useFocusVisible.mjs +9 -3
- package/dist/useFocusVisible.module.js +9 -3
- package/dist/useFocusVisible.module.js.map +1 -1
- package/dist/useFocusWithin.main.js +33 -4
- package/dist/useFocusWithin.main.js.map +1 -1
- package/dist/useFocusWithin.mjs +34 -5
- package/dist/useFocusWithin.module.js +34 -5
- package/dist/useFocusWithin.module.js.map +1 -1
- package/dist/useFocusable.main.js +112 -0
- package/dist/useFocusable.main.js.map +1 -0
- package/dist/useFocusable.mjs +100 -0
- package/dist/useFocusable.module.js +100 -0
- package/dist/useFocusable.module.js.map +1 -0
- package/dist/useHover.main.js +18 -3
- package/dist/useHover.main.js.map +1 -1
- package/dist/useHover.mjs +18 -3
- package/dist/useHover.module.js +18 -3
- package/dist/useHover.module.js.map +1 -1
- package/dist/useInteractOutside.main.js +6 -1
- package/dist/useInteractOutside.main.js.map +1 -1
- package/dist/useInteractOutside.mjs +6 -1
- package/dist/useInteractOutside.module.js +6 -1
- package/dist/useInteractOutside.module.js.map +1 -1
- package/dist/useLongPress.main.js +2 -0
- package/dist/useLongPress.main.js.map +1 -1
- package/dist/useLongPress.mjs +3 -1
- package/dist/useLongPress.module.js +3 -1
- package/dist/useLongPress.module.js.map +1 -1
- package/dist/usePress.main.js +97 -94
- package/dist/usePress.main.js.map +1 -1
- package/dist/usePress.mjs +98 -95
- package/dist/usePress.module.js +98 -95
- package/dist/usePress.module.js.map +1 -1
- package/dist/utils.main.js +57 -1
- package/dist/utils.main.js.map +1 -1
- package/dist/utils.mjs +55 -2
- package/dist/utils.module.js +55 -2
- package/dist/utils.module.js.map +1 -1
- package/package.json +7 -5
- package/src/Pressable.tsx +66 -6
- package/src/createEventHandler.ts +8 -1
- package/src/focusSafely.ts +45 -0
- package/src/index.ts +3 -0
- package/src/textSelection.ts +6 -4
- package/src/useFocus.ts +3 -3
- package/src/useFocusVisible.ts +14 -4
- package/src/useFocusWithin.ts +34 -5
- package/src/useFocusable.tsx +183 -0
- package/src/useHover.ts +17 -3
- package/src/useInteractOutside.ts +9 -3
- package/src/useLongPress.ts +8 -2
- package/src/usePress.ts +132 -131
- package/src/utils.ts +80 -1
- package/src/DOMPropsContext.ts +0 -39
- package/src/DOMPropsResponder.tsx +0 -47
- package/src/useDOMPropsResponder.ts +0 -27
package/src/usePress.ts
CHANGED
|
@@ -15,10 +15,27 @@
|
|
|
15
15
|
// NOTICE file in the root directory of this source tree.
|
|
16
16
|
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
chain,
|
|
20
|
+
focusWithoutScrolling,
|
|
21
|
+
getEventTarget,
|
|
22
|
+
getOwnerDocument,
|
|
23
|
+
getOwnerWindow,
|
|
24
|
+
isMac,
|
|
25
|
+
isVirtualClick,
|
|
26
|
+
isVirtualPointerEvent,
|
|
27
|
+
mergeProps,
|
|
28
|
+
nodeContains,
|
|
29
|
+
openLink,
|
|
30
|
+
useEffectEvent,
|
|
31
|
+
useGlobalListeners,
|
|
32
|
+
useSyncRef
|
|
33
|
+
} from '@react-aria/utils';
|
|
19
34
|
import {disableTextSelection, restoreTextSelection} from './textSelection';
|
|
20
35
|
import {DOMAttributes, FocusableElement, PressEvent as IPressEvent, PointerType, PressEvents, RefObject} from '@react-types/shared';
|
|
36
|
+
import {flushSync} from 'react-dom';
|
|
21
37
|
import {PressResponderContext} from './context';
|
|
38
|
+
import {preventFocus} from './utils';
|
|
22
39
|
import {TouchEvent as RTouchEvent, useContext, useEffect, useMemo, useRef, useState} from 'react';
|
|
23
40
|
|
|
24
41
|
export interface PressProps extends PressEvents {
|
|
@@ -47,7 +64,6 @@ export interface PressHookProps extends PressProps {
|
|
|
47
64
|
interface PressState {
|
|
48
65
|
isPressed: boolean,
|
|
49
66
|
ignoreEmulatedMouseEvents: boolean,
|
|
50
|
-
ignoreClickAfterPress: boolean,
|
|
51
67
|
didFirePressStart: boolean,
|
|
52
68
|
isTriggeringEvent: boolean,
|
|
53
69
|
activePointerId: any,
|
|
@@ -55,7 +71,8 @@ interface PressState {
|
|
|
55
71
|
isOverTarget: boolean,
|
|
56
72
|
pointerType: PointerType | null,
|
|
57
73
|
userSelect?: string,
|
|
58
|
-
metaKeyEvents?: Map<string, KeyboardEvent
|
|
74
|
+
metaKeyEvents?: Map<string, KeyboardEvent>,
|
|
75
|
+
disposables: Array<() => void>
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
interface EventBase {
|
|
@@ -167,13 +184,13 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
167
184
|
let ref = useRef<PressState>({
|
|
168
185
|
isPressed: false,
|
|
169
186
|
ignoreEmulatedMouseEvents: false,
|
|
170
|
-
ignoreClickAfterPress: false,
|
|
171
187
|
didFirePressStart: false,
|
|
172
188
|
isTriggeringEvent: false,
|
|
173
189
|
activePointerId: null,
|
|
174
190
|
target: null,
|
|
175
191
|
isOverTarget: false,
|
|
176
|
-
pointerType: null
|
|
192
|
+
pointerType: null,
|
|
193
|
+
disposables: []
|
|
177
194
|
});
|
|
178
195
|
|
|
179
196
|
let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();
|
|
@@ -208,7 +225,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
208
225
|
return false;
|
|
209
226
|
}
|
|
210
227
|
|
|
211
|
-
state.ignoreClickAfterPress = true;
|
|
212
228
|
state.didFirePressStart = false;
|
|
213
229
|
state.isTriggeringEvent = true;
|
|
214
230
|
|
|
@@ -255,7 +271,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
255
271
|
let cancel = useEffectEvent((e: EventBase) => {
|
|
256
272
|
let state = ref.current;
|
|
257
273
|
if (state.isPressed && state.target) {
|
|
258
|
-
if (state.
|
|
274
|
+
if (state.didFirePressStart && state.pointerType != null) {
|
|
259
275
|
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
|
260
276
|
}
|
|
261
277
|
state.isPressed = false;
|
|
@@ -266,6 +282,10 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
266
282
|
if (!allowTextSelectionOnPress) {
|
|
267
283
|
restoreTextSelection(state.target);
|
|
268
284
|
}
|
|
285
|
+
for (let dispose of state.disposables) {
|
|
286
|
+
dispose();
|
|
287
|
+
}
|
|
288
|
+
state.disposables = [];
|
|
269
289
|
}
|
|
270
290
|
});
|
|
271
291
|
|
|
@@ -279,8 +299,8 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
279
299
|
let state = ref.current;
|
|
280
300
|
let pressProps: DOMAttributes = {
|
|
281
301
|
onKeyDown(e) {
|
|
282
|
-
if (isValidKeyboardEvent(e.nativeEvent, e.currentTarget) && e.currentTarget
|
|
283
|
-
if (shouldPreventDefaultKeyboard(e.
|
|
302
|
+
if (isValidKeyboardEvent(e.nativeEvent, e.currentTarget) && nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
303
|
+
if (shouldPreventDefaultKeyboard(getEventTarget(e.nativeEvent), e.key)) {
|
|
284
304
|
e.preventDefault();
|
|
285
305
|
}
|
|
286
306
|
|
|
@@ -291,6 +311,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
291
311
|
if (!state.isPressed && !e.repeat) {
|
|
292
312
|
state.target = e.currentTarget;
|
|
293
313
|
state.isPressed = true;
|
|
314
|
+
state.pointerType = 'keyboard';
|
|
294
315
|
shouldStopPropagation = triggerPressStart(e, 'keyboard');
|
|
295
316
|
|
|
296
317
|
// Focus may move before the key up event, so register the event on the document
|
|
@@ -298,7 +319,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
298
319
|
// before stopPropagation from useKeyboard on a child element may happen and thus we can still call triggerPress for the parent element.
|
|
299
320
|
let originalTarget = e.currentTarget;
|
|
300
321
|
let pressUp = (e) => {
|
|
301
|
-
if (isValidKeyboardEvent(e, originalTarget) && !e.repeat && originalTarget
|
|
322
|
+
if (isValidKeyboardEvent(e, originalTarget) && !e.repeat && nodeContains(originalTarget, getEventTarget(e)) && state.target) {
|
|
302
323
|
triggerPressUp(createEvent(state.target, e), 'keyboard');
|
|
303
324
|
}
|
|
304
325
|
};
|
|
@@ -325,7 +346,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
325
346
|
}
|
|
326
347
|
},
|
|
327
348
|
onClick(e) {
|
|
328
|
-
if (e && !e.currentTarget
|
|
349
|
+
if (e && !nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
329
350
|
return;
|
|
330
351
|
}
|
|
331
352
|
|
|
@@ -334,23 +355,22 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
334
355
|
if (isDisabled) {
|
|
335
356
|
e.preventDefault();
|
|
336
357
|
}
|
|
337
|
-
|
|
358
|
+
|
|
338
359
|
// If triggered from a screen reader or by using element.click(),
|
|
339
360
|
// trigger as if it were a keyboard click.
|
|
340
|
-
if (!state.
|
|
341
|
-
// Ensure the element receives focus (VoiceOver on iOS does not do this)
|
|
342
|
-
if (!isDisabled && !preventFocusOnPress) {
|
|
343
|
-
focusWithoutScrolling(e.currentTarget);
|
|
344
|
-
}
|
|
345
|
-
|
|
361
|
+
if (!state.ignoreEmulatedMouseEvents && !state.isPressed && (state.pointerType === 'virtual' || isVirtualClick(e.nativeEvent))) {
|
|
346
362
|
let stopPressStart = triggerPressStart(e, 'virtual');
|
|
347
363
|
let stopPressUp = triggerPressUp(e, 'virtual');
|
|
348
364
|
let stopPressEnd = triggerPressEnd(e, 'virtual');
|
|
349
365
|
shouldStopPropagation = stopPressStart && stopPressUp && stopPressEnd;
|
|
366
|
+
} else if (state.isPressed && state.pointerType !== 'keyboard') {
|
|
367
|
+
let pointerType = state.pointerType || (e.nativeEvent as PointerEvent).pointerType as PointerType || 'virtual';
|
|
368
|
+
shouldStopPropagation = triggerPressEnd(createEvent(e.currentTarget, e), pointerType, true);
|
|
369
|
+
state.isOverTarget = false;
|
|
370
|
+
cancel(e);
|
|
350
371
|
}
|
|
351
372
|
|
|
352
373
|
state.ignoreEmulatedMouseEvents = false;
|
|
353
|
-
state.ignoreClickAfterPress = false;
|
|
354
374
|
if (shouldStopPropagation) {
|
|
355
375
|
e.stopPropagation();
|
|
356
376
|
}
|
|
@@ -360,18 +380,18 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
360
380
|
|
|
361
381
|
let onKeyUp = (e: KeyboardEvent) => {
|
|
362
382
|
if (state.isPressed && state.target && isValidKeyboardEvent(e, state.target)) {
|
|
363
|
-
if (shouldPreventDefaultKeyboard(e
|
|
383
|
+
if (shouldPreventDefaultKeyboard(getEventTarget(e), e.key)) {
|
|
364
384
|
e.preventDefault();
|
|
365
385
|
}
|
|
366
386
|
|
|
367
|
-
let target = e
|
|
368
|
-
triggerPressEnd(createEvent(state.target, e), 'keyboard', state.target
|
|
387
|
+
let target = getEventTarget(e);
|
|
388
|
+
triggerPressEnd(createEvent(state.target, e), 'keyboard', nodeContains(state.target, getEventTarget(e)));
|
|
369
389
|
removeAllGlobalListeners();
|
|
370
390
|
|
|
371
391
|
// If a link was triggered with a key other than Enter, open the URL ourselves.
|
|
372
392
|
// This means the link has a role override, and the default browser behavior
|
|
373
393
|
// only applies when using the Enter key.
|
|
374
|
-
if (e.key !== 'Enter' && isHTMLAnchorLink(state.target) && state.target
|
|
394
|
+
if (e.key !== 'Enter' && isHTMLAnchorLink(state.target) && nodeContains(state.target, target) && !e[LINK_CLICKED]) {
|
|
375
395
|
// Store a hidden property on the event so we only trigger link click once,
|
|
376
396
|
// even if there are multiple usePress instances attached to the element.
|
|
377
397
|
e[LINK_CLICKED] = true;
|
|
@@ -395,7 +415,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
395
415
|
if (typeof PointerEvent !== 'undefined') {
|
|
396
416
|
pressProps.onPointerDown = (e) => {
|
|
397
417
|
// Only handle left clicks, and ignore events that bubbled through portals.
|
|
398
|
-
if (e.button !== 0 || !e.currentTarget
|
|
418
|
+
if (e.button !== 0 || !nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
399
419
|
return;
|
|
400
420
|
}
|
|
401
421
|
|
|
@@ -408,12 +428,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
408
428
|
return;
|
|
409
429
|
}
|
|
410
430
|
|
|
411
|
-
// Due to browser inconsistencies, especially on mobile browsers, we prevent
|
|
412
|
-
// default on pointer down and handle focusing the pressable element ourselves.
|
|
413
|
-
if (shouldPreventDefaultDown(e.currentTarget as Element)) {
|
|
414
|
-
e.preventDefault();
|
|
415
|
-
}
|
|
416
|
-
|
|
417
431
|
state.pointerType = e.pointerType;
|
|
418
432
|
|
|
419
433
|
let shouldStopPropagation = true;
|
|
@@ -421,11 +435,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
421
435
|
state.isPressed = true;
|
|
422
436
|
state.isOverTarget = true;
|
|
423
437
|
state.activePointerId = e.pointerId;
|
|
424
|
-
state.target = e.currentTarget;
|
|
425
|
-
|
|
426
|
-
if (!isDisabled && !preventFocusOnPress) {
|
|
427
|
-
focusWithoutScrolling(e.currentTarget);
|
|
428
|
-
}
|
|
438
|
+
state.target = e.currentTarget as FocusableElement;
|
|
429
439
|
|
|
430
440
|
if (!allowTextSelectionOnPress) {
|
|
431
441
|
disableTextSelection(state.target);
|
|
@@ -433,7 +443,13 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
433
443
|
|
|
434
444
|
shouldStopPropagation = triggerPressStart(e, state.pointerType);
|
|
435
445
|
|
|
436
|
-
|
|
446
|
+
// Release pointer capture so that touch interactions can leave the original target.
|
|
447
|
+
// This enables onPointerLeave and onPointerEnter to fire.
|
|
448
|
+
let target = getEventTarget(e.nativeEvent);
|
|
449
|
+
if ('releasePointerCapture' in target) {
|
|
450
|
+
target.releasePointerCapture(e.pointerId);
|
|
451
|
+
}
|
|
452
|
+
|
|
437
453
|
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointerup', onPointerUp, false);
|
|
438
454
|
addGlobalListener(getOwnerDocument(e.currentTarget), 'pointercancel', onPointerCancel, false);
|
|
439
455
|
}
|
|
@@ -444,16 +460,16 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
444
460
|
};
|
|
445
461
|
|
|
446
462
|
pressProps.onMouseDown = (e) => {
|
|
447
|
-
if (!e.currentTarget
|
|
463
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
448
464
|
return;
|
|
449
465
|
}
|
|
450
466
|
|
|
451
467
|
if (e.button === 0) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
468
|
+
if (preventFocusOnPress) {
|
|
469
|
+
let dispose = preventFocus(e.target as FocusableElement);
|
|
470
|
+
if (dispose) {
|
|
471
|
+
state.disposables.push(dispose);
|
|
472
|
+
}
|
|
457
473
|
}
|
|
458
474
|
|
|
459
475
|
e.stopPropagation();
|
|
@@ -462,32 +478,25 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
462
478
|
|
|
463
479
|
pressProps.onPointerUp = (e) => {
|
|
464
480
|
// iOS fires pointerup with zero width and height, so check the pointerType recorded during pointerdown.
|
|
465
|
-
if (!e.currentTarget
|
|
481
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent)) || state.pointerType === 'virtual') {
|
|
466
482
|
return;
|
|
467
483
|
}
|
|
468
484
|
|
|
469
485
|
// Only handle left clicks
|
|
470
|
-
|
|
471
|
-
// when the touch isn't over the target, so double check.
|
|
472
|
-
if (e.button === 0 && isOverTarget(e, e.currentTarget)) {
|
|
486
|
+
if (e.button === 0) {
|
|
473
487
|
triggerPressUp(e, state.pointerType || e.pointerType);
|
|
474
488
|
}
|
|
475
489
|
};
|
|
476
490
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (e.pointerId !== state.activePointerId) {
|
|
482
|
-
return;
|
|
491
|
+
pressProps.onPointerEnter = (e) => {
|
|
492
|
+
if (e.pointerId === state.activePointerId && state.target && !state.isOverTarget && state.pointerType != null) {
|
|
493
|
+
state.isOverTarget = true;
|
|
494
|
+
triggerPressStart(createEvent(state.target, e), state.pointerType);
|
|
483
495
|
}
|
|
496
|
+
};
|
|
484
497
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
state.isOverTarget = true;
|
|
488
|
-
triggerPressStart(createEvent(state.target, e), state.pointerType);
|
|
489
|
-
}
|
|
490
|
-
} else if (state.target && state.isOverTarget && state.pointerType != null) {
|
|
498
|
+
pressProps.onPointerLeave = (e) => {
|
|
499
|
+
if (e.pointerId === state.activePointerId && state.target && state.isOverTarget && state.pointerType != null) {
|
|
491
500
|
state.isOverTarget = false;
|
|
492
501
|
triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
|
493
502
|
cancelOnPointerExit(e);
|
|
@@ -496,39 +505,38 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
496
505
|
|
|
497
506
|
let onPointerUp = (e: PointerEvent) => {
|
|
498
507
|
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) {
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
508
|
+
if (nodeContains(state.target, getEventTarget(e)) && state.pointerType != null) {
|
|
509
|
+
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
|
|
510
|
+
// is mutated between onPointerUp and onClick, and is more compatible with third party libraries.
|
|
511
|
+
// https://github.com/adobe/react-spectrum/issues/1513
|
|
512
|
+
// https://issues.chromium.org/issues/40732224
|
|
513
|
+
// However, iOS and Android do not focus or fire onClick after a long press.
|
|
514
|
+
// We work around this by triggering a click ourselves after a timeout.
|
|
515
|
+
// This timeout is canceled during the click event in case the real one fires first.
|
|
516
|
+
// The timeout must be at least 32ms, because Safari on iOS delays the click event on
|
|
517
|
+
// non-form elements without certain ARIA roles (for hover emulation).
|
|
518
|
+
// https://github.com/WebKit/WebKit/blob/dccfae42bb29bd4bdef052e469f604a9387241c0/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm#L875-L892
|
|
519
|
+
let clicked = false;
|
|
520
|
+
let timeout = setTimeout(() => {
|
|
521
|
+
if (state.isPressed && state.target instanceof HTMLElement) {
|
|
522
|
+
if (clicked) {
|
|
523
|
+
cancel(e);
|
|
524
|
+
} else {
|
|
525
|
+
focusWithoutScrolling(state.target);
|
|
526
|
+
state.target.click();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}, 80);
|
|
530
|
+
// Use a capturing listener to track if a click occurred.
|
|
531
|
+
// If stopPropagation is called it may never reach our handler.
|
|
532
|
+
addGlobalListener(e.currentTarget as Document, 'click', () => clicked = true, true);
|
|
533
|
+
state.disposables.push(() => clearTimeout(timeout));
|
|
534
|
+
} else {
|
|
535
|
+
cancel(e);
|
|
503
536
|
}
|
|
504
537
|
|
|
505
|
-
|
|
538
|
+
// Ignore subsequent onPointerLeave event before onClick on touch devices.
|
|
506
539
|
state.isOverTarget = false;
|
|
507
|
-
state.activePointerId = null;
|
|
508
|
-
state.pointerType = null;
|
|
509
|
-
removeAllGlobalListeners();
|
|
510
|
-
if (!allowTextSelectionOnPress) {
|
|
511
|
-
restoreTextSelection(state.target);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Prevent subsequent touchend event from triggering onClick on unrelated elements on Android. See below.
|
|
515
|
-
// Both 'touch' and 'pen' pointerTypes trigger onTouchEnd, but 'mouse' does not.
|
|
516
|
-
if ('ontouchend' in state.target && e.pointerType !== 'mouse') {
|
|
517
|
-
addGlobalListener(state.target, 'touchend', onTouchEnd, {once: true});
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
// This is a workaround for an Android Chrome/Firefox issue where click events are fired on an incorrect element
|
|
523
|
-
// if the original target is removed during onPointerUp (before onClick).
|
|
524
|
-
// https://github.com/adobe/react-spectrum/issues/1513
|
|
525
|
-
// https://issues.chromium.org/issues/40732224
|
|
526
|
-
// Note: this event must be registered directly on the element, not via React props in order to work.
|
|
527
|
-
// https://github.com/facebook/react/issues/9809
|
|
528
|
-
let onTouchEnd = (e: TouchEvent) => {
|
|
529
|
-
// Don't preventDefault if we actually want the default (e.g. submit/link click).
|
|
530
|
-
if (shouldPreventDefaultUp(e.currentTarget as Element)) {
|
|
531
|
-
e.preventDefault();
|
|
532
540
|
}
|
|
533
541
|
};
|
|
534
542
|
|
|
@@ -537,7 +545,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
537
545
|
};
|
|
538
546
|
|
|
539
547
|
pressProps.onDragStart = (e) => {
|
|
540
|
-
if (!e.currentTarget
|
|
548
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
541
549
|
return;
|
|
542
550
|
}
|
|
543
551
|
|
|
@@ -545,18 +553,15 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
545
553
|
cancel(e);
|
|
546
554
|
};
|
|
547
555
|
} else {
|
|
556
|
+
// NOTE: this fallback branch is almost entirely used by unit tests.
|
|
557
|
+
// All browsers now support pointer events, but JSDOM still does not.
|
|
558
|
+
|
|
548
559
|
pressProps.onMouseDown = (e) => {
|
|
549
560
|
// Only handle left clicks
|
|
550
|
-
if (e.button !== 0 || !e.currentTarget
|
|
561
|
+
if (e.button !== 0 || !nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
551
562
|
return;
|
|
552
563
|
}
|
|
553
564
|
|
|
554
|
-
// Due to browser inconsistencies, especially on mobile browsers, we prevent
|
|
555
|
-
// default on mouse down and handle focusing the pressable element ourselves.
|
|
556
|
-
if (shouldPreventDefaultDown(e.currentTarget)) {
|
|
557
|
-
e.preventDefault();
|
|
558
|
-
}
|
|
559
|
-
|
|
560
565
|
if (state.ignoreEmulatedMouseEvents) {
|
|
561
566
|
e.stopPropagation();
|
|
562
567
|
return;
|
|
@@ -567,20 +572,24 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
567
572
|
state.target = e.currentTarget;
|
|
568
573
|
state.pointerType = isVirtualClick(e.nativeEvent) ? 'virtual' : 'mouse';
|
|
569
574
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
let shouldStopPropagation = triggerPressStart(e, state.pointerType);
|
|
575
|
+
// Flush sync so that focus moved during react re-renders occurs before we yield back to the browser.
|
|
576
|
+
let shouldStopPropagation = flushSync(() => triggerPressStart(e, state.pointerType!));
|
|
575
577
|
if (shouldStopPropagation) {
|
|
576
578
|
e.stopPropagation();
|
|
577
579
|
}
|
|
578
580
|
|
|
581
|
+
if (preventFocusOnPress) {
|
|
582
|
+
let dispose = preventFocus(e.target as FocusableElement);
|
|
583
|
+
if (dispose) {
|
|
584
|
+
state.disposables.push(dispose);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
579
588
|
addGlobalListener(getOwnerDocument(e.currentTarget), 'mouseup', onMouseUp, false);
|
|
580
589
|
};
|
|
581
590
|
|
|
582
591
|
pressProps.onMouseEnter = (e) => {
|
|
583
|
-
if (!e.currentTarget
|
|
592
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
584
593
|
return;
|
|
585
594
|
}
|
|
586
595
|
|
|
@@ -596,7 +605,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
596
605
|
};
|
|
597
606
|
|
|
598
607
|
pressProps.onMouseLeave = (e) => {
|
|
599
|
-
if (!e.currentTarget
|
|
608
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
600
609
|
return;
|
|
601
610
|
}
|
|
602
611
|
|
|
@@ -613,7 +622,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
613
622
|
};
|
|
614
623
|
|
|
615
624
|
pressProps.onMouseUp = (e) => {
|
|
616
|
-
if (!e.currentTarget
|
|
625
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
617
626
|
return;
|
|
618
627
|
}
|
|
619
628
|
|
|
@@ -628,25 +637,23 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
628
637
|
return;
|
|
629
638
|
}
|
|
630
639
|
|
|
631
|
-
state.isPressed = false;
|
|
632
|
-
removeAllGlobalListeners();
|
|
633
|
-
|
|
634
640
|
if (state.ignoreEmulatedMouseEvents) {
|
|
635
641
|
state.ignoreEmulatedMouseEvents = false;
|
|
636
642
|
return;
|
|
637
643
|
}
|
|
638
644
|
|
|
639
|
-
if (state.target &&
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
645
|
+
if (state.target && state.target.contains(e.target as Element) && state.pointerType != null) {
|
|
646
|
+
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
|
|
647
|
+
// is mutated between onMouseUp and onClick, and is more compatible with third party libraries.
|
|
648
|
+
} else {
|
|
649
|
+
cancel(e);
|
|
643
650
|
}
|
|
644
651
|
|
|
645
652
|
state.isOverTarget = false;
|
|
646
653
|
};
|
|
647
654
|
|
|
648
655
|
pressProps.onTouchStart = (e) => {
|
|
649
|
-
if (!e.currentTarget
|
|
656
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
650
657
|
return;
|
|
651
658
|
}
|
|
652
659
|
|
|
@@ -661,12 +668,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
661
668
|
state.target = e.currentTarget;
|
|
662
669
|
state.pointerType = 'touch';
|
|
663
670
|
|
|
664
|
-
// Due to browser inconsistencies, especially on mobile browsers, we prevent default
|
|
665
|
-
// on the emulated mouse event and handle focusing the pressable element ourselves.
|
|
666
|
-
if (!isDisabled && !preventFocusOnPress) {
|
|
667
|
-
focusWithoutScrolling(e.currentTarget);
|
|
668
|
-
}
|
|
669
|
-
|
|
670
671
|
if (!allowTextSelectionOnPress) {
|
|
671
672
|
disableTextSelection(state.target);
|
|
672
673
|
}
|
|
@@ -680,7 +681,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
680
681
|
};
|
|
681
682
|
|
|
682
683
|
pressProps.onTouchMove = (e) => {
|
|
683
|
-
if (!e.currentTarget
|
|
684
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
684
685
|
return;
|
|
685
686
|
}
|
|
686
687
|
|
|
@@ -708,7 +709,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
708
709
|
};
|
|
709
710
|
|
|
710
711
|
pressProps.onTouchEnd = (e) => {
|
|
711
|
-
if (!e.currentTarget
|
|
712
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
712
713
|
return;
|
|
713
714
|
}
|
|
714
715
|
|
|
@@ -741,7 +742,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
741
742
|
};
|
|
742
743
|
|
|
743
744
|
pressProps.onTouchCancel = (e) => {
|
|
744
|
-
if (!e.currentTarget
|
|
745
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
745
746
|
return;
|
|
746
747
|
}
|
|
747
748
|
|
|
@@ -752,7 +753,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
752
753
|
};
|
|
753
754
|
|
|
754
755
|
let onScroll = (e: Event) => {
|
|
755
|
-
if (state.isPressed && (e
|
|
756
|
+
if (state.isPressed && nodeContains(getEventTarget(e), state.target)) {
|
|
756
757
|
cancel({
|
|
757
758
|
currentTarget: state.target,
|
|
758
759
|
shiftKey: false,
|
|
@@ -764,7 +765,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
764
765
|
};
|
|
765
766
|
|
|
766
767
|
pressProps.onDragStart = (e) => {
|
|
767
|
-
if (!e.currentTarget
|
|
768
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
768
769
|
return;
|
|
769
770
|
}
|
|
770
771
|
|
|
@@ -787,13 +788,18 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
787
788
|
]);
|
|
788
789
|
|
|
789
790
|
// Remove user-select: none in case component unmounts immediately after pressStart
|
|
790
|
-
|
|
791
|
+
|
|
791
792
|
useEffect(() => {
|
|
793
|
+
let state = ref.current;
|
|
792
794
|
return () => {
|
|
793
795
|
if (!allowTextSelectionOnPress) {
|
|
794
|
-
|
|
795
|
-
restoreTextSelection(
|
|
796
|
+
|
|
797
|
+
restoreTextSelection(state.target ?? undefined);
|
|
796
798
|
}
|
|
799
|
+
for (let dispose of state.disposables) {
|
|
800
|
+
dispose();
|
|
801
|
+
}
|
|
802
|
+
state.disposables = [];
|
|
797
803
|
};
|
|
798
804
|
}, [allowTextSelectionOnPress]);
|
|
799
805
|
|
|
@@ -933,11 +939,6 @@ function isOverTarget(point: EventPoint, target: Element) {
|
|
|
933
939
|
return areRectanglesOverlapping(rect, pointRect);
|
|
934
940
|
}
|
|
935
941
|
|
|
936
|
-
function shouldPreventDefaultDown(target: Element) {
|
|
937
|
-
// We cannot prevent default if the target is a draggable element.
|
|
938
|
-
return !(target instanceof HTMLElement) || !target.hasAttribute('draggable');
|
|
939
|
-
}
|
|
940
|
-
|
|
941
942
|
function shouldPreventDefaultUp(target: Element) {
|
|
942
943
|
if (target instanceof HTMLInputElement) {
|
|
943
944
|
return false;
|
package/src/utils.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import {FocusableElement} from '@react-types/shared';
|
|
14
|
+
import {focusWithoutScrolling, getOwnerWindow, isFocusable, useEffectEvent, useLayoutEffect} from '@react-aria/utils';
|
|
13
15
|
import {FocusEvent as ReactFocusEvent, useCallback, useRef} from 'react';
|
|
14
|
-
import {useEffectEvent, useLayoutEffect} from '@react-aria/utils';
|
|
15
16
|
|
|
16
17
|
export class SyntheticFocusEvent<Target = Element> implements ReactFocusEvent<Target> {
|
|
17
18
|
nativeEvent: FocusEvent;
|
|
@@ -128,3 +129,81 @@ export function useSyntheticBlurEvent<Target = Element>(onBlur: (e: ReactFocusEv
|
|
|
128
129
|
}
|
|
129
130
|
}, [dispatchBlur]);
|
|
130
131
|
}
|
|
132
|
+
|
|
133
|
+
export let ignoreFocusEvent = false;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* This function prevents the next focus event fired on `target`, without using `event.preventDefault()`.
|
|
137
|
+
* It works by waiting for the series of focus events to occur, and reverts focus back to where it was before.
|
|
138
|
+
* It also makes these events mostly non-observable by using a capturing listener on the window and stopping propagation.
|
|
139
|
+
*/
|
|
140
|
+
export function preventFocus(target: FocusableElement | null) {
|
|
141
|
+
// The browser will focus the nearest focusable ancestor of our target.
|
|
142
|
+
while (target && !isFocusable(target)) {
|
|
143
|
+
target = target.parentElement;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let window = getOwnerWindow(target);
|
|
147
|
+
let activeElement = window.document.activeElement as FocusableElement | null;
|
|
148
|
+
if (!activeElement || activeElement === target) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
ignoreFocusEvent = true;
|
|
153
|
+
let isRefocusing = false;
|
|
154
|
+
let onBlur = (e: FocusEvent) => {
|
|
155
|
+
if (e.target === activeElement || isRefocusing) {
|
|
156
|
+
e.stopImmediatePropagation();
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
let onFocusOut = (e: FocusEvent) => {
|
|
161
|
+
if (e.target === activeElement || isRefocusing) {
|
|
162
|
+
e.stopImmediatePropagation();
|
|
163
|
+
|
|
164
|
+
// If there was no focusable ancestor, we don't expect a focus event.
|
|
165
|
+
// Re-focus the original active element here.
|
|
166
|
+
if (!target && !isRefocusing) {
|
|
167
|
+
isRefocusing = true;
|
|
168
|
+
focusWithoutScrolling(activeElement);
|
|
169
|
+
cleanup();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
let onFocus = (e: FocusEvent) => {
|
|
175
|
+
if (e.target === target || isRefocusing) {
|
|
176
|
+
e.stopImmediatePropagation();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
let onFocusIn = (e: FocusEvent) => {
|
|
181
|
+
if (e.target === target || isRefocusing) {
|
|
182
|
+
e.stopImmediatePropagation();
|
|
183
|
+
|
|
184
|
+
if (!isRefocusing) {
|
|
185
|
+
isRefocusing = true;
|
|
186
|
+
focusWithoutScrolling(activeElement);
|
|
187
|
+
cleanup();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
window.addEventListener('blur', onBlur, true);
|
|
193
|
+
window.addEventListener('focusout', onFocusOut, true);
|
|
194
|
+
window.addEventListener('focusin', onFocusIn, true);
|
|
195
|
+
window.addEventListener('focus', onFocus, true);
|
|
196
|
+
|
|
197
|
+
let cleanup = () => {
|
|
198
|
+
cancelAnimationFrame(raf);
|
|
199
|
+
window.removeEventListener('blur', onBlur, true);
|
|
200
|
+
window.removeEventListener('focusout', onFocusOut, true);
|
|
201
|
+
window.removeEventListener('focusin', onFocusIn, true);
|
|
202
|
+
window.removeEventListener('focus', onFocus, true);
|
|
203
|
+
ignoreFocusEvent = false;
|
|
204
|
+
isRefocusing = false;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
let raf = requestAnimationFrame(cleanup);
|
|
208
|
+
return cleanup;
|
|
209
|
+
}
|