@react-aria/interactions 3.4.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/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, { HTMLAttributes, RefObject, ReactElement, ReactNode, FocusEvent, SyntheticEvent } from "react";
2
- import { PressEvents, FocusEvents, HoverEvents, KeyboardEvents, MoveEvents, ScrollEvents } from "@react-types/shared";
2
+ import { PressEvents, FocusEvents, HoverEvents, KeyboardEvents, MoveEvents, ScrollEvents, LongPressEvent } from "@react-types/shared";
3
3
  export interface PressProps extends PressEvents {
4
4
  /** Whether the target is in a controlled press state (e.g. an overlay it triggers is open). */
5
5
  isPressed?: boolean;
@@ -7,6 +7,15 @@ export interface PressProps extends PressEvents {
7
7
  isDisabled?: boolean;
8
8
  /** Whether the target should not receive focus on press. */
9
9
  preventFocusOnPress?: boolean;
10
+ /**
11
+ * Whether press events should be canceled when the pointer leaves the target while pressed.
12
+ * By default, this is `false`, which means if the pointer returns back over the target while
13
+ * still pressed, onPressStart will be fired again. If set to `true`, the press is canceled
14
+ * when the pointer leaves the target and onPressStart will not be fired if the pointer returns.
15
+ */
16
+ shouldCancelOnPointerExit?: boolean;
17
+ /** Whether text selection should be enabled on the pressable element. */
18
+ allowTextSelectionOnPress?: boolean;
10
19
  }
11
20
  export interface PressHookProps extends PressProps {
12
21
  /** A ref to the target element. */
@@ -46,6 +55,7 @@ interface FocusResult {
46
55
  */
47
56
  export function useFocus(props: FocusProps): FocusResult;
48
57
  type Modality = 'keyboard' | 'pointer' | 'virtual';
58
+ type FocusVisibleHandler = (isFocusVisible: boolean) => void;
49
59
  interface FocusVisibleProps {
50
60
  /** Whether the element is a text input. */
51
61
  isTextInput?: boolean;
@@ -70,6 +80,12 @@ export function useInteractionModality(): Modality;
70
80
  * Manages focus visible state for the page, and subscribes individual components for updates.
71
81
  */
72
82
  export function useFocusVisible(props?: FocusVisibleProps): FocusVisibleResult;
83
+ /**
84
+ * Listens for trigger change and reports if focus is visible (i.e., modality is not pointer).
85
+ */
86
+ export function useFocusVisibleListener(fn: FocusVisibleHandler, deps: ReadonlyArray<any>, opts?: {
87
+ isTextInput?: boolean;
88
+ }): void;
73
89
  interface FocusWithinProps {
74
90
  /** Whether the focus within events should be disabled. */
75
91
  isDisabled?: boolean;
@@ -105,6 +121,7 @@ export function useHover(props: HoverProps): HoverResult;
105
121
  interface InteractOutsideProps {
106
122
  ref: RefObject<Element>;
107
123
  onInteractOutside?: (e: SyntheticEvent) => void;
124
+ onInteractOutsideStart?: (e: SyntheticEvent) => void;
108
125
  /** Whether the interact outside events should be disabled. */
109
126
  isDisabled?: boolean;
110
127
  }
@@ -140,5 +157,40 @@ export interface ScrollWheelProps extends ScrollEvents {
140
157
  isDisabled?: boolean;
141
158
  }
142
159
  export function useScrollWheel(props: ScrollWheelProps, ref: RefObject<HTMLElement>): void;
160
+ interface LongPressProps {
161
+ /** Whether long press events should be disabled. */
162
+ isDisabled?: boolean;
163
+ /** Handler that is called when a long press interaction starts. */
164
+ onLongPressStart?: (e: LongPressEvent) => void;
165
+ /**
166
+ * Handler that is called when a long press interaction ends, either
167
+ * over the target or when the pointer leaves the target.
168
+ */
169
+ onLongPressEnd?: (e: LongPressEvent) => void;
170
+ /**
171
+ * Handler that is called when the threshold time is met while
172
+ * the press is over the target.
173
+ */
174
+ onLongPress?: (e: LongPressEvent) => void;
175
+ /**
176
+ * The amount of time in milliseconds to wait before triggering a long press.
177
+ * @default 500ms
178
+ */
179
+ threshold?: number;
180
+ /**
181
+ * A description for assistive techology users indicating that a long press
182
+ * action is available, e.g. "Long press to open menu".
183
+ */
184
+ accessibilityDescription?: string;
185
+ }
186
+ interface LongPressResult {
187
+ /** Props to spread on the target element. */
188
+ longPressProps: HTMLAttributes<HTMLElement>;
189
+ }
190
+ /**
191
+ * Handles long press interactions across mouse and touch devices. Supports a customizable time threshold,
192
+ * accessibility description, and normalizes behavior across browsers and devices.
193
+ */
194
+ export function useLongPress(props: LongPressProps): LongPressResult;
143
195
 
144
196
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"mappings":"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","sources":["./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/textSelection.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/utils.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/context.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/usePress.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/Pressable.tsx","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/PressResponder.tsx","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useFocus.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useFocusVisible.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useFocusWithin.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useHover.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useInteractOutside.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/createEventHandler.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useKeyboard.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useMove.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useScrollWheel.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/index.ts"],"sourcesContent":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"names":[],"version":3,"file":"types.d.ts.map"}
1
+ {"mappings":"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","sources":["./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/textSelection.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/utils.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/context.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/usePress.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/Pressable.tsx","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/PressResponder.tsx","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useFocus.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useFocusVisible.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useFocusWithin.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useHover.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useInteractOutside.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/createEventHandler.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useKeyboard.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useMove.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useScrollWheel.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/useLongPress.ts","./packages/@react-aria/interactions/src/packages/@react-aria/interactions/src/index.ts"],"sourcesContent":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"names":[],"version":3,"file":"types.d.ts.map"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-aria/interactions",
3
- "version": "3.4.0",
3
+ "version": "3.7.0",
4
4
  "description": "Spectrum UI components in React",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/main.js",
@@ -18,8 +18,8 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@babel/runtime": "^7.6.2",
21
- "@react-aria/utils": "^3.8.0",
22
- "@react-types/shared": "^3.6.0"
21
+ "@react-aria/utils": "^3.10.0",
22
+ "@react-types/shared": "^3.10.0"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "react": "^16.8.0 || ^17.0.0-rc.1"
@@ -27,5 +27,5 @@
27
27
  "publishConfig": {
28
28
  "access": "public"
29
29
  },
30
- "gitHead": "3aae08e7d8a75382bedcddac7c86107e40db9296"
30
+ "gitHead": "896eabe5521a0349a675c4773ed7629addd4b8c4"
31
31
  }
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {mergeProps} from '@react-aria/utils';
13
+ import {mergeProps, useSyncRef} from '@react-aria/utils';
14
14
  import {PressProps} from './usePress';
15
15
  import {PressResponderContext} from './context';
16
16
  import React, {ReactNode, RefObject, useContext, useEffect, useRef} from 'react';
@@ -24,7 +24,7 @@ export const PressResponder = React.forwardRef(({children, ...props}: PressRespo
24
24
  let prevContext = useContext(PressResponderContext);
25
25
  let context = mergeProps(prevContext || {}, {
26
26
  ...props,
27
- ref,
27
+ ref: ref || prevContext?.ref,
28
28
  register() {
29
29
  isRegistered.current = true;
30
30
  if (prevContext) {
@@ -33,6 +33,8 @@ export const PressResponder = React.forwardRef(({children, ...props}: PressRespo
33
33
  }
34
34
  });
35
35
 
36
+ useSyncRef(prevContext, ref);
37
+
36
38
  useEffect(() => {
37
39
  if (!isRegistered.current) {
38
40
  console.warn(
package/src/index.ts CHANGED
@@ -21,3 +21,4 @@ export * from './useKeyboard';
21
21
  export * from './useMove';
22
22
  export * from './usePress';
23
23
  export * from './useScrollWheel';
24
+ export * from './useLongPress';
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {runAfterTransition} from '@react-aria/utils';
13
+ import {isIOS, runAfterTransition} from '@react-aria/utils';
14
14
 
15
15
  // Safari on iOS starts selecting text on long press. The only way to avoid this, it seems,
16
16
  // is to add user-select: none to the entire page. Adding it to the pressable element prevents
@@ -21,44 +21,75 @@ import {runAfterTransition} from '@react-aria/utils';
21
21
  // There are three possible states due to the delay before removing user-select: none after
22
22
  // pointer up. The 'default' state always transitions to the 'disabled' state, which transitions
23
23
  // to 'restoring'. The 'restoring' state can either transition back to 'disabled' or 'default'.
24
+
25
+ // For non-iOS devices, we apply user-select: none to the pressed element instead to avoid possible
26
+ // performance issues that arise from applying and removing user-select: none to the entire page
27
+ // (see https://github.com/adobe/react-spectrum/issues/1609).
24
28
  type State = 'default' | 'disabled' | 'restoring';
25
29
 
30
+ // Note that state only matters here for iOS. Non-iOS gets user-select: none applied to the target element
31
+ // rather than at the document level so we just need to apply/remove user-select: none for each pressed element individually
26
32
  let state: State = 'default';
27
33
  let savedUserSelect = '';
34
+ let modifiedElementMap = new WeakMap<HTMLElement, string>();
28
35
 
29
- export function disableTextSelection() {
30
- if (state === 'default') {
31
- savedUserSelect = document.documentElement.style.webkitUserSelect;
32
- document.documentElement.style.webkitUserSelect = 'none';
33
- }
36
+ export function disableTextSelection(target?: HTMLElement) {
37
+ if (isIOS()) {
38
+ if (state === 'default') {
39
+ savedUserSelect = document.documentElement.style.webkitUserSelect;
40
+ document.documentElement.style.webkitUserSelect = 'none';
41
+ }
34
42
 
35
- state = 'disabled';
43
+ state = 'disabled';
44
+ } else if (target) {
45
+ // If not iOS, store the target's original user-select and change to user-select: none
46
+ // Ignore state since it doesn't apply for non iOS
47
+ modifiedElementMap.set(target, target.style.userSelect);
48
+ target.style.userSelect = 'none';
49
+ }
36
50
  }
37
51
 
38
- export function restoreTextSelection() {
39
- // If the state is already default, there's nothing to do.
40
- // If it is restoring, then there's no need to queue a second restore.
41
- if (state !== 'disabled') {
42
- return;
43
- }
52
+ export function restoreTextSelection(target?: HTMLElement) {
53
+ if (isIOS()) {
54
+ // If the state is already default, there's nothing to do.
55
+ // If it is restoring, then there's no need to queue a second restore.
56
+ if (state !== 'disabled') {
57
+ return;
58
+ }
44
59
 
45
- state = 'restoring';
60
+ state = 'restoring';
46
61
 
47
- // There appears to be a delay on iOS where selection still might occur
48
- // after pointer up, so wait a bit before removing user-select.
49
- setTimeout(() => {
50
- // Wait for any CSS transitions to complete so we don't recompute style
51
- // for the whole page in the middle of the animation and cause jank.
52
- runAfterTransition(() => {
53
- // Avoid race conditions
54
- if (state === 'restoring') {
55
- if (document.documentElement.style.webkitUserSelect === 'none') {
56
- document.documentElement.style.webkitUserSelect = savedUserSelect || '';
62
+ // There appears to be a delay on iOS where selection still might occur
63
+ // after pointer up, so wait a bit before removing user-select.
64
+ setTimeout(() => {
65
+ // Wait for any CSS transitions to complete so we don't recompute style
66
+ // for the whole page in the middle of the animation and cause jank.
67
+ runAfterTransition(() => {
68
+ // Avoid race conditions
69
+ if (state === 'restoring') {
70
+ if (document.documentElement.style.webkitUserSelect === 'none') {
71
+ document.documentElement.style.webkitUserSelect = savedUserSelect || '';
72
+ }
73
+
74
+ savedUserSelect = '';
75
+ state = 'default';
57
76
  }
77
+ });
78
+ }, 300);
79
+ } else {
80
+ // If not iOS, restore the target's original user-select if any
81
+ // Ignore state since it doesn't apply for non iOS
82
+ if (target && modifiedElementMap.has(target)) {
83
+ let targetOldUserSelect = modifiedElementMap.get(target);
84
+
85
+ if (target.style.userSelect === 'none') {
86
+ target.style.userSelect = targetOldUserSelect;
87
+ }
58
88
 
59
- savedUserSelect = '';
60
- state = 'default';
89
+ if (target.getAttribute('style') === '') {
90
+ target.removeAttribute('style');
61
91
  }
62
- });
63
- }, 300);
92
+ modifiedElementMap.delete(target);
93
+ }
94
+ }
64
95
  }
@@ -22,6 +22,7 @@ import {useEffect, useState} from 'react';
22
22
  type Modality = 'keyboard' | 'pointer' | 'virtual';
23
23
  type HandlerEvent = PointerEvent | MouseEvent | KeyboardEvent | FocusEvent;
24
24
  type Handler = (modality: Modality, e: HandlerEvent) => void;
25
+ type FocusVisibleHandler = (isFocusVisible: boolean) => void;
25
26
  interface FocusVisibleProps {
26
27
  /** Whether the element is a text input. */
27
28
  isTextInput?: boolean,
@@ -38,6 +39,7 @@ let currentModality = null;
38
39
  let changeHandlers = new Set<Handler>();
39
40
  let hasSetupGlobalListeners = false;
40
41
  let hasEventBeforeFocus = false;
42
+ let hasBlurredWindowRecently = false;
41
43
 
42
44
  // Only Tab or Esc keys will make focus visible on text input elements
43
45
  const FOCUS_VISIBLE_INPUT_KEYS = {
@@ -55,9 +57,11 @@ function triggerChangeHandlers(modality: Modality, e: HandlerEvent) {
55
57
  * Helper function to determine if a KeyboardEvent is unmodified and could make keyboard focus styles visible.
56
58
  */
57
59
  function isValidKey(e: KeyboardEvent) {
58
- return !(e.metaKey || (!isMac() && e.altKey) || e.ctrlKey);
60
+ // Control and Shift keys trigger when navigating back to the tab with keyboard.
61
+ return !(e.metaKey || (!isMac() && e.altKey) || e.ctrlKey || e.key === 'Control' || e.key === 'Shift' || e.key === 'Meta');
59
62
  }
60
63
 
64
+
61
65
  function handleKeyboardEvent(e: KeyboardEvent) {
62
66
  hasEventBeforeFocus = true;
63
67
  if (isValidKey(e)) {
@@ -91,18 +95,20 @@ function handleFocusEvent(e: FocusEvent) {
91
95
 
92
96
  // If a focus event occurs without a preceding keyboard or pointer event, switch to virtual modality.
93
97
  // This occurs, for example, when navigating a form with the next/previous buttons on iOS.
94
- if (!hasEventBeforeFocus) {
98
+ if (!hasEventBeforeFocus && !hasBlurredWindowRecently) {
95
99
  currentModality = 'virtual';
96
100
  triggerChangeHandlers('virtual', e);
97
101
  }
98
102
 
99
103
  hasEventBeforeFocus = false;
104
+ hasBlurredWindowRecently = false;
100
105
  }
101
106
 
102
107
  function handleWindowBlur() {
103
108
  // When the window is blurred, reset state. This is necessary when tabbing out of the window,
104
109
  // for example, since a subsequent focus event won't be fired.
105
110
  hasEventBeforeFocus = false;
111
+ hasBlurredWindowRecently = true;
106
112
  }
107
113
 
108
114
  /**
@@ -190,30 +196,41 @@ export function useInteractionModality(): Modality {
190
196
  return modality;
191
197
  }
192
198
 
199
+ /**
200
+ * If this is attached to text input component, return if the event is a focus event (Tab/Escape keys pressed) so that
201
+ * focus visible style can be properly set.
202
+ */
203
+ function isKeyboardFocusEvent(isTextInput: boolean, modality: Modality, e: HandlerEvent) {
204
+ return !(isTextInput && modality === 'keyboard' && e instanceof KeyboardEvent && !FOCUS_VISIBLE_INPUT_KEYS[e.key]);
205
+ }
206
+
193
207
  /**
194
208
  * Manages focus visible state for the page, and subscribes individual components for updates.
195
209
  */
196
210
  export function useFocusVisible(props: FocusVisibleProps = {}): FocusVisibleResult {
197
- setupGlobalFocusEvents();
198
-
199
211
  let {isTextInput, autoFocus} = props;
200
212
  let [isFocusVisibleState, setFocusVisible] = useState(autoFocus || isFocusVisible());
213
+ useFocusVisibleListener((isFocusVisible) => {
214
+ setFocusVisible(isFocusVisible);
215
+ }, [isTextInput], {isTextInput});
216
+
217
+ return {isFocusVisible: isFocusVisibleState};
218
+ }
219
+
220
+ /**
221
+ * Listens for trigger change and reports if focus is visible (i.e., modality is not pointer).
222
+ */
223
+ export function useFocusVisibleListener(fn: FocusVisibleHandler, deps: ReadonlyArray<any>, opts?: {isTextInput?: boolean}): void {
224
+ setupGlobalFocusEvents();
225
+
201
226
  useEffect(() => {
202
227
  let handler = (modality: Modality, e: HandlerEvent) => {
203
- // If this is a text input component, don't update the focus visible style when
204
- // typing except for when the Tab and Escape keys are pressed.
205
- if (isTextInput && modality === 'keyboard' && e instanceof KeyboardEvent && !FOCUS_VISIBLE_INPUT_KEYS[e.key]) {
228
+ if (!isKeyboardFocusEvent(opts?.isTextInput, modality, e)) {
206
229
  return;
207
230
  }
208
-
209
- setFocusVisible(isFocusVisible());
231
+ fn(isFocusVisible());
210
232
  };
211
-
212
233
  changeHandlers.add(handler);
213
- return () => {
214
- changeHandlers.delete(handler);
215
- };
216
- }, [isTextInput]);
217
-
218
- return {isFocusVisible: isFocusVisibleState};
234
+ return () => changeHandlers.delete(handler);
235
+ }, deps);
219
236
  }
package/src/useHover.ts CHANGED
@@ -94,19 +94,23 @@ export function useHover(props: HoverProps): HoverResult {
94
94
  let [isHovered, setHovered] = useState(false);
95
95
  let state = useRef({
96
96
  isHovered: false,
97
- ignoreEmulatedMouseEvents: false
97
+ ignoreEmulatedMouseEvents: false,
98
+ pointerType: '',
99
+ target: null
98
100
  }).current;
99
101
 
100
102
  useEffect(setupGlobalTouchEvents, []);
101
103
 
102
- let hoverProps = useMemo(() => {
104
+ let {hoverProps, triggerHoverEnd} = useMemo(() => {
103
105
  let triggerHoverStart = (event, pointerType) => {
104
- if (isDisabled || pointerType === 'touch' || state.isHovered) {
106
+ state.pointerType = pointerType;
107
+ if (isDisabled || pointerType === 'touch' || state.isHovered || !event.currentTarget.contains(event.target)) {
105
108
  return;
106
109
  }
107
110
 
108
111
  state.isHovered = true;
109
- let target = event.target;
112
+ let target = event.currentTarget;
113
+ state.target = target;
110
114
 
111
115
  if (onHoverStart) {
112
116
  onHoverStart({
@@ -124,13 +128,15 @@ export function useHover(props: HoverProps): HoverResult {
124
128
  };
125
129
 
126
130
  let triggerHoverEnd = (event, pointerType) => {
131
+ state.pointerType = '';
132
+ state.target = null;
133
+
127
134
  if (pointerType === 'touch' || !state.isHovered) {
128
135
  return;
129
136
  }
130
137
 
131
138
  state.isHovered = false;
132
- let target = event.target;
133
-
139
+ let target = event.currentTarget;
134
140
  if (onHoverEnd) {
135
141
  onHoverEnd({
136
142
  type: 'hoverend',
@@ -158,7 +164,9 @@ export function useHover(props: HoverProps): HoverResult {
158
164
  };
159
165
 
160
166
  hoverProps.onPointerLeave = (e) => {
161
- triggerHoverEnd(e, e.pointerType);
167
+ if (!isDisabled && e.currentTarget.contains(e.target as HTMLElement)) {
168
+ triggerHoverEnd(e, e.pointerType);
169
+ }
162
170
  };
163
171
  } else {
164
172
  hoverProps.onTouchStart = () => {
@@ -174,14 +182,25 @@ export function useHover(props: HoverProps): HoverResult {
174
182
  };
175
183
 
176
184
  hoverProps.onMouseLeave = (e) => {
177
- triggerHoverEnd(e, 'mouse');
185
+ if (!isDisabled && e.currentTarget.contains(e.target as HTMLElement)) {
186
+ triggerHoverEnd(e, 'mouse');
187
+ }
178
188
  };
179
189
  }
180
- return hoverProps;
190
+ return {hoverProps, triggerHoverEnd};
181
191
  }, [onHoverStart, onHoverChange, onHoverEnd, isDisabled, state]);
182
192
 
193
+ useEffect(() => {
194
+ // Call the triggerHoverEnd as soon as isDisabled changes to true
195
+ // Safe to call triggerHoverEnd, it will early return if we aren't currently hovering
196
+ if (isDisabled) {
197
+ triggerHoverEnd({currentTarget: state.target}, state.pointerType);
198
+ }
199
+ }, [isDisabled]);
200
+
183
201
  return {
184
202
  hoverProps,
185
203
  isHovered
186
204
  };
187
205
  }
206
+
@@ -20,6 +20,7 @@ import {RefObject, SyntheticEvent, useEffect, useRef} from 'react';
20
20
  interface InteractOutsideProps {
21
21
  ref: RefObject<Element>,
22
22
  onInteractOutside?: (e: SyntheticEvent) => void,
23
+ onInteractOutsideStart?: (e: SyntheticEvent) => void,
23
24
  /** Whether the interact outside events should be disabled. */
24
25
  isDisabled?: boolean
25
26
  }
@@ -29,31 +30,37 @@ interface InteractOutsideProps {
29
30
  * when a user clicks outside them.
30
31
  */
31
32
  export function useInteractOutside(props: InteractOutsideProps) {
32
- let {ref, onInteractOutside, isDisabled} = props;
33
+ let {ref, onInteractOutside, isDisabled, onInteractOutsideStart} = props;
33
34
  let stateRef = useRef({
34
35
  isPointerDown: false,
35
- ignoreEmulatedMouseEvents: false
36
+ ignoreEmulatedMouseEvents: false,
37
+ onInteractOutside,
38
+ onInteractOutsideStart
36
39
  });
37
40
  let state = stateRef.current;
41
+ state.onInteractOutside = onInteractOutside;
42
+ state.onInteractOutsideStart = onInteractOutsideStart;
38
43
 
39
44
  useEffect(() => {
45
+ if (isDisabled) {
46
+ return;
47
+ }
48
+
40
49
  let onPointerDown = (e) => {
41
- if (isDisabled) {
42
- return;
43
- }
44
- if (isValidEvent(e, ref)) {
50
+ if (isValidEvent(e, ref) && state.onInteractOutside) {
51
+ if (state.onInteractOutsideStart) {
52
+ state.onInteractOutsideStart(e);
53
+ }
45
54
  state.isPointerDown = true;
46
55
  }
47
56
  };
48
- /*
49
- // FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=1675846 prevents us from using this pointerevent
50
- // once it's fixed we can uncomment
57
+
51
58
  // Use pointer events if available. Otherwise, fall back to mouse and touch events.
52
59
  if (typeof PointerEvent !== 'undefined') {
53
60
  let onPointerUp = (e) => {
54
- if (state.isPointerDown && onInteractOutside && isValidEvent(e, ref)) {
61
+ if (state.isPointerDown && state.onInteractOutside && isValidEvent(e, ref)) {
55
62
  state.isPointerDown = false;
56
- onInteractOutside(e);
63
+ state.onInteractOutside(e);
57
64
  }
58
65
  };
59
66
 
@@ -65,42 +72,37 @@ export function useInteractOutside(props: InteractOutsideProps) {
65
72
  document.removeEventListener('pointerdown', onPointerDown, true);
66
73
  document.removeEventListener('pointerup', onPointerUp, true);
67
74
  };
68
- } else {*/
69
- let onMouseUp = (e) => {
70
- if (isDisabled) {
71
- return;
72
- }
73
- if (state.ignoreEmulatedMouseEvents) {
74
- state.ignoreEmulatedMouseEvents = false;
75
- } else if (state.isPointerDown && onInteractOutside && isValidEvent(e, ref)) {
76
- state.isPointerDown = false;
77
- onInteractOutside(e);
78
- }
79
- };
75
+ } else {
76
+ let onMouseUp = (e) => {
77
+ if (state.ignoreEmulatedMouseEvents) {
78
+ state.ignoreEmulatedMouseEvents = false;
79
+ } else if (state.isPointerDown && state.onInteractOutside && isValidEvent(e, ref)) {
80
+ state.isPointerDown = false;
81
+ state.onInteractOutside(e);
82
+ }
83
+ };
80
84
 
81
- let onTouchEnd = (e) => {
82
- if (isDisabled) {
83
- return;
84
- }
85
- state.ignoreEmulatedMouseEvents = true;
86
- if (onInteractOutside && state.isPointerDown && isValidEvent(e, ref)) {
87
- state.isPointerDown = false;
88
- onInteractOutside(e);
89
- }
90
- };
85
+ let onTouchEnd = (e) => {
86
+ state.ignoreEmulatedMouseEvents = true;
87
+ if (state.onInteractOutside && state.isPointerDown && isValidEvent(e, ref)) {
88
+ state.isPointerDown = false;
89
+ state.onInteractOutside(e);
90
+ }
91
+ };
91
92
 
92
- document.addEventListener('mousedown', onPointerDown, true);
93
- document.addEventListener('mouseup', onMouseUp, true);
94
- document.addEventListener('touchstart', onPointerDown, true);
95
- document.addEventListener('touchend', onTouchEnd, true);
93
+ document.addEventListener('mousedown', onPointerDown, true);
94
+ document.addEventListener('mouseup', onMouseUp, true);
95
+ document.addEventListener('touchstart', onPointerDown, true);
96
+ document.addEventListener('touchend', onTouchEnd, true);
96
97
 
97
- return () => {
98
- document.removeEventListener('mousedown', onPointerDown, true);
99
- document.removeEventListener('mouseup', onMouseUp, true);
100
- document.removeEventListener('touchstart', onPointerDown, true);
101
- document.removeEventListener('touchend', onTouchEnd, true);
102
- };
103
- }, [onInteractOutside, ref, state.ignoreEmulatedMouseEvents, state.isPointerDown, isDisabled]);
98
+ return () => {
99
+ document.removeEventListener('mousedown', onPointerDown, true);
100
+ document.removeEventListener('mouseup', onMouseUp, true);
101
+ document.removeEventListener('touchstart', onPointerDown, true);
102
+ document.removeEventListener('touchend', onTouchEnd, true);
103
+ };
104
+ }
105
+ }, [ref, state, isDisabled]);
104
106
  }
105
107
 
106
108
  function isValidEvent(event, ref) {