@react-aria/interactions 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.js CHANGED
@@ -1,44 +1,72 @@
1
1
  import _react, { useContext, useEffect, useMemo, useRef, useState, useCallback } from "react";
2
- import { mergeProps, runAfterTransition, focusWithoutScrolling, useGlobalListeners, useSyncRef, isMac, useEvent } from "@react-aria/utils";
2
+ import { mergeProps, isIOS, runAfterTransition, focusWithoutScrolling, useGlobalListeners, useSyncRef, isMac, useEvent, useDescription } from "@react-aria/utils";
3
3
  import _babelRuntimeHelpersEsmObjectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
4
4
  import _babelRuntimeHelpersEsmExtends from "@babel/runtime/helpers/esm/extends";
5
+ // Note that state only matters here for iOS. Non-iOS gets user-select: none applied to the target element
6
+ // rather than at the document level so we just need to apply/remove user-select: none for each pressed element individually
5
7
  let $e17c9db826984f8ab8e5d837bf0b8$var$state = 'default';
6
8
  let $e17c9db826984f8ab8e5d837bf0b8$var$savedUserSelect = '';
9
+ let $e17c9db826984f8ab8e5d837bf0b8$var$modifiedElementMap = new WeakMap();
7
10
 
8
- function $e17c9db826984f8ab8e5d837bf0b8$export$disableTextSelection() {
9
- if ($e17c9db826984f8ab8e5d837bf0b8$var$state === 'default') {
10
- $e17c9db826984f8ab8e5d837bf0b8$var$savedUserSelect = document.documentElement.style.webkitUserSelect;
11
- document.documentElement.style.webkitUserSelect = 'none';
12
- }
11
+ function $e17c9db826984f8ab8e5d837bf0b8$export$disableTextSelection(target) {
12
+ if (isIOS()) {
13
+ if ($e17c9db826984f8ab8e5d837bf0b8$var$state === 'default') {
14
+ $e17c9db826984f8ab8e5d837bf0b8$var$savedUserSelect = document.documentElement.style.webkitUserSelect;
15
+ document.documentElement.style.webkitUserSelect = 'none';
16
+ }
13
17
 
14
- $e17c9db826984f8ab8e5d837bf0b8$var$state = 'disabled';
18
+ $e17c9db826984f8ab8e5d837bf0b8$var$state = 'disabled';
19
+ } else if (target) {
20
+ // If not iOS, store the target's original user-select and change to user-select: none
21
+ // Ignore state since it doesn't apply for non iOS
22
+ $e17c9db826984f8ab8e5d837bf0b8$var$modifiedElementMap.set(target, target.style.userSelect);
23
+ target.style.userSelect = 'none';
24
+ }
15
25
  }
16
26
 
17
- function $e17c9db826984f8ab8e5d837bf0b8$export$restoreTextSelection() {
18
- // If the state is already default, there's nothing to do.
19
- // If it is restoring, then there's no need to queue a second restore.
20
- if ($e17c9db826984f8ab8e5d837bf0b8$var$state !== 'disabled') {
21
- return;
22
- }
27
+ function $e17c9db826984f8ab8e5d837bf0b8$export$restoreTextSelection(target) {
28
+ if (isIOS()) {
29
+ // If the state is already default, there's nothing to do.
30
+ // If it is restoring, then there's no need to queue a second restore.
31
+ if ($e17c9db826984f8ab8e5d837bf0b8$var$state !== 'disabled') {
32
+ return;
33
+ }
23
34
 
24
- $e17c9db826984f8ab8e5d837bf0b8$var$state = 'restoring'; // There appears to be a delay on iOS where selection still might occur
25
- // after pointer up, so wait a bit before removing user-select.
35
+ $e17c9db826984f8ab8e5d837bf0b8$var$state = 'restoring'; // There appears to be a delay on iOS where selection still might occur
36
+ // after pointer up, so wait a bit before removing user-select.
37
+
38
+ setTimeout(() => {
39
+ // Wait for any CSS transitions to complete so we don't recompute style
40
+ // for the whole page in the middle of the animation and cause jank.
41
+ runAfterTransition(() => {
42
+ // Avoid race conditions
43
+ if ($e17c9db826984f8ab8e5d837bf0b8$var$state === 'restoring') {
44
+ if (document.documentElement.style.webkitUserSelect === 'none') {
45
+ document.documentElement.style.webkitUserSelect = $e17c9db826984f8ab8e5d837bf0b8$var$savedUserSelect || '';
46
+ }
26
47
 
27
- setTimeout(() => {
28
- // Wait for any CSS transitions to complete so we don't recompute style
29
- // for the whole page in the middle of the animation and cause jank.
30
- runAfterTransition(() => {
31
- // Avoid race conditions
32
- if ($e17c9db826984f8ab8e5d837bf0b8$var$state === 'restoring') {
33
- if (document.documentElement.style.webkitUserSelect === 'none') {
34
- document.documentElement.style.webkitUserSelect = $e17c9db826984f8ab8e5d837bf0b8$var$savedUserSelect || '';
48
+ $e17c9db826984f8ab8e5d837bf0b8$var$savedUserSelect = '';
49
+ $e17c9db826984f8ab8e5d837bf0b8$var$state = 'default';
35
50
  }
51
+ });
52
+ }, 300);
53
+ } else {
54
+ // If not iOS, restore the target's original user-select if any
55
+ // Ignore state since it doesn't apply for non iOS
56
+ if (target && $e17c9db826984f8ab8e5d837bf0b8$var$modifiedElementMap.has(target)) {
57
+ let targetOldUserSelect = $e17c9db826984f8ab8e5d837bf0b8$var$modifiedElementMap.get(target);
36
58
 
37
- $e17c9db826984f8ab8e5d837bf0b8$var$savedUserSelect = '';
38
- $e17c9db826984f8ab8e5d837bf0b8$var$state = 'default';
59
+ if (target.style.userSelect === 'none') {
60
+ target.style.userSelect = targetOldUserSelect;
39
61
  }
40
- });
41
- }, 300);
62
+
63
+ if (target.getAttribute('style') === '') {
64
+ target.removeAttribute('style');
65
+ }
66
+
67
+ $e17c9db826984f8ab8e5d837bf0b8$var$modifiedElementMap.delete(target);
68
+ }
69
+ }
42
70
  }
43
71
 
44
72
  /*
@@ -109,9 +137,10 @@ export function usePress(props) {
109
137
  isDisabled,
110
138
  isPressed: isPressedProp,
111
139
  preventFocusOnPress,
112
- shouldCancelOnPointerExit
140
+ shouldCancelOnPointerExit,
141
+ allowTextSelectionOnPress
113
142
  } = _usePressResponderCon,
114
- domProps = _babelRuntimeHelpersEsmObjectWithoutPropertiesLoose(_usePressResponderCon, ["onPress", "onPressChange", "onPressStart", "onPressEnd", "onPressUp", "isDisabled", "isPressed", "preventFocusOnPress", "shouldCancelOnPointerExit", "ref"]);
143
+ domProps = _babelRuntimeHelpersEsmObjectWithoutPropertiesLoose(_usePressResponderCon, ["onPress", "onPressChange", "onPressStart", "onPressEnd", "onPressUp", "isDisabled", "isPressed", "preventFocusOnPress", "shouldCancelOnPointerExit", "allowTextSelectionOnPress", "ref"]);
115
144
 
116
145
  let propsRef = useRef(null);
117
146
  propsRef.current = {
@@ -256,7 +285,10 @@ export function usePress(props) {
256
285
  state.activePointerId = null;
257
286
  state.pointerType = null;
258
287
  removeAllGlobalListeners();
259
- $e17c9db826984f8ab8e5d837bf0b8$export$restoreTextSelection();
288
+
289
+ if (!allowTextSelectionOnPress) {
290
+ $e17c9db826984f8ab8e5d837bf0b8$export$restoreTextSelection(state.target);
291
+ }
260
292
  }
261
293
  };
262
294
 
@@ -299,7 +331,7 @@ export function usePress(props) {
299
331
  // trigger as if it were a keyboard click.
300
332
 
301
333
 
302
- if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && $f67ef9f1b8ed09b4b00fd0840cd8b94b$export$isVirtualClick(e.nativeEvent)) {
334
+ if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && (state.pointerType === 'virtual' || $f67ef9f1b8ed09b4b00fd0840cd8b94b$export$isVirtualClick(e.nativeEvent))) {
303
335
  // Ensure the element receives focus (VoiceOver on iOS does not do this)
304
336
  if (!isDisabled && !preventFocusOnPress) {
305
337
  focusWithoutScrolling(e.currentTarget);
@@ -322,11 +354,12 @@ export function usePress(props) {
322
354
  e.preventDefault();
323
355
  e.stopPropagation();
324
356
  state.isPressed = false;
325
- triggerPressEnd($ffc54430b1dbeee65879852feaaff07d$var$createEvent(state.target, e), 'keyboard', e.target === state.target);
357
+ let target = e.target;
358
+ triggerPressEnd($ffc54430b1dbeee65879852feaaff07d$var$createEvent(state.target, e), 'keyboard', state.target.contains(target));
326
359
  removeAllGlobalListeners(); // If the target is a link, trigger the click method to open the URL,
327
360
  // but defer triggering pressEnd until onClick event handler.
328
361
 
329
- if (e.target === state.target && $ffc54430b1dbeee65879852feaaff07d$var$isHTMLAnchorLink(state.target) || state.target.getAttribute('role') === 'link') {
362
+ if (state.target.contains(target) && $ffc54430b1dbeee65879852feaaff07d$var$isHTMLAnchorLink(state.target) || state.target.getAttribute('role') === 'link') {
330
363
  state.target.click();
331
364
  }
332
365
  }
@@ -337,17 +370,24 @@ export function usePress(props) {
337
370
  // Only handle left clicks, and ignore events that bubbled through portals.
338
371
  if (e.button !== 0 || !e.currentTarget.contains(e.target)) {
339
372
  return;
373
+ } // iOS safari fires pointer events from VoiceOver with incorrect coordinates/target.
374
+ // Ignore and let the onClick handler take care of it instead.
375
+ // https://bugs.webkit.org/show_bug.cgi?id=222627
376
+ // https://bugs.webkit.org/show_bug.cgi?id=223202
377
+
378
+
379
+ if ($ffc54430b1dbeee65879852feaaff07d$var$isVirtualPointerEvent(e.nativeEvent)) {
380
+ state.pointerType = 'virtual';
381
+ return;
340
382
  } // Due to browser inconsistencies, especially on mobile browsers, we prevent
341
383
  // default on pointer down and handle focusing the pressable element ourselves.
342
384
 
343
385
 
344
386
  if ($ffc54430b1dbeee65879852feaaff07d$var$shouldPreventDefault(e.target)) {
345
387
  e.preventDefault();
346
- } // iOS safari fires pointer events from VoiceOver (but only when outside an iframe...)
347
- // https://bugs.webkit.org/show_bug.cgi?id=222627
348
-
388
+ }
349
389
 
350
- state.pointerType = $ffc54430b1dbeee65879852feaaff07d$var$isVirtualPointerEvent(e.nativeEvent) ? 'virtual' : e.pointerType;
390
+ state.pointerType = e.pointerType;
351
391
  e.stopPropagation();
352
392
 
353
393
  if (!state.isPressed) {
@@ -360,7 +400,10 @@ export function usePress(props) {
360
400
  focusWithoutScrolling(e.currentTarget);
361
401
  }
362
402
 
363
- $e17c9db826984f8ab8e5d837bf0b8$export$disableTextSelection();
403
+ if (!allowTextSelectionOnPress) {
404
+ $e17c9db826984f8ab8e5d837bf0b8$export$disableTextSelection(state.target);
405
+ }
406
+
364
407
  triggerPressStart(e, state.pointerType);
365
408
  addGlobalListener(document, 'pointermove', onPointerMove, false);
366
409
  addGlobalListener(document, 'pointerup', onPointerUp, false);
@@ -386,7 +429,8 @@ export function usePress(props) {
386
429
  };
387
430
 
388
431
  pressProps.onPointerUp = e => {
389
- if (!e.currentTarget.contains(e.target)) {
432
+ // iOS fires pointerup with zero width and height, so check the pointerType recorded during pointerdown.
433
+ if (!e.currentTarget.contains(e.target) || state.pointerType === 'virtual') {
390
434
  return;
391
435
  } // Only handle left clicks
392
436
  // Safari on iOS sometimes fires pointerup events, even
@@ -394,7 +438,7 @@ export function usePress(props) {
394
438
 
395
439
 
396
440
  if (e.button === 0 && $ffc54430b1dbeee65879852feaaff07d$var$isOverTarget(e, e.currentTarget)) {
397
- triggerPressUp(e, state.pointerType || ($ffc54430b1dbeee65879852feaaff07d$var$isVirtualPointerEvent(e.nativeEvent) ? 'virtual' : e.pointerType));
441
+ triggerPressUp(e, state.pointerType || e.pointerType);
398
442
  }
399
443
  }; // Safari on iOS < 13.2 does not implement pointerenter/pointerleave events correctly.
400
444
  // Use pointer move events instead to implement our own hit testing.
@@ -434,7 +478,10 @@ export function usePress(props) {
434
478
  state.activePointerId = null;
435
479
  state.pointerType = null;
436
480
  removeAllGlobalListeners();
437
- $e17c9db826984f8ab8e5d837bf0b8$export$restoreTextSelection();
481
+
482
+ if (!allowTextSelectionOnPress) {
483
+ $e17c9db826984f8ab8e5d837bf0b8$export$restoreTextSelection(state.target);
484
+ }
438
485
  }
439
486
  };
440
487
 
@@ -569,7 +616,10 @@ export function usePress(props) {
569
616
  focusWithoutScrolling(e.currentTarget);
570
617
  }
571
618
 
572
- $e17c9db826984f8ab8e5d837bf0b8$export$disableTextSelection();
619
+ if (!allowTextSelectionOnPress) {
620
+ $e17c9db826984f8ab8e5d837bf0b8$export$disableTextSelection(state.target);
621
+ }
622
+
573
623
  triggerPressStart(e, state.pointerType);
574
624
  addGlobalListener(window, 'scroll', onScroll, true);
575
625
  };
@@ -626,7 +676,11 @@ export function usePress(props) {
626
676
  state.activePointerId = null;
627
677
  state.isOverTarget = false;
628
678
  state.ignoreEmulatedMouseEvents = true;
629
- $e17c9db826984f8ab8e5d837bf0b8$export$restoreTextSelection();
679
+
680
+ if (!allowTextSelectionOnPress) {
681
+ $e17c9db826984f8ab8e5d837bf0b8$export$restoreTextSelection(state.target);
682
+ }
683
+
630
684
  removeAllGlobalListeners();
631
685
  };
632
686
 
@@ -664,12 +718,16 @@ export function usePress(props) {
664
718
  }
665
719
 
666
720
  return pressProps;
667
- }, [addGlobalListener, isDisabled, preventFocusOnPress, removeAllGlobalListeners]); // Remove user-select: none in case component unmounts immediately after pressStart
721
+ }, [addGlobalListener, isDisabled, preventFocusOnPress, removeAllGlobalListeners, allowTextSelectionOnPress]); // Remove user-select: none in case component unmounts immediately after pressStart
668
722
  // eslint-disable-next-line arrow-body-style
669
723
 
670
724
  useEffect(() => {
671
- return () => $e17c9db826984f8ab8e5d837bf0b8$export$restoreTextSelection();
672
- }, []);
725
+ return () => {
726
+ if (!allowTextSelectionOnPress) {
727
+ $e17c9db826984f8ab8e5d837bf0b8$export$restoreTextSelection(ref.current.target);
728
+ }
729
+ };
730
+ }, [allowTextSelectionOnPress]);
673
731
  return {
674
732
  isPressed: isPressedProp || isPressed,
675
733
  pressProps: mergeProps(domProps, pressProps)
@@ -683,6 +741,7 @@ function $ffc54430b1dbeee65879852feaaff07d$var$isHTMLAnchorLink(target) {
683
741
  function $ffc54430b1dbeee65879852feaaff07d$var$isValidKeyboardEvent(event) {
684
742
  const {
685
743
  key,
744
+ code,
686
745
  target
687
746
  } = event;
688
747
  const element = target;
@@ -693,7 +752,7 @@ function $ffc54430b1dbeee65879852feaaff07d$var$isValidKeyboardEvent(event) {
693
752
  const role = element.getAttribute('role'); // Accessibility for keyboards. Space and Enter only.
694
753
  // "Spacebar" is for IE 11
695
754
 
696
- return (key === 'Enter' || key === ' ' || key === 'Spacebar') && tagName !== 'INPUT' && tagName !== 'TEXTAREA' && isContentEditable !== true && ( // A link with a valid href should be handled natively,
755
+ return (key === 'Enter' || key === ' ' || key === 'Spacebar' || code === 'Space') && tagName !== 'INPUT' && tagName !== 'TEXTAREA' && isContentEditable !== true && ( // A link with a valid href should be handled natively,
697
756
  // unless it also has role='button' and was triggered using Space.
698
757
  !$ffc54430b1dbeee65879852feaaff07d$var$isHTMLAnchorLink(element) || role === 'button' && key !== 'Enter') && // An element with role='link' should only trigger with Enter key
699
758
  !(role === 'link' && key !== 'Enter');
@@ -773,7 +832,11 @@ function $ffc54430b1dbeee65879852feaaff07d$var$shouldPreventDefault(target) {
773
832
 
774
833
  function $ffc54430b1dbeee65879852feaaff07d$var$isVirtualPointerEvent(event) {
775
834
  // If the pointer size is zero, then we assume it's from a screen reader.
776
- return event.width === 0 && event.height === 0;
835
+ // Android TalkBack double tap will sometimes return a event with width and height of 1
836
+ // and pointerType === 'mouse' so we need to check for a specific combination of event attributes.
837
+ // Cannot use "event.pressure === 0" as the sole check due to Safari pointer events always returning pressure === 0
838
+ // instead of .5, see https://bugs.webkit.org/show_bug.cgi?id=206216
839
+ return event.width === 0 && event.height === 0 || event.width === 1 && event.height === 1 && event.pressure === 0 && event.detail === 0;
777
840
  }
778
841
 
779
842
  export const Pressable = /*#__PURE__*/_react.forwardRef((_ref, ref) => {
@@ -903,7 +966,7 @@ function $d01f69bb2ab5f70dfd0005370a2a2cbc$var$triggerChangeHandlers(modality, e
903
966
 
904
967
  function $d01f69bb2ab5f70dfd0005370a2a2cbc$var$isValidKey(e) {
905
968
  // Control and Shift keys trigger when navigating back to the tab with keyboard.
906
- return !(e.metaKey || !isMac() && e.altKey || e.ctrlKey || e.type === 'keyup' && (e.key === 'Control' || e.key === 'Shift'));
969
+ return !(e.metaKey || !isMac() && e.altKey || e.ctrlKey || e.key === 'Control' || e.key === 'Shift' || e.key === 'Meta');
907
970
  }
908
971
 
909
972
  function $d01f69bb2ab5f70dfd0005370a2a2cbc$var$handleKeyboardEvent(e) {
@@ -1225,7 +1288,7 @@ export function useHover(props) {
1225
1288
  }
1226
1289
 
1227
1290
  state.isHovered = true;
1228
- let target = event.target;
1291
+ let target = event.currentTarget;
1229
1292
  state.target = target;
1230
1293
 
1231
1294
  if (onHoverStart) {
@@ -1252,7 +1315,7 @@ export function useHover(props) {
1252
1315
  }
1253
1316
 
1254
1317
  state.isHovered = false;
1255
- let target = event.target;
1318
+ let target = event.currentTarget;
1256
1319
 
1257
1320
  if (onHoverEnd) {
1258
1321
  onHoverEnd({
@@ -1315,7 +1378,7 @@ export function useHover(props) {
1315
1378
  // Safe to call triggerHoverEnd, it will early return if we aren't currently hovering
1316
1379
  if (isDisabled) {
1317
1380
  triggerHoverEnd({
1318
- target: state.target
1381
+ currentTarget: state.target
1319
1382
  }, state.pointerType);
1320
1383
  }
1321
1384
  }, [isDisabled]);
@@ -1744,4 +1807,91 @@ export function useScrollWheel(props, ref) {
1744
1807
  }, [onScroll]);
1745
1808
  useEvent(ref, 'wheel', isDisabled ? null : onScrollHandler);
1746
1809
  }
1810
+ const $cd8e0096d064b4c36af8a188dfbda75c$var$DEFAULT_THRESHOLD = 500;
1811
+ /**
1812
+ * Handles long press interactions across mouse and touch devices. Supports a customizable time threshold,
1813
+ * accessibility description, and normalizes behavior across browsers and devices.
1814
+ */
1815
+
1816
+ export function useLongPress(props) {
1817
+ let {
1818
+ isDisabled,
1819
+ onLongPressStart,
1820
+ onLongPressEnd,
1821
+ onLongPress,
1822
+ threshold = $cd8e0096d064b4c36af8a188dfbda75c$var$DEFAULT_THRESHOLD,
1823
+ accessibilityDescription
1824
+ } = props;
1825
+ const timeRef = useRef(null);
1826
+ let {
1827
+ addGlobalListener,
1828
+ removeGlobalListener
1829
+ } = useGlobalListeners();
1830
+ let {
1831
+ pressProps
1832
+ } = usePress({
1833
+ isDisabled,
1834
+
1835
+ onPressStart(e) {
1836
+ if (e.pointerType === 'mouse' || e.pointerType === 'touch') {
1837
+ if (onLongPressStart) {
1838
+ onLongPressStart(_babelRuntimeHelpersEsmExtends({}, e, {
1839
+ type: 'longpressstart'
1840
+ }));
1841
+ }
1842
+
1843
+ timeRef.current = setTimeout(() => {
1844
+ // Prevent other usePress handlers from also handling this event.
1845
+ e.target.dispatchEvent(new PointerEvent('pointercancel', {
1846
+ bubbles: true
1847
+ }));
1848
+
1849
+ if (onLongPress) {
1850
+ onLongPress(_babelRuntimeHelpersEsmExtends({}, e, {
1851
+ type: 'longpress'
1852
+ }));
1853
+ }
1854
+
1855
+ timeRef.current = null;
1856
+ }, threshold); // Prevent context menu, which may be opened on long press on touch devices
1857
+
1858
+ if (e.pointerType === 'touch') {
1859
+ let onContextMenu = e => {
1860
+ e.preventDefault();
1861
+ };
1862
+
1863
+ addGlobalListener(e.target, 'contextmenu', onContextMenu, {
1864
+ once: true
1865
+ });
1866
+ addGlobalListener(window, 'pointerup', () => {
1867
+ // If no contextmenu event is fired quickly after pointerup, remove the handler
1868
+ // so future context menu events outside a long press are not prevented.
1869
+ setTimeout(() => {
1870
+ removeGlobalListener(e.target, 'contextmenu', onContextMenu);
1871
+ }, 30);
1872
+ }, {
1873
+ once: true
1874
+ });
1875
+ }
1876
+ }
1877
+ },
1878
+
1879
+ onPressEnd(e) {
1880
+ if (timeRef.current) {
1881
+ clearTimeout(timeRef.current);
1882
+ }
1883
+
1884
+ if (onLongPressEnd && (e.pointerType === 'mouse' || e.pointerType === 'touch')) {
1885
+ onLongPressEnd(_babelRuntimeHelpersEsmExtends({}, e, {
1886
+ type: 'longpressend'
1887
+ }));
1888
+ }
1889
+ }
1890
+
1891
+ });
1892
+ let descriptionProps = useDescription(onLongPress && !isDisabled ? accessibilityDescription : null);
1893
+ return {
1894
+ longPressProps: mergeProps(pressProps, descriptionProps)
1895
+ };
1896
+ }
1747
1897
  //# sourceMappingURL=module.js.map