@react-aria/interactions 3.23.0 → 3.24.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.
Files changed (84) hide show
  1. package/dist/Pressable.main.js +28 -4
  2. package/dist/Pressable.main.js.map +1 -1
  3. package/dist/Pressable.mjs +30 -6
  4. package/dist/Pressable.module.js +30 -6
  5. package/dist/Pressable.module.js.map +1 -1
  6. package/dist/focusSafely.main.js +40 -0
  7. package/dist/focusSafely.main.js.map +1 -0
  8. package/dist/focusSafely.mjs +35 -0
  9. package/dist/focusSafely.module.js +35 -0
  10. package/dist/focusSafely.module.js.map +1 -0
  11. package/dist/import.mjs +5 -1
  12. package/dist/main.js +9 -0
  13. package/dist/main.js.map +1 -1
  14. package/dist/module.js +5 -1
  15. package/dist/module.js.map +1 -1
  16. package/dist/textSelection.main.js +5 -3
  17. package/dist/textSelection.main.js.map +1 -1
  18. package/dist/textSelection.mjs +5 -3
  19. package/dist/textSelection.module.js +5 -3
  20. package/dist/textSelection.module.js.map +1 -1
  21. package/dist/types.d.ts +59 -25
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/useFocus.main.js +2 -1
  24. package/dist/useFocus.main.js.map +1 -1
  25. package/dist/useFocus.mjs +3 -2
  26. package/dist/useFocus.module.js +3 -2
  27. package/dist/useFocus.module.js.map +1 -1
  28. package/dist/useFocusVisible.main.js +9 -3
  29. package/dist/useFocusVisible.main.js.map +1 -1
  30. package/dist/useFocusVisible.mjs +9 -3
  31. package/dist/useFocusVisible.module.js +9 -3
  32. package/dist/useFocusVisible.module.js.map +1 -1
  33. package/dist/useFocusWithin.main.js +33 -4
  34. package/dist/useFocusWithin.main.js.map +1 -1
  35. package/dist/useFocusWithin.mjs +34 -5
  36. package/dist/useFocusWithin.module.js +34 -5
  37. package/dist/useFocusWithin.module.js.map +1 -1
  38. package/dist/useFocusable.main.js +112 -0
  39. package/dist/useFocusable.main.js.map +1 -0
  40. package/dist/useFocusable.mjs +100 -0
  41. package/dist/useFocusable.module.js +100 -0
  42. package/dist/useFocusable.module.js.map +1 -0
  43. package/dist/useHover.main.js +18 -3
  44. package/dist/useHover.main.js.map +1 -1
  45. package/dist/useHover.mjs +18 -3
  46. package/dist/useHover.module.js +18 -3
  47. package/dist/useHover.module.js.map +1 -1
  48. package/dist/useInteractOutside.main.js +6 -1
  49. package/dist/useInteractOutside.main.js.map +1 -1
  50. package/dist/useInteractOutside.mjs +6 -1
  51. package/dist/useInteractOutside.module.js +6 -1
  52. package/dist/useInteractOutside.module.js.map +1 -1
  53. package/dist/useLongPress.main.js +2 -0
  54. package/dist/useLongPress.main.js.map +1 -1
  55. package/dist/useLongPress.mjs +3 -1
  56. package/dist/useLongPress.module.js +3 -1
  57. package/dist/useLongPress.module.js.map +1 -1
  58. package/dist/usePress.main.js +85 -80
  59. package/dist/usePress.main.js.map +1 -1
  60. package/dist/usePress.mjs +86 -81
  61. package/dist/usePress.module.js +86 -81
  62. package/dist/usePress.module.js.map +1 -1
  63. package/dist/utils.main.js +57 -1
  64. package/dist/utils.main.js.map +1 -1
  65. package/dist/utils.mjs +55 -2
  66. package/dist/utils.module.js +55 -2
  67. package/dist/utils.module.js.map +1 -1
  68. package/package.json +5 -4
  69. package/src/Pressable.tsx +66 -6
  70. package/src/focusSafely.ts +45 -0
  71. package/src/index.ts +3 -0
  72. package/src/textSelection.ts +6 -4
  73. package/src/useFocus.ts +3 -3
  74. package/src/useFocusVisible.ts +14 -4
  75. package/src/useFocusWithin.ts +34 -5
  76. package/src/useFocusable.tsx +183 -0
  77. package/src/useHover.ts +17 -3
  78. package/src/useInteractOutside.ts +9 -3
  79. package/src/useLongPress.ts +8 -2
  80. package/src/usePress.ts +117 -115
  81. package/src/utils.ts +80 -1
  82. package/src/DOMPropsContext.ts +0 -39
  83. package/src/DOMPropsResponder.tsx +0 -47
  84. 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 {chain, focusWithoutScrolling, getOwnerDocument, getOwnerWindow, isMac, isVirtualClick, isVirtualPointerEvent, mergeProps, openLink, useEffectEvent, useGlobalListeners, useSyncRef} from '@react-aria/utils';
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.isOverTarget && state.pointerType != null) {
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.contains(e.target as Element)) {
283
- if (shouldPreventDefaultKeyboard(e.target as Element, e.key)) {
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.contains(e.target as Element) && state.target) {
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.contains(e.target as Element)) {
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.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && !state.isPressed && (state.pointerType === 'virtual' || isVirtualClick(e.nativeEvent))) {
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.target as Element, e.key)) {
383
+ if (shouldPreventDefaultKeyboard(getEventTarget(e), e.key)) {
364
384
  e.preventDefault();
365
385
  }
366
386
 
367
- let target = e.target as Element;
368
- triggerPressEnd(createEvent(state.target, e), 'keyboard', state.target.contains(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.contains(target) && !e[LINK_CLICKED]) {
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.contains(e.target as Element)) {
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.target as Element;
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.contains(e.target as Element)) {
463
+ if (!nodeContains(e.currentTarget, getEventTarget(e.nativeEvent))) {
454
464
  return;
455
465
  }
456
466
 
457
467
  if (e.button === 0) {
458
- // Chrome and Firefox on touch Windows devices require mouse down events
459
- // to be canceled in addition to pointer events, or an extra asynchronous
460
- // focus event will be fired.
461
- if (shouldPreventDefaultDown(e.currentTarget as Element)) {
462
- e.preventDefault();
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.contains(e.target as Element) || state.pointerType === 'virtual') {
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.contains(e.target as Element) && state.pointerType != null) {
499
- triggerPressEnd(createEvent(state.target, e), state.pointerType);
500
- } else if (state.isOverTarget && state.pointerType != null) {
501
- triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
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
- state.isPressed = false;
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.contains(e.target as Element)) {
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.contains(e.target as Element)) {
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
- if (!isDisabled && !preventFocusOnPress) {
570
- focusWithoutScrolling(e.currentTarget);
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.contains(e.target as Element)) {
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.contains(e.target as Element)) {
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.contains(e.target as Element)) {
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 && isOverTarget(e, state.target) && state.pointerType != null) {
639
- triggerPressEnd(createEvent(state.target, e), state.pointerType);
640
- } else if (state.target && state.isOverTarget && state.pointerType != null) {
641
- triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
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.contains(e.target as Element)) {
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.contains(e.target as Element)) {
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.contains(e.target as Element)) {
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.contains(e.target as Element)) {
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.target as Element).contains(state.target)) {
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.contains(e.target as Element)) {
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
- // eslint-disable-next-line react-hooks/exhaustive-deps
794
- restoreTextSelection(ref.current.target ?? undefined);
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
+ }
@@ -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
- }