@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/main.js +206 -52
- package/dist/main.js.map +1 -1
- package/dist/module.js +202 -52
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +38 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/textSelection.ts +59 -28
- package/src/useFocusVisible.ts +1 -1
- package/src/useHover.ts +3 -3
- package/src/useLongPress.ts +128 -0
- package/src/usePress.ts +59 -20
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"A;A;AGwBA,2BAA4B,SAAQ,WAAW;IAC7C,+FAA+F;IAC/F,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4DAA4D;IAC5D,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;A;A;A;A;OAKG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAA;CACpC;AAED,+BAAgC,SAAQ,UAAU;IAChD,mCAAmC;IACnC,GAAG,CAAC,EAAE,UAAU,WAAW,CAAC,CAAA;CAC7B;AAsBD;IACE,+CAA+C;IAC/C,SAAS,EAAE,OAAO,CAAC;IACnB,6CAA6C;IAC7C,UAAU,EAAE,eAAe,WAAW,CAAC,CAAA;CACxC;AAeD;A;A;A;GAIG;AACH,yBAAyB,KAAK,EAAE,cAAc,GAAG,WAAW,
|
|
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.
|
|
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.
|
|
22
|
-
"@react-types/shared": "^3.
|
|
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": "
|
|
30
|
+
"gitHead": "896eabe5521a0349a675c4773ed7629addd4b8c4"
|
|
31
31
|
}
|
package/src/index.ts
CHANGED
package/src/textSelection.ts
CHANGED
|
@@ -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 (
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
60
|
+
state = 'restoring';
|
|
46
61
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
89
|
+
if (target.getAttribute('style') === '') {
|
|
90
|
+
target.removeAttribute('style');
|
|
61
91
|
}
|
|
62
|
-
|
|
63
|
-
|
|
92
|
+
modifiedElementMap.delete(target);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
64
95
|
}
|
package/src/useFocusVisible.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 () =>
|
|
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
|
|
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
|
}
|