@react-aria/interactions 3.5.1 → 3.8.1
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 +1252 -1661
- package/dist/main.js.map +1 -1
- package/dist/module.js +1239 -1612
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +45 -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 +4 -2
- package/src/useHover.ts +3 -3
- package/src/useLongPress.ts +128 -0
- package/src/useMove.ts +39 -24
- package/src/usePress.ts +102 -31
- package/src/useScrollWheel.ts +3 -12
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. */
|
|
@@ -148,5 +157,40 @@ export interface ScrollWheelProps extends ScrollEvents {
|
|
|
148
157
|
isDisabled?: boolean;
|
|
149
158
|
}
|
|
150
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;
|
|
151
195
|
|
|
152
196
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"
|
|
1
|
+
{"mappings":";;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;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,yEAAyE;IACzE,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;;;;GAIG;AACH,yBAAyB,KAAK,EAAE,cAAc,GAAG,WAAW,CA0jB3D;ACtoBD,wBAAyB,SAAQ,UAAU;IACzC,QAAQ,EAAE,aAAa,eAAe,WAAW,CAAC,EAAE,MAAM,CAAC,CAAA;CAC5D;AAED,OAAO,MAAM,6FAUX,CAAC;ACbH,6BAA8B,SAAQ,UAAU;IAC9C,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,OAAO,MAAM,uGA8BX,CAAC;AC/BH,oBAAqB,SAAQ,WAAW;IACtC,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED;IACE,+CAA+C;IAC/C,UAAU,EAAE,eAAe,WAAW,CAAC,CAAA;CACxC;AAED;;;GAGG;AACH,yBAAyB,KAAK,EAAE,UAAU,GAAG,WAAW,CAwCvD;ACrDD,gBAAgB,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAGnD,2BAA2B,CAAC,gBAAgB,OAAO,KAAK,IAAI,CAAC;AAC7D;IACE,2CAA2C;IAC3C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gDAAgD;IAChD,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED;IACE,kDAAkD;IAClD,gBAAgB,OAAO,CAAA;CACxB;AA8HD;;GAEG;AACH,kCAAkC,OAAO,CAExC;AAED,0CAA0C,QAAQ,CAEjD;AAED,uCAAuC,QAAQ,EAAE,QAAQ,QAGxD;AAED;;GAEG;AACH,0CAA0C,QAAQ,CAgBjD;AAUD;;GAEG;AACH,gCAAgC,KAAK,GAAE,iBAAsB,GAAG,kBAAkB,CAQjF;AAED;;GAEG;AACH,wCAAwC,EAAE,EAAE,mBAAmB,EAAE,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,EAAE;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAC,GAAG,IAAI,CAe/H;AC1ND;IACE,0DAA0D;IAC1D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qFAAqF;IACrF,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACxC,qFAAqF;IACrF,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACvC,sEAAsE;IACtE,mBAAmB,CAAC,EAAE,CAAC,aAAa,EAAE,OAAO,KAAK,IAAI,CAAA;CACvD;AAED;IACE,+CAA+C;IAC/C,gBAAgB,EAAE,eAAe,WAAW,CAAC,CAAA;CAC9C;AAED;;GAEG;AACH,+BAA+B,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CA8CzE;AChED,2BAA4B,SAAQ,WAAW;IAC7C,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED;IACE,6CAA6C;IAC7C,UAAU,EAAE,eAAe,WAAW,CAAC,CAAC;IACxC,SAAS,EAAE,OAAO,CAAA;CACnB;AAoDD;;;GAGG;AACH,yBAAyB,KAAK,EAAE,UAAU,GAAG,WAAW,CAuHvD;ACzLD;IACE,GAAG,EAAE,UAAU,OAAO,CAAC,CAAC;IACxB,iBAAiB,CAAC,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IAChD,sBAAsB,CAAC,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IACrD,8DAA8D;IAC9D,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED;;;GAGG;AACH,mCAAmC,KAAK,EAAE,oBAAoB,QA0E7D;AEzFD,8BAA+B,SAAQ,cAAc;IACnD,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED;IACE,+CAA+C;IAC/C,aAAa,EAAE,eAAe,WAAW,CAAC,CAAA;CAC3C;AAED;;GAEG;AACH,4BAA4B,KAAK,EAAE,aAAa,GAAG,cAAc,CAOhE;ACnBD;IACE,6CAA6C;IAC7C,SAAS,EAAE,eAAe,WAAW,CAAC,CAAA;CACvC;AASD;;;;GAIG;AACH,wBAAwB,KAAK,EAAE,UAAU,GAAG,UAAU,CAkMrD;ACpND,iCAAkC,SAAQ,YAAY;IACpD,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAGD,+BAA+B,KAAK,EAAE,gBAAgB,EAAE,GAAG,EAAE,UAAU,WAAW,CAAC,GAAG,IAAI,CAkBzF;ACvBD;IACE,oDAAoD;IACpD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,mEAAmE;IACnE,gBAAgB,CAAC,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IAC/C;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IAC1C;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAA;CAClC;AAED;IACE,6CAA6C;IAC7C,cAAc,EAAE,eAAe,WAAW,CAAC,CAAA;CAC5C;AAID;;;GAGG;AACH,6BAA6B,KAAK,EAAE,cAAc,GAAG,eAAe,CAwEnE","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","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,"/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nexport * from './Pressable';\nexport * from './PressResponder';\nexport * from './useFocus';\nexport * from './useFocusVisible';\nexport * from './useFocusWithin';\nexport * from './useHover';\nexport * from './useInteractOutside';\nexport * from './useKeyboard';\nexport * from './useMove';\nexport * from './usePress';\nexport * from './useScrollWheel';\nexport * from './useLongPress';\n"],"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.8.1",
|
|
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.11.2",
|
|
22
|
+
"@react-types/shared": "^3.11.1"
|
|
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": "404d41859b7d6f56201d7fc01bd9f22ae3512937"
|
|
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
|
|
|
@@ -231,6 +231,8 @@ export function useFocusVisibleListener(fn: FocusVisibleHandler, deps: ReadonlyA
|
|
|
231
231
|
fn(isFocusVisible());
|
|
232
232
|
};
|
|
233
233
|
changeHandlers.add(handler);
|
|
234
|
-
return () =>
|
|
234
|
+
return () => {
|
|
235
|
+
changeHandlers.delete(handler);
|
|
236
|
+
};
|
|
235
237
|
}, deps);
|
|
236
238
|
}
|
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/useMove.ts
CHANGED
|
@@ -20,6 +20,13 @@ interface MoveResult {
|
|
|
20
20
|
moveProps: HTMLAttributes<HTMLElement>
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
interface EventBase {
|
|
24
|
+
shiftKey: boolean,
|
|
25
|
+
ctrlKey: boolean,
|
|
26
|
+
metaKey: boolean,
|
|
27
|
+
altKey: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
/**
|
|
24
31
|
* Handles move interactions across mouse, touch, and keyboard, including dragging with
|
|
25
32
|
* the mouse or touch, and using the arrow keys. Normalizes behavior across browsers and
|
|
@@ -43,7 +50,7 @@ export function useMove(props: MoveEvents): MoveResult {
|
|
|
43
50
|
disableTextSelection();
|
|
44
51
|
state.current.didMove = false;
|
|
45
52
|
};
|
|
46
|
-
let move = (pointerType: PointerType, deltaX: number, deltaY: number) => {
|
|
53
|
+
let move = (originalEvent: EventBase, pointerType: PointerType, deltaX: number, deltaY: number) => {
|
|
47
54
|
if (deltaX === 0 && deltaY === 0) {
|
|
48
55
|
return;
|
|
49
56
|
}
|
|
@@ -52,22 +59,34 @@ export function useMove(props: MoveEvents): MoveResult {
|
|
|
52
59
|
state.current.didMove = true;
|
|
53
60
|
onMoveStart?.({
|
|
54
61
|
type: 'movestart',
|
|
55
|
-
pointerType
|
|
62
|
+
pointerType,
|
|
63
|
+
shiftKey: originalEvent.shiftKey,
|
|
64
|
+
metaKey: originalEvent.metaKey,
|
|
65
|
+
ctrlKey: originalEvent.ctrlKey,
|
|
66
|
+
altKey: originalEvent.altKey
|
|
56
67
|
});
|
|
57
68
|
}
|
|
58
69
|
onMove({
|
|
59
70
|
type: 'move',
|
|
60
71
|
pointerType,
|
|
61
72
|
deltaX: deltaX,
|
|
62
|
-
deltaY: deltaY
|
|
73
|
+
deltaY: deltaY,
|
|
74
|
+
shiftKey: originalEvent.shiftKey,
|
|
75
|
+
metaKey: originalEvent.metaKey,
|
|
76
|
+
ctrlKey: originalEvent.ctrlKey,
|
|
77
|
+
altKey: originalEvent.altKey
|
|
63
78
|
});
|
|
64
79
|
};
|
|
65
|
-
let end = (pointerType: PointerType) => {
|
|
80
|
+
let end = (originalEvent: EventBase, pointerType: PointerType) => {
|
|
66
81
|
restoreTextSelection();
|
|
67
82
|
if (state.current.didMove) {
|
|
68
83
|
onMoveEnd?.({
|
|
69
84
|
type: 'moveend',
|
|
70
|
-
pointerType
|
|
85
|
+
pointerType,
|
|
86
|
+
shiftKey: originalEvent.shiftKey,
|
|
87
|
+
metaKey: originalEvent.metaKey,
|
|
88
|
+
ctrlKey: originalEvent.ctrlKey,
|
|
89
|
+
altKey: originalEvent.altKey
|
|
71
90
|
});
|
|
72
91
|
}
|
|
73
92
|
};
|
|
@@ -75,13 +94,13 @@ export function useMove(props: MoveEvents): MoveResult {
|
|
|
75
94
|
if (typeof PointerEvent === 'undefined') {
|
|
76
95
|
let onMouseMove = (e: MouseEvent) => {
|
|
77
96
|
if (e.button === 0) {
|
|
78
|
-
move('mouse', e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY);
|
|
97
|
+
move(e, 'mouse', e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY);
|
|
79
98
|
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
|
|
80
99
|
}
|
|
81
100
|
};
|
|
82
101
|
let onMouseUp = (e: MouseEvent) => {
|
|
83
102
|
if (e.button === 0) {
|
|
84
|
-
end('mouse');
|
|
103
|
+
end(e, 'mouse');
|
|
85
104
|
removeGlobalListener(window, 'mousemove', onMouseMove, false);
|
|
86
105
|
removeGlobalListener(window, 'mouseup', onMouseUp, false);
|
|
87
106
|
}
|
|
@@ -98,19 +117,17 @@ export function useMove(props: MoveEvents): MoveResult {
|
|
|
98
117
|
};
|
|
99
118
|
|
|
100
119
|
let onTouchMove = (e: TouchEvent) => {
|
|
101
|
-
// @ts-ignore
|
|
102
120
|
let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
|
|
103
121
|
if (touch >= 0) {
|
|
104
122
|
let {pageX, pageY} = e.changedTouches[touch];
|
|
105
|
-
move('touch', pageX - state.current.lastPosition.pageX, pageY - state.current.lastPosition.pageY);
|
|
123
|
+
move(e, 'touch', pageX - state.current.lastPosition.pageX, pageY - state.current.lastPosition.pageY);
|
|
106
124
|
state.current.lastPosition = {pageX, pageY};
|
|
107
125
|
}
|
|
108
126
|
};
|
|
109
127
|
let onTouchEnd = (e: TouchEvent) => {
|
|
110
|
-
// @ts-ignore
|
|
111
128
|
let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
|
|
112
129
|
if (touch >= 0) {
|
|
113
|
-
end('touch');
|
|
130
|
+
end(e, 'touch');
|
|
114
131
|
state.current.id = null;
|
|
115
132
|
removeGlobalListener(window, 'touchmove', onTouchMove);
|
|
116
133
|
removeGlobalListener(window, 'touchend', onTouchEnd);
|
|
@@ -135,22 +152,20 @@ export function useMove(props: MoveEvents): MoveResult {
|
|
|
135
152
|
} else {
|
|
136
153
|
let onPointerMove = (e: PointerEvent) => {
|
|
137
154
|
if (e.pointerId === state.current.id) {
|
|
138
|
-
|
|
139
|
-
let pointerType: PointerType = e.pointerType || 'mouse';
|
|
155
|
+
let pointerType = (e.pointerType || 'mouse') as PointerType;
|
|
140
156
|
|
|
141
157
|
// Problems with PointerEvent#movementX/movementY:
|
|
142
158
|
// 1. it is always 0 on macOS Safari.
|
|
143
159
|
// 2. On Chrome Android, it's scaled by devicePixelRatio, but not on Chrome macOS
|
|
144
|
-
move(pointerType, e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY);
|
|
160
|
+
move(e, pointerType, e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY);
|
|
145
161
|
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
|
|
146
162
|
}
|
|
147
163
|
};
|
|
148
164
|
|
|
149
165
|
let onPointerUp = (e: PointerEvent) => {
|
|
150
166
|
if (e.pointerId === state.current.id) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
end(pointerType);
|
|
167
|
+
let pointerType = (e.pointerType || 'mouse') as PointerType;
|
|
168
|
+
end(e, pointerType);
|
|
154
169
|
state.current.id = null;
|
|
155
170
|
removeGlobalListener(window, 'pointermove', onPointerMove, false);
|
|
156
171
|
removeGlobalListener(window, 'pointerup', onPointerUp, false);
|
|
@@ -172,10 +187,10 @@ export function useMove(props: MoveEvents): MoveResult {
|
|
|
172
187
|
};
|
|
173
188
|
}
|
|
174
189
|
|
|
175
|
-
let triggerKeyboardMove = (deltaX: number, deltaY: number) => {
|
|
190
|
+
let triggerKeyboardMove = (e: EventBase, deltaX: number, deltaY: number) => {
|
|
176
191
|
start();
|
|
177
|
-
move('keyboard', deltaX, deltaY);
|
|
178
|
-
end('keyboard');
|
|
192
|
+
move(e, 'keyboard', deltaX, deltaY);
|
|
193
|
+
end(e, 'keyboard');
|
|
179
194
|
};
|
|
180
195
|
|
|
181
196
|
moveProps.onKeyDown = (e) => {
|
|
@@ -184,25 +199,25 @@ export function useMove(props: MoveEvents): MoveResult {
|
|
|
184
199
|
case 'ArrowLeft':
|
|
185
200
|
e.preventDefault();
|
|
186
201
|
e.stopPropagation();
|
|
187
|
-
triggerKeyboardMove(-1, 0);
|
|
202
|
+
triggerKeyboardMove(e, -1, 0);
|
|
188
203
|
break;
|
|
189
204
|
case 'Right':
|
|
190
205
|
case 'ArrowRight':
|
|
191
206
|
e.preventDefault();
|
|
192
207
|
e.stopPropagation();
|
|
193
|
-
triggerKeyboardMove(1, 0);
|
|
208
|
+
triggerKeyboardMove(e, 1, 0);
|
|
194
209
|
break;
|
|
195
210
|
case 'Up':
|
|
196
211
|
case 'ArrowUp':
|
|
197
212
|
e.preventDefault();
|
|
198
213
|
e.stopPropagation();
|
|
199
|
-
triggerKeyboardMove(0, -1);
|
|
214
|
+
triggerKeyboardMove(e, 0, -1);
|
|
200
215
|
break;
|
|
201
216
|
case 'Down':
|
|
202
217
|
case 'ArrowDown':
|
|
203
218
|
e.preventDefault();
|
|
204
219
|
e.stopPropagation();
|
|
205
|
-
triggerKeyboardMove(0, 1);
|
|
220
|
+
triggerKeyboardMove(e, 0, 1);
|
|
206
221
|
break;
|
|
207
222
|
}
|
|
208
223
|
};
|