@react-aria/interactions 3.25.6 → 3.27.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.
Files changed (63) hide show
  1. package/dist/PressResponder.main.js +2 -3
  2. package/dist/PressResponder.main.js.map +1 -1
  3. package/dist/PressResponder.mjs +3 -4
  4. package/dist/PressResponder.module.js +3 -4
  5. package/dist/PressResponder.module.js.map +1 -1
  6. package/dist/focusSafely.main.js +4 -4
  7. package/dist/focusSafely.main.js.map +1 -1
  8. package/dist/focusSafely.mjs +4 -4
  9. package/dist/focusSafely.module.js +4 -4
  10. package/dist/focusSafely.module.js.map +1 -1
  11. package/dist/import.mjs +2 -2
  12. package/dist/main.js +1 -0
  13. package/dist/main.js.map +1 -1
  14. package/dist/module.js +2 -2
  15. package/dist/module.js.map +1 -1
  16. package/dist/types.d.ts +4 -1
  17. package/dist/types.d.ts.map +1 -1
  18. package/dist/useFocusVisible.main.js +19 -8
  19. package/dist/useFocusVisible.main.js.map +1 -1
  20. package/dist/useFocusVisible.mjs +20 -10
  21. package/dist/useFocusVisible.module.js +20 -10
  22. package/dist/useFocusVisible.module.js.map +1 -1
  23. package/dist/useFocusWithin.main.js +3 -3
  24. package/dist/useFocusWithin.main.js.map +1 -1
  25. package/dist/useFocusWithin.mjs +4 -4
  26. package/dist/useFocusWithin.module.js +4 -4
  27. package/dist/useFocusWithin.module.js.map +1 -1
  28. package/dist/useHover.main.js +3 -3
  29. package/dist/useHover.main.js.map +1 -1
  30. package/dist/useHover.mjs +4 -4
  31. package/dist/useHover.module.js +4 -4
  32. package/dist/useHover.module.js.map +1 -1
  33. package/dist/useInteractOutside.main.js +2 -4
  34. package/dist/useInteractOutside.main.js.map +1 -1
  35. package/dist/useInteractOutside.mjs +3 -5
  36. package/dist/useInteractOutside.module.js +3 -5
  37. package/dist/useInteractOutside.module.js.map +1 -1
  38. package/dist/useMove.main.js +110 -74
  39. package/dist/useMove.main.js.map +1 -1
  40. package/dist/useMove.mjs +112 -76
  41. package/dist/useMove.module.js +112 -76
  42. package/dist/useMove.module.js.map +1 -1
  43. package/dist/usePress.main.js +197 -117
  44. package/dist/usePress.main.js.map +1 -1
  45. package/dist/usePress.mjs +199 -119
  46. package/dist/usePress.module.js +199 -119
  47. package/dist/usePress.module.js.map +1 -1
  48. package/dist/utils.main.js +2 -5
  49. package/dist/utils.main.js.map +1 -1
  50. package/dist/utils.mjs +3 -6
  51. package/dist/utils.module.js +3 -6
  52. package/dist/utils.module.js.map +1 -1
  53. package/package.json +4 -4
  54. package/src/PressResponder.tsx +3 -4
  55. package/src/focusSafely.ts +4 -4
  56. package/src/index.ts +1 -0
  57. package/src/useFocusVisible.ts +21 -5
  58. package/src/useFocusWithin.ts +3 -3
  59. package/src/useHover.ts +3 -3
  60. package/src/useInteractOutside.ts +3 -3
  61. package/src/useMove.ts +85 -57
  62. package/src/usePress.ts +199 -151
  63. package/src/utils.ts +3 -7
@@ -15,8 +15,9 @@
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 {getOwnerDocument, getOwnerWindow, isMac, isVirtualClick} from '@react-aria/utils';
18
+ import {getOwnerDocument, getOwnerWindow, isMac, isVirtualClick, openLink} from '@react-aria/utils';
19
19
  import {ignoreFocusEvent} from './utils';
20
+ import {PointerType} from '@react-types/shared';
20
21
  import {useEffect, useState} from 'react';
