@react-aria/interactions 3.23.0 → 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/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 +85 -80
- package/dist/usePress.main.js.map +1 -1
- package/dist/usePress.mjs +86 -81
- package/dist/usePress.module.js +86 -81
- 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 +5 -4
- package/src/Pressable.tsx +66 -6
- 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 +117 -115
- 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);
|
|
@@ -435,7 +445,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
435
445
|
|
|
436
446
|
// Release pointer capture so that touch interactions can leave the original target.
|
|
437
447
|
// This enables onPointerLeave and onPointerEnter to fire.
|
|
438
|
-
let target = e.
|
|
448
|
+
let target = getEventTarget(e.nativeEvent);
|
|
439
449
|
if ('releasePointerCapture' in target) {
|
|
440
450
|
target.releasePointerCapture(e.pointerId);
|
|
441
451
|
}
|
|
@@ -450,16 +460,16 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
450
460
|
};
|
|
451
461
|
|
|
452
462
|
pressProps.onMouseDown = (e) => {
|
|
453
|
-
if (!e.currentTarget
|
|
463
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
454
464
|
return;
|
|
455
465
|
}
|
|
456
466
|
|
|
457
467
|
if (e.button === 0) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
468
|
+
if (preventFocusOnPress) {
|
|
469
|
+
let dispose = preventFocus(e.target as FocusableElement);
|
|
470
|
+
if (dispose) {
|
|
471
|
+
state.disposables.push(dispose);
|
|
472
|
+
}
|
|
463
473
|
}
|
|
464
474
|
|
|
465
475
|
e.stopPropagation();
|
|
@@ -468,7 +478,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
468
478
|
|
|
469
479
|
pressProps.onPointerUp = (e) => {
|
|
470
480
|
// iOS fires pointerup with zero width and height, so check the pointerType recorded during pointerdown.
|
|
471
|
-
if (!e.currentTarget
|
|
481
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent)) || state.pointerType === 'virtual') {
|
|
472
482
|
return;
|
|
473
483
|
}
|
|
474
484
|
|
|
@@ -495,39 +505,38 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
495
505
|
|
|
496
506
|
let onPointerUp = (e: PointerEvent) => {
|
|
497
507
|
if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0 && state.target) {
|
|
498
|
-
if (state.target
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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);
|
|
502
536
|
}
|
|
503
537
|
|
|
504
|
-
|
|
538
|
+
// Ignore subsequent onPointerLeave event before onClick on touch devices.
|
|
505
539
|
state.isOverTarget = false;
|
|
506
|
-
state.activePointerId = null;
|
|
507
|
-
state.pointerType = null;
|
|
508
|
-
removeAllGlobalListeners();
|
|
509
|
-
if (!allowTextSelectionOnPress) {
|
|
510
|
-
restoreTextSelection(state.target);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Prevent subsequent touchend event from triggering onClick on unrelated elements on Android. See below.
|
|
514
|
-
// Both 'touch' and 'pen' pointerTypes trigger onTouchEnd, but 'mouse' does not.
|
|
515
|
-
if ('ontouchend' in state.target && e.pointerType !== 'mouse') {
|
|
516
|
-
addGlobalListener(state.target, 'touchend', onTouchEnd, {once: true});
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
// This is a workaround for an Android Chrome/Firefox issue where click events are fired on an incorrect element
|
|
522
|
-
// if the original target is removed during onPointerUp (before onClick).
|
|
523
|
-
// https://github.com/adobe/react-spectrum/issues/1513
|
|
524
|
-
// https://issues.chromium.org/issues/40732224
|
|
525
|
-
// Note: this event must be registered directly on the element, not via React props in order to work.
|
|
526
|
-
// https://github.com/facebook/react/issues/9809
|
|
527
|
-
let onTouchEnd = (e: TouchEvent) => {
|
|
528
|
-
// Don't preventDefault if we actually want the default (e.g. submit/link click).
|
|
529
|
-
if (shouldPreventDefaultUp(e.currentTarget as Element)) {
|
|
530
|
-
e.preventDefault();
|
|
531
540
|
}
|
|
532
541
|
};
|
|
533
542
|
|
|
@@ -536,7 +545,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
536
545
|
};
|
|
537
546
|
|
|
538
547
|
pressProps.onDragStart = (e) => {
|
|
539
|
-
if (!e.currentTarget
|
|
548
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
540
549
|
return;
|
|
541
550
|
}
|
|
542
551
|
|
|
@@ -544,18 +553,15 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
544
553
|
cancel(e);
|
|
545
554
|
};
|
|
546
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
|
+
|
|
547
559
|
pressProps.onMouseDown = (e) => {
|
|
548
560
|
// Only handle left clicks
|
|
549
|
-
if (e.button !== 0 || !e.currentTarget
|
|
561
|
+
if (e.button !== 0 || !nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
550
562
|
return;
|
|
551
563
|
}
|
|
552
564
|
|
|
553
|
-
// Due to browser inconsistencies, especially on mobile browsers, we prevent
|
|
554
|
-
// default on mouse down and handle focusing the pressable element ourselves.
|
|
555
|
-
if (shouldPreventDefaultDown(e.currentTarget)) {
|
|
556
|
-
e.preventDefault();
|
|
557
|
-
}
|
|
558
|
-
|
|
559
565
|
if (state.ignoreEmulatedMouseEvents) {
|
|
560
566
|
e.stopPropagation();
|
|
561
567
|
return;
|
|
@@ -566,20 +572,24 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
566
572
|
state.target = e.currentTarget;
|
|
567
573
|
state.pointerType = isVirtualClick(e.nativeEvent) ? 'virtual' : 'mouse';
|
|
568
574
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
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!));
|
|
574
577
|
if (shouldStopPropagation) {
|
|
575
578
|
e.stopPropagation();
|
|
576
579
|
}
|
|
577
580
|
|
|
581
|
+
if (preventFocusOnPress) {
|
|
582
|
+
let dispose = preventFocus(e.target as FocusableElement);
|
|
583
|
+
if (dispose) {
|
|
584
|
+
state.disposables.push(dispose);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
578
588
|
addGlobalListener(getOwnerDocument(e.currentTarget), 'mouseup', onMouseUp, false);
|
|
579
589
|
};
|
|
580
590
|
|
|
581
591
|
pressProps.onMouseEnter = (e) => {
|
|
582
|
-
if (!e.currentTarget
|
|
592
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
583
593
|
return;
|
|
584
594
|
}
|
|
585
595
|
|
|
@@ -595,7 +605,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
595
605
|
};
|
|
596
606
|
|
|
597
607
|
pressProps.onMouseLeave = (e) => {
|
|
598
|
-
if (!e.currentTarget
|
|
608
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
599
609
|
return;
|
|
600
610
|
}
|
|
601
611
|
|
|
@@ -612,7 +622,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
612
622
|
};
|
|
613
623
|
|
|
614
624
|
pressProps.onMouseUp = (e) => {
|
|
615
|
-
if (!e.currentTarget
|
|
625
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
616
626
|
return;
|
|
617
627
|
}
|
|
618
628
|
|
|
@@ -627,25 +637,23 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
627
637
|
return;
|
|
628
638
|
}
|
|
629
639
|
|
|
630
|
-
state.isPressed = false;
|
|
631
|
-
removeAllGlobalListeners();
|
|
632
|
-
|
|
633
640
|
if (state.ignoreEmulatedMouseEvents) {
|
|
634
641
|
state.ignoreEmulatedMouseEvents = false;
|
|
635
642
|
return;
|
|
636
643
|
}
|
|
637
644
|
|
|
638
|
-
if (state.target &&
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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);
|
|
642
650
|
}
|
|
643
651
|
|
|
644
652
|
state.isOverTarget = false;
|
|
645
653
|
};
|
|
646
654
|
|
|
647
655
|
pressProps.onTouchStart = (e) => {
|
|
648
|
-
if (!e.currentTarget
|
|
656
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
649
657
|
return;
|
|
650
658
|
}
|
|
651
659
|
|
|
@@ -660,12 +668,6 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
660
668
|
state.target = e.currentTarget;
|
|
661
669
|
state.pointerType = 'touch';
|
|
662
670
|
|
|
663
|
-
// Due to browser inconsistencies, especially on mobile browsers, we prevent default
|
|
664
|
-
// on the emulated mouse event and handle focusing the pressable element ourselves.
|
|
665
|
-
if (!isDisabled && !preventFocusOnPress) {
|
|
666
|
-
focusWithoutScrolling(e.currentTarget);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
671
|
if (!allowTextSelectionOnPress) {
|
|
670
672
|
disableTextSelection(state.target);
|
|
671
673
|
}
|
|
@@ -679,7 +681,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
679
681
|
};
|
|
680
682
|
|
|
681
683
|
pressProps.onTouchMove = (e) => {
|
|
682
|
-
if (!e.currentTarget
|
|
684
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
683
685
|
return;
|
|
684
686
|
}
|
|
685
687
|
|
|
@@ -707,7 +709,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
707
709
|
};
|
|
708
710
|
|
|
709
711
|
pressProps.onTouchEnd = (e) => {
|
|
710
|
-
if (!e.currentTarget
|
|
712
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
711
713
|
return;
|
|
712
714
|
}
|
|
713
715
|
|
|
@@ -740,7 +742,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
740
742
|
};
|
|
741
743
|
|
|
742
744
|
pressProps.onTouchCancel = (e) => {
|
|
743
|
-
if (!e.currentTarget
|
|
745
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
744
746
|
return;
|
|
745
747
|
}
|
|
746
748
|
|
|
@@ -751,7 +753,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
751
753
|
};
|
|
752
754
|
|
|
753
755
|
let onScroll = (e: Event) => {
|
|
754
|
-
if (state.isPressed && (e
|
|
756
|
+
if (state.isPressed && nodeContains(getEventTarget(e), state.target)) {
|
|
755
757
|
cancel({
|
|
756
758
|
currentTarget: state.target,
|
|
757
759
|
shiftKey: false,
|
|
@@ -763,7 +765,7 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
763
765
|
};
|
|
764
766
|
|
|
765
767
|
pressProps.onDragStart = (e) => {
|
|
766
|
-
if (!e.currentTarget
|
|
768
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
|
|
767
769
|
return;
|
|
768
770
|
}
|
|
769
771
|
|
|
@@ -788,11 +790,16 @@ export function usePress(props: PressHookProps): PressResult {
|
|
|
788
790
|
// Remove user-select: none in case component unmounts immediately after pressStart
|
|
789
791
|
|
|
790
792
|
useEffect(() => {
|
|
793
|
+
let state = ref.current;
|
|
791
794
|
return () => {
|
|
792
795
|
if (!allowTextSelectionOnPress) {
|
|
793
|
-
|
|
794
|
-
restoreTextSelection(
|
|
796
|
+
|
|
797
|
+
restoreTextSelection(state.target ?? undefined);
|
|
798
|
+
}
|
|
799
|
+
for (let dispose of state.disposables) {
|
|
800
|
+
dispose();
|
|
795
801
|
}
|
|
802
|
+
state.disposables = [];
|
|
796
803
|
};
|
|
797
804
|
}, [allowTextSelectionOnPress]);
|
|
798
805
|
|
|
@@ -932,11 +939,6 @@ function isOverTarget(point: EventPoint, target: Element) {
|
|
|
932
939
|
return areRectanglesOverlapping(rect, pointRect);
|
|
933
940
|
}
|
|
934
941
|
|
|
935
|
-
function shouldPreventDefaultDown(target: Element) {
|
|
936
|
-
// We cannot prevent default if the target is a draggable element.
|
|
937
|
-
return !(target instanceof HTMLElement) || !target.hasAttribute('draggable');
|
|
938
|
-
}
|
|
939
|
-
|
|
940
942
|
function shouldPreventDefaultUp(target: Element) {
|
|
941
943
|
if (target instanceof HTMLInputElement) {
|
|
942
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
|
+
}
|
package/src/DOMPropsContext.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
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 {DOMAttributes, RefObject} from '@react-types/shared';
|
|
14
|
-
import {mergeProps, useSyncRef} from '@react-aria/utils';
|
|
15
|
-
import React, {MutableRefObject, useContext} from 'react';
|
|
16
|
-
|
|
17
|
-
interface DOMPropsResponderProps extends DOMAttributes {
|
|
18
|
-
ref?: RefObject<Element | null>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface IDOMPropsResponderContext extends DOMAttributes {
|
|
22
|
-
register(): void,
|
|
23
|
-
ref?: MutableRefObject<Element | null>
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const DOMPropsResponderContext = React.createContext<IDOMPropsResponderContext | null>(null);
|
|
27
|
-
|
|
28
|
-
export function useDOMPropsResponderContext(props: DOMPropsResponderProps): DOMPropsResponderProps {
|
|
29
|
-
// Consume context from <DOMPropsResponder> and merge with props.
|
|
30
|
-
let context = useContext(DOMPropsResponderContext);
|
|
31
|
-
if (context) {
|
|
32
|
-
let {register, ...contextProps} = context;
|
|
33
|
-
props = mergeProps(contextProps, props);
|
|
34
|
-
register();
|
|
35
|
-
}
|
|
36
|
-
useSyncRef(context, props.ref);
|
|
37
|
-
|
|
38
|
-
return props;
|
|
39
|
-
}
|