@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/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;
@@ -14,6 +14,8 @@ export interface PressProps extends PressEvents {
14
14
  * when the pointer leaves the target and onPressStart will not be fired if the pointer returns.
15
15
  */
16
16
  shouldCancelOnPointerExit?: boolean;
17
+ /** Whether text selection should be enabled on the pressable element. */
18
+ allowTextSelectionOnPress?: boolean;
17
19
  }
18
20
  export interface PressHookProps extends PressProps {
19
21
  /** A ref to the target element. */
@@ -155,5 +157,40 @@ export interface ScrollWheelProps extends ScrollEvents {
155
157
  isDisabled?: boolean;
156
158
  }
157
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;
158
195
 
159
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.6.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.9.0",
22
- "@react-types/shared": "^3.9.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": "eb6c352a42ca230774b923cc0bde692d206e0999"
30
+ "gitHead": "896eabe5521a0349a675c4773ed7629addd4b8c4"
31
31
  }
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
  }
@@ -58,7 +58,7 @@ function triggerChangeHandlers(modality: Modality, e: HandlerEvent) {
58
58
  */
59
59
  function isValidKey(e: KeyboardEvent) {
60
60
  // Control and Shift keys trigger when navigating back to the tab with keyboard.
61
- return !(e.metaKey || (!isMac() && e.altKey) || e.ctrlKey || e.type === 'keyup' && (e.key === 'Control' || e.key === 'Shift'));
61
+ return !(e.metaKey || (!isMac() && e.altKey) || e.ctrlKey || e.key === 'Control' || e.key === 'Shift' || e.key === 'Meta');
62
62
  }
63
63
 
64
64
 
package/src/useHover.ts CHANGED
@@ -109,7 +109,7 @@ export function useHover(props: HoverProps): HoverResult {
109
109
  }
110
110
 
111
111
  state.isHovered = true;
112
- let target = event.target;
112
+ let target = event.currentTarget;
113
113
  state.target = target;
114
114
 
115
115
  if (onHoverStart) {
@@ -136,7 +136,7 @@ export function useHover(props: HoverProps): HoverResult {
136
136
  }
137
137
 
138
138
  state.isHovered = false;
139
- let target = event.target;
139
+ let target = event.currentTarget;
140
140
  if (onHoverEnd) {
141
141
  onHoverEnd({
142
142
  type: 'hoverend',
@@ -194,7 +194,7 @@ export function useHover(props: HoverProps): HoverResult {
194
194
  // Call the triggerHoverEnd as soon as isDisabled changes to true
195
195
  // Safe to call triggerHoverEnd, it will early return if we aren't currently hovering
196
196
  if (isDisabled) {
197
- triggerHoverEnd({target: state.target}, state.pointerType);
197
+ triggerHoverEnd({currentTarget: state.target}, state.pointerType);
198
198
  }
199
199
  }, [isDisabled]);
200
200
 
@@ -0,0 +1,128 @@
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 {HTMLAttributes, useRef} from 'react';
14
+ import {LongPressEvent} from '@react-types/shared';
15
+ import {mergeProps, useDescription, useGlobalListeners} from '@react-aria/utils';
16
+ import {usePress} from './usePress';
17
+
18
+ interface LongPressProps {
19
+ /** Whether long press events should be disabled. */
20
+ isDisabled?: boolean,
21
+ /** Handler that is called when a long press interaction starts. */
22
+ onLongPressStart?: (e: LongPressEvent) => void,
23
+ /**
24
+ * Handler that is called when a long press interaction ends, either
25
+ * over the target or when the pointer leaves the target.
26
+ */
27
+ onLongPressEnd?: (e: LongPressEvent) => void,
28
+ /**
29
+ * Handler that is called when the threshold time is met while
30
+ * the press is over the target.
31
+ */
32
+ onLongPress?: (e: LongPressEvent) => void,
33
+ /**
34
+ * The amount of time in milliseconds to wait before triggering a long press.
35
+ * @default 500ms
36
+ */
37
+ threshold?: number,
38
+ /**
39
+ * A description for assistive techology users indicating that a long press
40
+ * action is available, e.g. "Long press to open menu".
41
+ */
42
+ accessibilityDescription?: string
43
+ }
44
+
45
+ interface LongPressResult {
46
+ /** Props to spread on the target element. */
47
+ longPressProps: HTMLAttributes<HTMLElement>
48
+ }
49
+
50
+ const DEFAULT_THRESHOLD = 500;
51
+
52
+ /**
53
+ * Handles long press interactions across mouse and touch devices. Supports a customizable time threshold,
54
+ * accessibility description, and normalizes behavior across browsers and devices.
55
+ */
56
+ export function useLongPress(props: LongPressProps): LongPressResult {
57
+ let {
58
+ isDisabled,
59
+ onLongPressStart,
60
+ onLongPressEnd,
61
+ onLongPress,
62
+ threshold = DEFAULT_THRESHOLD,
63
+ accessibilityDescription
64
+ } = props;
65
+
66
+ const timeRef = useRef(null);
67
+ let {addGlobalListener, removeGlobalListener} = useGlobalListeners();
68
+
69
+ let {pressProps} = usePress({
70
+ isDisabled,
71
+ onPressStart(e) {
72
+ if (e.pointerType === 'mouse' || e.pointerType === 'touch') {
73
+ if (onLongPressStart) {
74
+ onLongPressStart({
75
+ ...e,
76
+ type: 'longpressstart'
77
+ });
78
+ }
79
+
80
+ timeRef.current = setTimeout(() => {
81
+ // Prevent other usePress handlers from also handling this event.
82
+ e.target.dispatchEvent(new PointerEvent('pointercancel', {bubbles: true}));
83
+ if (onLongPress) {
84
+ onLongPress({
85
+ ...e,
86
+ type: 'longpress'
87
+ });
88
+ }
89
+ timeRef.current = null;
90
+ }, threshold);
91
+
92
+ // Prevent context menu, which may be opened on long press on touch devices
93
+ if (e.pointerType === 'touch') {
94
+ let onContextMenu = e => {
95
+ e.preventDefault();
96
+ };
97
+
98
+ addGlobalListener(e.target, 'contextmenu', onContextMenu, {once: true});
99
+ addGlobalListener(window, 'pointerup', () => {
100
+ // If no contextmenu event is fired quickly after pointerup, remove the handler
101
+ // so future context menu events outside a long press are not prevented.
102
+ setTimeout(() => {
103
+ removeGlobalListener(e.target, 'contextmenu', onContextMenu);
104
+ }, 30);
105
+ }, {once: true});
106
+ }
107
+ }
108
+ },
109
+ onPressEnd(e) {
110
+ if (timeRef.current) {
111
+ clearTimeout(timeRef.current);
112
+ }
113
+
114
+ if (onLongPressEnd && (e.pointerType === 'mouse' || e.pointerType === 'touch')) {
115
+ onLongPressEnd({
116
+ ...e,
117
+ type: 'longpressend'
118
+ });
119
+ }
120
+ }
121
+ });
122
+
123
+ let descriptionProps = useDescription(onLongPress && !isDisabled ? accessibilityDescription : null);
124
+
125
+ return {
126
+ longPressProps: mergeProps(pressProps, descriptionProps)
127
+ };
128
+ }
package/src/usePress.ts CHANGED
@@ -35,7 +35,9 @@ export interface PressProps extends PressEvents {
35
35
  * still pressed, onPressStart will be fired again. If set to `true`, the press is canceled
36
36
  * when the pointer leaves the target and onPressStart will not be fired if the pointer returns.
37
37
  */
38
- shouldCancelOnPointerExit?: boolean
38
+ shouldCancelOnPointerExit?: boolean,
39
+ /** Whether text selection should be enabled on the pressable element. */
40
+ allowTextSelectionOnPress?: boolean
39
41
  }
40
42
 
41
43
  export interface PressHookProps extends PressProps {
@@ -99,6 +101,7 @@ export function usePress(props: PressHookProps): PressResult {
99
101
  isPressed: isPressedProp,
100
102
  preventFocusOnPress,
101
103
  shouldCancelOnPointerExit,
104
+ allowTextSelectionOnPress,
102
105
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
103
106
  ref: _, // Removing `ref` from `domProps` because TypeScript is dumb,
104
107
  ...domProps
@@ -217,7 +220,9 @@ export function usePress(props: PressHookProps): PressResult {
217
220
  state.activePointerId = null;
218
221
  state.pointerType = null;
219
222
  removeAllGlobalListeners();
220
- restoreTextSelection();
223
+ if (!allowTextSelectionOnPress) {
224
+ restoreTextSelection(state.target);
225
+ }
221
226
  }
222
227
  };
223
228
 
@@ -259,7 +264,7 @@ export function usePress(props: PressHookProps): PressResult {
259
264
 
260
265
  // If triggered from a screen reader or by using element.click(),
261
266
  // trigger as if it were a keyboard click.
262
- if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && isVirtualClick(e.nativeEvent)) {
267
+ if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && (state.pointerType === 'virtual' || isVirtualClick(e.nativeEvent))) {
263
268
  // Ensure the element receives focus (VoiceOver on iOS does not do this)
264
269
  if (!isDisabled && !preventFocusOnPress) {
265
270
  focusWithoutScrolling(e.currentTarget);
@@ -282,12 +287,13 @@ export function usePress(props: PressHookProps): PressResult {
282
287
  e.stopPropagation();
283
288
 
284
289
  state.isPressed = false;
285
- triggerPressEnd(createEvent(state.target, e), 'keyboard', e.target === state.target);
290
+ let target = e.target as HTMLElement;
291
+ triggerPressEnd(createEvent(state.target, e), 'keyboard', state.target.contains(target));
286
292
  removeAllGlobalListeners();
287
293
 
288
294
  // If the target is a link, trigger the click method to open the URL,
289
295
  // but defer triggering pressEnd until onClick event handler.
290
- if (e.target === state.target && isHTMLAnchorLink(state.target) || state.target.getAttribute('role') === 'link') {
296
+ if (state.target.contains(target) && isHTMLAnchorLink(state.target) || state.target.getAttribute('role') === 'link') {
291
297
  state.target.click();
292
298
  }
293
299
  }
@@ -300,15 +306,22 @@ export function usePress(props: PressHookProps): PressResult {
300
306
  return;
301
307
  }
302
308
 
309
+ // iOS safari fires pointer events from VoiceOver with incorrect coordinates/target.
310
+ // Ignore and let the onClick handler take care of it instead.
311
+ // https://bugs.webkit.org/show_bug.cgi?id=222627
312
+ // https://bugs.webkit.org/show_bug.cgi?id=223202
313
+ if (isVirtualPointerEvent(e.nativeEvent)) {
314
+ state.pointerType = 'virtual';
315
+ return;
316
+ }
317
+
303
318
  // Due to browser inconsistencies, especially on mobile browsers, we prevent
304
319
  // default on pointer down and handle focusing the pressable element ourselves.
305
320
  if (shouldPreventDefault(e.target as Element)) {
306
321
  e.preventDefault();
307
322
  }
308
323
 
309
- // iOS safari fires pointer events from VoiceOver (but only when outside an iframe...)
310
- // https://bugs.webkit.org/show_bug.cgi?id=222627
311
- state.pointerType = isVirtualPointerEvent(e.nativeEvent) ? 'virtual' : e.pointerType;
324
+ state.pointerType = e.pointerType;
312
325
 
313
326
  e.stopPropagation();
314
327
  if (!state.isPressed) {
@@ -321,7 +334,10 @@ export function usePress(props: PressHookProps): PressResult {
321
334
  focusWithoutScrolling(e.currentTarget);
322
335
  }
323
336
 
324
- disableTextSelection();
337
+ if (!allowTextSelectionOnPress) {
338
+ disableTextSelection(state.target);
339
+ }
340
+
325
341
  triggerPressStart(e, state.pointerType);
326
342
 
327
343
  addGlobalListener(document, 'pointermove', onPointerMove, false);
@@ -348,7 +364,8 @@ export function usePress(props: PressHookProps): PressResult {
348
364
  };
349
365
 
350
366
  pressProps.onPointerUp = (e) => {
351
- if (!e.currentTarget.contains(e.target as HTMLElement)) {
367
+ // iOS fires pointerup with zero width and height, so check the pointerType recorded during pointerdown.
368
+ if (!e.currentTarget.contains(e.target as HTMLElement) || state.pointerType === 'virtual') {
352
369
  return;
353
370
  }
354
371
 
@@ -356,7 +373,7 @@ export function usePress(props: PressHookProps): PressResult {
356
373
  // Safari on iOS sometimes fires pointerup events, even
357
374
  // when the touch isn't over the target, so double check.
358
375
  if (e.button === 0 && isOverTarget(e, e.currentTarget)) {
359
- triggerPressUp(e, state.pointerType || (isVirtualPointerEvent(e.nativeEvent) ? 'virtual' : e.pointerType));
376
+ triggerPressUp(e, state.pointerType || e.pointerType);
360
377
  }
361
378
  };
362
379
 
@@ -395,7 +412,9 @@ export function usePress(props: PressHookProps): PressResult {
395
412
  state.activePointerId = null;
396
413
  state.pointerType = null;
397
414
  removeAllGlobalListeners();
398
- restoreTextSelection();
415
+ if (!allowTextSelectionOnPress) {
416
+ restoreTextSelection(state.target);
417
+ }
399
418
  }
400
419
  };
401
420
 
@@ -526,7 +545,10 @@ export function usePress(props: PressHookProps): PressResult {
526
545
  focusWithoutScrolling(e.currentTarget);
527
546
  }
528
547
 
529
- disableTextSelection();
548
+ if (!allowTextSelectionOnPress) {
549
+ disableTextSelection(state.target);
550
+ }
551
+
530
552
  triggerPressStart(e, state.pointerType);
531
553
 
532
554
  addGlobalListener(window, 'scroll', onScroll, true);
@@ -579,7 +601,9 @@ export function usePress(props: PressHookProps): PressResult {
579
601
  state.activePointerId = null;
580
602
  state.isOverTarget = false;
581
603
  state.ignoreEmulatedMouseEvents = true;
582
- restoreTextSelection();
604
+ if (!allowTextSelectionOnPress) {
605
+ restoreTextSelection(state.target);
606
+ }
583
607
  removeAllGlobalListeners();
584
608
  };
585
609
 
@@ -616,13 +640,17 @@ export function usePress(props: PressHookProps): PressResult {
616
640
  }
617
641
 
618
642
  return pressProps;
619
- }, [addGlobalListener, isDisabled, preventFocusOnPress, removeAllGlobalListeners]);
643
+ }, [addGlobalListener, isDisabled, preventFocusOnPress, removeAllGlobalListeners, allowTextSelectionOnPress]);
620
644
 
621
645
  // Remove user-select: none in case component unmounts immediately after pressStart
622
646
  // eslint-disable-next-line arrow-body-style
623
647
  useEffect(() => {
624
- return () => restoreTextSelection();
625
- }, []);
648
+ return () => {
649
+ if (!allowTextSelectionOnPress) {
650
+ restoreTextSelection(ref.current.target);
651
+ }
652
+ };
653
+ }, [allowTextSelectionOnPress]);
626
654
 
627
655
  return {
628
656
  isPressed: isPressedProp || isPressed,
@@ -635,14 +663,14 @@ function isHTMLAnchorLink(target: HTMLElement): boolean {
635
663
  }
636
664
 
637
665
  function isValidKeyboardEvent(event: KeyboardEvent): boolean {
638
- const {key, target} = event;
666
+ const {key, code, target} = event;
639
667
  const element = target as HTMLElement;
640
668
  const {tagName, isContentEditable} = element;
641
669
  const role = element.getAttribute('role');
642
670
  // Accessibility for keyboards. Space and Enter only.
643
671
  // "Spacebar" is for IE 11
644
672
  return (
645
- (key === 'Enter' || key === ' ' || key === 'Spacebar') &&
673
+ (key === 'Enter' || key === ' ' || key === 'Spacebar' || code === 'Space') &&
646
674
  (tagName !== 'INPUT' &&
647
675
  tagName !== 'TEXTAREA' &&
648
676
  isContentEditable !== true) &&
@@ -739,5 +767,16 @@ function shouldPreventDefault(target: Element) {
739
767
 
740
768
  function isVirtualPointerEvent(event: PointerEvent) {
741
769
  // If the pointer size is zero, then we assume it's from a screen reader.
742
- return event.width === 0 && event.height === 0;
770
+ // Android TalkBack double tap will sometimes return a event with width and height of 1
771
+ // and pointerType === 'mouse' so we need to check for a specific combination of event attributes.
772
+ // Cannot use "event.pressure === 0" as the sole check due to Safari pointer events always returning pressure === 0
773
+ // instead of .5, see https://bugs.webkit.org/show_bug.cgi?id=206216
774
+ return (
775
+ (event.width === 0 && event.height === 0) ||
776
+ (event.width === 1 &&
777
+ event.height === 1 &&
778
+ event.pressure === 0 &&
779
+ event.detail === 0
780
+ )
781
+ );
743
782
  }