21
22
  import {useIsSSR} from '@react-aria/ssr';
22
23
 
@@ -37,7 +38,8 @@ export interface FocusVisibleResult {
37
38
  }
38
39
 
39
40
  let currentModality: null | Modality = null;
40
- let changeHandlers = new Set<Handler>();
41
+ let currentPointerType: PointerType = 'keyboard';
42
+ export const changeHandlers = new Set<Handler>();
41
43
  interface GlobalListenerData {
42
44
  focus: () => void
43
45
  }
@@ -68,14 +70,16 @@ function isValidKey(e: KeyboardEvent) {
68
70
 
69
71
  function handleKeyboardEvent(e: KeyboardEvent) {
70
72
  hasEventBeforeFocus = true;
71
- if (isValidKey(e)) {
73
+ if (!(openLink as any).isOpening && isValidKey(e)) {
72
74
  currentModality = 'keyboard';
75
+ currentPointerType = 'keyboard';
73
76
  triggerChangeHandlers('keyboard', e);
74
77
  }
75
78
  }
76
79
 
77
80
  function handlePointerEvent(e: PointerEvent | MouseEvent) {
78
81
  currentModality = 'pointer';
82
+ currentPointerType = 'pointerType' in e ? e.pointerType as PointerType : 'mouse';
79
83
  if (e.type === 'mousedown' || e.type === 'pointerdown') {
80
84
  hasEventBeforeFocus = true;
81
85
  triggerChangeHandlers('pointer', e);
@@ -83,9 +87,10 @@ function handlePointerEvent(e: PointerEvent | MouseEvent) {
83
87
  }
84
88
 
85
89
  function handleClickEvent(e: MouseEvent) {
86
- if (isVirtualClick(e)) {
90
+ if (!(openLink as any).isOpening && isVirtualClick(e)) {
87
91
  hasEventBeforeFocus = true;
88
92
  currentModality = 'virtual';
93
+ currentPointerType = 'virtual';
89
94
  }
90
95
  }
91
96
 
@@ -101,6 +106,7 @@ function handleFocusEvent(e: FocusEvent) {
101
106
  // This occurs, for example, when navigating a form with the next/previous buttons on iOS.
102
107
  if (!hasEventBeforeFocus && !hasBlurredWindowRecently) {
103
108
  currentModality = 'virtual';
109
+ currentPointerType = 'virtual';
104
110
  triggerChangeHandlers('virtual', e);
105
111
  }
106
112
 
@@ -249,9 +255,15 @@ export function getInteractionModality(): Modality | null {
249
255
 
250
256
  export function setInteractionModality(modality: Modality): void {
251
257
  currentModality = modality;
258
+ currentPointerType = modality === 'pointer' ? 'mouse' : modality;
252
259
  triggerChangeHandlers(modality, null);
253
260
  }
254
261
 
262
+ /** @private */
263
+ export function getPointerType(): PointerType {
264
+ return currentPointerType;
265
+ }
266
+
255
267
  /**
256
268
  * Keeps state of the current modality.
257
269
  */
@@ -321,10 +333,13 @@ export function useFocusVisible(props: FocusVisibleProps = {}): FocusVisibleResu
321
333
  /**
322
334
  * Listens for trigger change and reports if focus is visible (i.e., modality is not pointer).
323
335
  */
324
- export function useFocusVisibleListener(fn: FocusVisibleHandler, deps: ReadonlyArray<any>, opts?: {isTextInput?: boolean}): void {
336
+ export function useFocusVisibleListener(fn: FocusVisibleHandler, deps: ReadonlyArray<any>, opts?: {enabled?: boolean, isTextInput?: boolean}): void {
325
337
  setupGlobalFocusEvents();
326
338
 
327
339
  useEffect(() => {
340
+ if (opts?.enabled === false) {
341
+ return;
342
+ }
328
343
  let handler = (modality: Modality, e: HandlerEvent) => {
329
344
  // We want to early return for any keyboard events that occur inside text inputs EXCEPT for Tab and Escape
330
345
  if (!isKeyboardFocusEvent(!!(opts?.isTextInput), modality, e)) {
@@ -339,3 +354,4 @@ export function useFocusVisibleListener(fn: FocusVisibleHandler, deps: ReadonlyA
339
354
  // eslint-disable-next-line react-hooks/exhaustive-deps
340
355
  }, deps);
341
356
  }
357
+
@@ -54,14 +54,14 @@ export function useFocusWithin(props: FocusWithinProps): FocusWithinResult {
54
54
 
55
55
  let onBlur = useCallback((e: FocusEvent) => {
56
56
  // Ignore events bubbling through portals.
57
- if (!e.currentTarget.contains(e.target)) {
57
+ if (!nodeContains(e.currentTarget, e.target)) {
58
58
  return;
59
59
  }
60
60
 
61
61
  // We don't want to trigger onBlurWithin and then immediately onFocusWithin again
62
62
  // when moving focus inside the element. Only trigger if the currentTarget doesn't
63
63
  // include the relatedTarget (where focus is moving).
64
- if (state.current.isFocusWithin && !(e.currentTarget as Element).contains(e.relatedTarget as Element)) {
64
+ if (state.current.isFocusWithin && !nodeContains(e.currentTarget as Element, e.relatedTarget as Element)) {
65
65
  state.current.isFocusWithin = false;
66
66
  removeAllGlobalListeners();
67
67
 
@@ -78,7 +78,7 @@ export function useFocusWithin(props: FocusWithinProps): FocusWithinResult {
78
78
  let onSyntheticFocus = useSyntheticBlurEvent(onBlur);
79
79
  let onFocus = useCallback((e: FocusEvent) => {
80
80
  // Ignore events bubbling through portals.
81
- if (!e.currentTarget.contains(e.target)) {
81
+ if (!nodeContains(e.currentTarget, e.target)) {
82
82
  return;
83
83
  }
84
84
 
package/src/useHover.ts CHANGED
@@ -108,7 +108,7 @@ export function useHover(props: HoverProps): HoverResult {
108
108
  let {hoverProps, triggerHoverEnd} = useMemo(() => {
109
109
  let triggerHoverStart = (event, pointerType) => {
110
110
  state.pointerType = pointerType;
111
- if (isDisabled || pointerType === 'touch' || state.isHovered || !event.currentTarget.contains(event.target)) {
111
+ if (isDisabled || pointerType === 'touch' || state.isHovered || !nodeContains(event.currentTarget, event.target)) {
112
112
  return;
113
113
  }
114
114
 
@@ -180,7 +180,7 @@ export function useHover(props: HoverProps): HoverResult {
180
180
  };
181
181
 
182
182
  hoverProps.onPointerLeave = (e) => {
183
- if (!isDisabled && e.currentTarget.contains(e.target as Element)) {
183
+ if (!isDisabled && nodeContains(e.currentTarget, e.target as Element)) {
184
184
  triggerHoverEnd(e, e.pointerType);
185
185
  }
186
186
  };
@@ -198,7 +198,7 @@ export function useHover(props: HoverProps): HoverResult {
198
198
  };
199
199
 
200
200
  hoverProps.onMouseLeave = (e) => {
201
- if (!isDisabled && e.currentTarget.contains(e.target as Element)) {
201
+ if (!isDisabled && nodeContains(e.currentTarget, e.target as Element)) {
202
202
  triggerHoverEnd(e, 'mouse');
203
203
  }
204
204
  };
@@ -15,7 +15,7 @@
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 {getOwnerDocument, useEffectEvent} from '@react-aria/utils';
18
+ import {getOwnerDocument, nodeContains, useEffectEvent} from '@react-aria/utils';
19
19
  import {RefObject} from '@react-types/shared';
20
20
  import {useEffect, useRef} from 'react';
21
21
 
@@ -111,7 +111,7 @@ export function useInteractOutside(props: InteractOutsideProps): void {
111
111
  documentObject.removeEventListener('touchend', onTouchEnd, true);
112
112
  };
113
113
  }
114
- }, [ref, isDisabled, onPointerDown, triggerInteractOutside]);
114
+ }, [ref, isDisabled]);
115
115
  }
116
116
 
117
117
  function isValidEvent(event, ref) {
@@ -121,7 +121,7 @@ function isValidEvent(event, ref) {
121
121
  if (event.target) {
122
122
  // if the event target is no longer in the document, ignore
123
123
  const ownerDocument = event.target.ownerDocument;
124
- if (!ownerDocument || !ownerDocument.documentElement.contains(event.target)) {
124
+ if (!ownerDocument || !nodeContains(ownerDocument.documentElement, event.target)) {
125
125
  return false;
126
126
  }
127
127
  // If the target is within a top layer element (e.g. toasts), ignore.
package/src/useMove.ts CHANGED
@@ -12,8 +12,8 @@
12
12
 
13
13
  import {disableTextSelection, restoreTextSelection} from './textSelection';
14
14
  import {DOMAttributes, MoveEvents, PointerType} from '@react-types/shared';
15
- import React, {useMemo, useRef} from 'react';
16
- import {useEffectEvent, useGlobalListeners} from '@react-aria/utils';
15
+ import React, {useCallback, useMemo, useRef, useState} from 'react';
16
+ import {useEffectEvent, useGlobalListeners, useLayoutEffect} from '@react-aria/utils';
17
17
 
18
18
  export interface MoveResult {
19
19
  /** Props to spread on the target element. */
@@ -43,7 +43,7 @@ export function useMove(props: MoveEvents): MoveResult {
43
43
 
44
44
  let {addGlobalListener, removeGlobalListener} = useGlobalListeners();
45
45
 
46
- let move = useEffectEvent((originalEvent: EventBase, pointerType: PointerType, deltaX: number, deltaY: number) => {
46
+ let move = useCallback((originalEvent: EventBase, pointerType: PointerType, deltaX: number, deltaY: number) => {
47
47
  if (deltaX === 0 && deltaY === 0) {
48
48
  return;
49
49
  }
@@ -69,9 +69,10 @@ export function useMove(props: MoveEvents): MoveResult {
69
69
  ctrlKey: originalEvent.ctrlKey,
70
70
  altKey: originalEvent.altKey
71
71
  });
72
- });
72
+ }, [onMoveStart, onMove, state]);
73
+ let moveEvent = useEffectEvent(move);
73
74
 
74
- let end = useEffectEvent((originalEvent: EventBase, pointerType: PointerType) => {
75
+ let end = useCallback((originalEvent: EventBase, pointerType: PointerType) => {
75
76
  restoreTextSelection();
76
77
  if (state.current.didMove) {
77
78
  onMoveEnd?.({
@@ -83,57 +84,111 @@ export function useMove(props: MoveEvents): MoveResult {
83
84
  altKey: originalEvent.altKey
84
85
  });
85
86
  }
86
- });
87
+ }, [onMoveEnd, state]);
88
+ let endEvent = useEffectEvent(end);
87
89
 
88
- let moveProps = useMemo(() => {
89
- let moveProps: DOMAttributes = {};
90
+ let [pointerDown, setPointerDown] = useState<'pointer' | 'mouse' | 'touch' | null>(null);
91
+ useLayoutEffect(() => {
92
+ if (pointerDown === 'pointer') {
93
+ let onPointerMove = (e: PointerEvent) => {
94
+ if (e.pointerId === state.current.id) {
95
+ let pointerType = (e.pointerType || 'mouse') as PointerType;
90
96
 
91
- let start = () => {
92
- disableTextSelection();
93
- state.current.didMove = false;
94
- };
97
+ // Problems with PointerEvent#movementX/movementY:
98
+ // 1. it is always 0 on macOS Safari.
99
+ // 2. On Chrome Android, it's scaled by devicePixelRatio, but not on Chrome macOS
100
+ moveEvent(e, pointerType, e.pageX - (state.current.lastPosition?.pageX ?? 0), e.pageY - (state.current.lastPosition?.pageY ?? 0));
101
+ state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
102
+ }
103
+ };
95
104
 
96
- if (typeof PointerEvent === 'undefined' && process.env.NODE_ENV === 'test') {
105
+ let onPointerUp = (e: PointerEvent) => {
106
+ if (e.pointerId === state.current.id) {
107
+ let pointerType = (e.pointerType || 'mouse') as PointerType;
108
+ endEvent(e, pointerType);
109
+ state.current.id = null;
110
+ removeGlobalListener(window, 'pointermove', onPointerMove, false);
111
+ removeGlobalListener(window, 'pointerup', onPointerUp, false);
112
+ removeGlobalListener(window, 'pointercancel', onPointerUp, false);
113
+ setPointerDown(null);
114
+ }
115
+ };
116
+ addGlobalListener(window, 'pointermove', onPointerMove, false);
117
+ addGlobalListener(window, 'pointerup', onPointerUp, false);
118
+ addGlobalListener(window, 'pointercancel', onPointerUp, false);
119
+ return () => {
120
+ removeGlobalListener(window, 'pointermove', onPointerMove, false);
121
+ removeGlobalListener(window, 'pointerup', onPointerUp, false);
122
+ removeGlobalListener(window, 'pointercancel', onPointerUp, false);
123
+ };
124
+ } else if (pointerDown === 'mouse' && process.env.NODE_ENV === 'test') {
97
125
  let onMouseMove = (e: MouseEvent) => {
98
126
  if (e.button === 0) {
99
- move(e, 'mouse', e.pageX - (state.current.lastPosition?.pageX ?? 0), e.pageY - (state.current.lastPosition?.pageY ?? 0));
127
+ moveEvent(e, 'mouse', e.pageX - (state.current.lastPosition?.pageX ?? 0), e.pageY - (state.current.lastPosition?.pageY ?? 0));
100
128
  state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
101
129
  }
102
130
  };
103
131
  let onMouseUp = (e: MouseEvent) => {
104
132
  if (e.button === 0) {
105
- end(e, 'mouse');
133
+ endEvent(e, 'mouse');
106
134
  removeGlobalListener(window, 'mousemove', onMouseMove, false);
107
135
  removeGlobalListener(window, 'mouseup', onMouseUp, false);
136
+ setPointerDown(null);
108
137
  }
109
138
  };
110
- moveProps.onMouseDown = (e: React.MouseEvent) => {
111
- if (e.button === 0) {
112
- start();
113
- e.stopPropagation();
114
- e.preventDefault();
115
- state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
116
- addGlobalListener(window, 'mousemove', onMouseMove, false);
117
- addGlobalListener(window, 'mouseup', onMouseUp, false);
118
- }
139
+ addGlobalListener(window, 'mousemove', onMouseMove, false);
140
+ addGlobalListener(window, 'mouseup', onMouseUp, false);
141
+ return () => {
142
+ removeGlobalListener(window, 'mousemove', onMouseMove, false);
143
+ removeGlobalListener(window, 'mouseup', onMouseUp, false);
119
144
  };
120
-
145
+ } else if (pointerDown === 'touch' && process.env.NODE_ENV === 'test') {
121
146
  let onTouchMove = (e: TouchEvent) => {
122
147
  let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
123
148
  if (touch >= 0) {
124
149
  let {pageX, pageY} = e.changedTouches[touch];
125
- move(e, 'touch', pageX - (state.current.lastPosition?.pageX ?? 0), pageY - (state.current.lastPosition?.pageY ?? 0));
150
+ moveEvent(e, 'touch', pageX - (state.current.lastPosition?.pageX ?? 0), pageY - (state.current.lastPosition?.pageY ?? 0));
126
151
  state.current.lastPosition = {pageX, pageY};
127
152
  }
128
153
  };
129
154
  let onTouchEnd = (e: TouchEvent) => {
130
155
  let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
131
156
  if (touch >= 0) {
132
- end(e, 'touch');
157
+ endEvent(e, 'touch');
133
158
  state.current.id = null;
134
159
  removeGlobalListener(window, 'touchmove', onTouchMove);
135
160
  removeGlobalListener(window, 'touchend', onTouchEnd);
136
161
  removeGlobalListener(window, 'touchcancel', onTouchEnd);
162
+ setPointerDown(null);
163
+ }
164
+ };
165
+ addGlobalListener(window, 'touchmove', onTouchMove, false);
166
+ addGlobalListener(window, 'touchend', onTouchEnd, false);
167
+ addGlobalListener(window, 'touchcancel', onTouchEnd, false);
168
+ return () => {
169
+ removeGlobalListener(window, 'touchmove', onTouchMove, false);
170
+ removeGlobalListener(window, 'touchend', onTouchEnd, false);
171
+ removeGlobalListener(window, 'touchcancel', onTouchEnd, false);
172
+ };
173
+ }
174
+ }, [pointerDown, addGlobalListener, removeGlobalListener]);
175
+
176
+ let moveProps = useMemo(() => {
177
+ let moveProps: DOMAttributes = {};
178
+
179
+ let start = () => {
180
+ disableTextSelection();
181
+ state.current.didMove = false;
182
+ };
183
+
184
+ if (typeof PointerEvent === 'undefined' && process.env.NODE_ENV === 'test') {
185
+ moveProps.onMouseDown = (e: React.MouseEvent) => {
186
+ if (e.button === 0) {
187
+ start();
188
+ e.stopPropagation();
189
+ e.preventDefault();
190
+ state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
191
+ setPointerDown('mouse');
137
192
  }
138
193
  };
139
194
  moveProps.onTouchStart = (e: React.TouchEvent) => {
@@ -147,34 +202,9 @@ export function useMove(props: MoveEvents): MoveResult {
147
202
  e.preventDefault();
148
203
  state.current.lastPosition = {pageX, pageY};
149
204
  state.current.id = identifier;
150
- addGlobalListener(window, 'touchmove', onTouchMove, false);
151
- addGlobalListener(window, 'touchend', onTouchEnd, false);
152
- addGlobalListener(window, 'touchcancel', onTouchEnd, false);
205
+ setPointerDown('touch');
153
206
  };
154
207
  } else {
155
- let onPointerMove = (e: PointerEvent) => {
156
- if (e.pointerId === state.current.id) {
157
- let pointerType = (e.pointerType || 'mouse') as PointerType;
158
-
159
- // Problems with PointerEvent#movementX/movementY:
160
- // 1. it is always 0 on macOS Safari.
161
- // 2. On Chrome Android, it's scaled by devicePixelRatio, but not on Chrome macOS
162
- move(e, pointerType, e.pageX - (state.current.lastPosition?.pageX ?? 0), e.pageY - (state.current.lastPosition?.pageY ?? 0));
163
- state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
164
- }
165
- };
166
-
167
- let onPointerUp = (e: PointerEvent) => {
168
- if (e.pointerId === state.current.id) {
169
- let pointerType = (e.pointerType || 'mouse') as PointerType;
170
- end(e, pointerType);
171
- state.current.id = null;
172
- removeGlobalListener(window, 'pointermove', onPointerMove, false);
173
- removeGlobalListener(window, 'pointerup', onPointerUp, false);
174
- removeGlobalListener(window, 'pointercancel', onPointerUp, false);
175
- }
176
- };
177
-
178
208
  moveProps.onPointerDown = (e: React.PointerEvent) => {
179
209
  if (e.button === 0 && state.current.id == null) {
180
210
  start();
@@ -182,9 +212,7 @@ export function useMove(props: MoveEvents): MoveResult {
182
212
  e.preventDefault();
183
213
  state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
184
214
  state.current.id = e.pointerId;
185
- addGlobalListener(window, 'pointermove', onPointerMove, false);
186
- addGlobalListener(window, 'pointerup', onPointerUp, false);
187
- addGlobalListener(window, 'pointercancel', onPointerUp, false);
215
+ setPointerDown('pointer');
188
216
  }
189
217
  };
190
218
  }
@@ -225,7 +253,7 @@ export function useMove(props: MoveEvents): MoveResult {
225
253
  };
226
254
 
227
255
  return moveProps;
228
- }, [state, addGlobalListener, removeGlobalListener, move, end]);
256
+ }, [state, move, end]);
229
257
 
230
258
  return {moveProps};
231
259
  }