Skip to content

Commit 1b57803

Browse files
authored
Add userSelect prop (#2280)
## Description This PR adds `userSelect` prop to web versions of gesture handlers in response to [this issue in react-navigation](react-navigation/react-navigation#10922). It also reverts changes made in PR mentioned in [this issue](#2211). Now gesture handlers have `userSelect` property, which can be used to manually set `userSelect` css property. This change also affects `DrawerLayout` and `ScrollView`. Not that by default `userSelect` is set to `"none"`. To enable it, just pass this prop to handler with proper value. Possible values are: `"none" | "auto" | "text"`. For example: ```JSX <PanGestureHandler userSelect="auto"> // ... </PanGestureHandler> ``` ## Test plan Tested on example app. Co-authored-by: Michał Bert <michal.bert@swmansion.com>
1 parent 4b9b0bc commit 1b57803

File tree

9 files changed

+49
-5
lines changed

9 files changed

+49
-5
lines changed

docs/docs/api/gestures/base-gesture-config.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ API.
3737
Sets a `testID` property for gesture object, allowing for querying for it in tests.
3838

3939
### `cancelsToucesInView(value)` (**iOS only**)
40+
4041
Accepts a boolean value.
4142
When `true`, the gesture will cancel touches for native UI components (`UIButton`, `UISwitch`, etc) it's attached to when it becomes [`ACTIVE`](../../under-the-hood/states-events.md#active).
4243
Default value is `true`.

docs/docs/api/gestures/gesture-detector.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ GestureDetector will decide whether to use Reanimated to process provided gestur
2525

2626
Starting with Reanimated-2.3.0-beta.4 Gesture Handler will provide a [StateManager](./state-manager.md) in the [touch events](./touch-events.md) that allows for managing the state of the gesture.
2727
:::
28+
29+
### `userSelect` (**web only**)
30+
31+
This parameter allows to specify which `userSelect` property should be applied to underlying view. Possible values are `"none" | "auto" | "text"`. If this parameter is not specified, default value is `"none"`.

docs/docs/gesture-handlers/api/common-gh.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ Specifying `width` or `height` is useful if we only want the gesture to activate
6363

6464
**IMPORTANT:** Note that this parameter is primarily designed to reduce the area where gesture can activate. Hence it is only supported for all the values (except `width` and `height`) to be non positive (0 or lower). Although on Android it is supported for the values to also be positive and therefore allow to expand beyond view bounds but not further than the parent view bounds. To achieve this effect on both platforms you can use React Native's View [hitSlop](https://facebook.github.io/react-native/docs/view.html#props) property.
6565

66+
### `userSelect` (**web only**)
67+
68+
This parameter allows to specify which `userSelect` property should be applied to underlying view. Possible values are `"none" | "auto" | "text"`. If this parameter is not specified, default value is `"none"`.
69+
6670
### `onGestureEvent`
6771

6872
Takes a callback that is going to be triggered for each subsequent touch event while the handler is in an [ACTIVE](../basics/state.md#active) state. Event payload depends on the particular handler type. Common set of event data attributes is documented [below](#event-data) and handler specific attributes are documented on the corresponding handler pages. E.g. event payload for [`PinchGestureHandler`](./rotation-gh.md#event-data) contains `scale` attribute that represents how the distance between fingers changed since when the gesture started.

src/components/DrawerLayout.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
import {
2727
GestureEvent,
2828
HandlerStateChangeEvent,
29+
UserSelect,
2930
} from '../handlers/gestureHandlerCommon';
3031
import {
3132
PanGestureHandler,
@@ -153,6 +154,13 @@ export interface DrawerLayoutProps {
153154
children?:
154155
| React.ReactNode
155156
| ((openValue?: Animated.AnimatedInterpolation) => React.ReactNode);
157+
158+
/**
159+
* @default 'none'
160+
* Defines which userSelect property should be used.
161+
* Values: 'none'|'text'|'auto'
162+
*/
163+
userSelect?: UserSelect;
156164
}
157165

158166
export type DrawerLayoutState = {
@@ -678,6 +686,7 @@ export default class DrawerLayout extends Component<
678686
return (
679687
<PanGestureHandler
680688
// @ts-ignore could be fixed in handler types
689+
userSelect={this.props.userSelect}
681690
ref={this.setPanGestureRef}
682691
hitSlop={hitSlop}
683692
activeOffsetX={gestureOrientation * minSwipeDistance!}

src/handlers/gestureHandlerCommon.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const commonProps = [
1818
'shouldCancelWhenOutside',
1919
'hitSlop',
2020
'cancelsTouchesInView',
21+
'userSelect',
2122
] as const;
2223

2324
const componentInteractionProps = ['waitFor', 'simultaneousHandlers'] as const;
@@ -62,6 +63,8 @@ export type HitSlop =
6263
| Record<'height' | 'top', number>
6364
| Record<'height' | 'bottom', number>;
6465

66+
export type UserSelect = 'none' | 'auto' | 'text';
67+
6568
//TODO(TS) events in handlers
6669

6770
export interface GestureEvent<ExtraEventPayloadT = Record<string, unknown>> {
@@ -101,6 +104,7 @@ export type CommonGestureConfig = {
101104
enabled?: boolean;
102105
shouldCancelWhenOutside?: boolean;
103106
hitSlop?: HitSlop;
107+
userSelect?: UserSelect;
104108
};
105109

106110
// Events payloads are types instead of interfaces due to TS limitation.

src/handlers/gestures/GestureDetector.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
GestureStateChangeEvent,
1919
HandlerStateChangeEvent,
2020
scheduleFlushOperations,
21+
UserSelect,
2122
} from '../gestureHandlerCommon';
2223
import {
2324
GestureStateManager,
@@ -573,12 +574,27 @@ function validateDetectorChildren(ref: any) {
573574
}
574575
}
575576

577+
const applyUserSelectProp = (
578+
userSelect: UserSelect,
579+
gesture: ComposedGesture | GestureType
580+
): void => {
581+
for (const g of gesture.toGestureArray()) {
582+
g.config.userSelect = userSelect;
583+
}
584+
};
585+
576586
interface GestureDetectorProps {
577587
gesture?: ComposedGesture | GestureType;
588+
userSelect?: UserSelect;
578589
children?: React.ReactNode;
579590
}
580591
export const GestureDetector = (props: GestureDetectorProps) => {
581592
const gestureConfig = props.gesture;
593+
594+
if (props.userSelect && gestureConfig) {
595+
applyUserSelectProp(props.userSelect, gestureConfig);
596+
}
597+
582598
const gesture = gestureConfig?.toGestureArray?.() ?? [];
583599
const useReanimatedHook = gesture.some((g) => g.shouldUseReanimated);
584600
const viewRef = useRef(null);

src/web/handlers/GestureHandler.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,16 @@ export default abstract class GestureHandler {
7272

7373
this.view = findNodeHandle(this.ref) as unknown as HTMLElement;
7474
this.view.style['touchAction'] = 'none';
75-
this.view.style['webkitUserSelect'] = 'none';
76-
this.view.style['userSelect'] = 'none';
77-
7875
//@ts-ignore This one disables default events on Safari
7976
this.view.style['WebkitTouchCallout'] = 'none';
77+
78+
if (!this.config.userSelect) {
79+
this.view.style['webkitUserSelect'] = 'none';
80+
this.view.style['userSelect'] = 'none';
81+
} else {
82+
this.view.style['webkitUserSelect'] = this.config.userSelect;
83+
this.view.style['userSelect'] = this.config.userSelect;
84+
}
8085
}
8186

8287
private addEventManager(manager: EventManager): void {

src/web/handlers/NativeViewGestureHandler.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ export default class NativeViewGestureHandler extends GestureHandler {
2929
this.buttonRole = true;
3030
} else {
3131
this.buttonRole = false;
32-
this.view.style['webkitUserSelect'] = 'auto';
33-
this.view.style['userSelect'] = 'auto';
3432
}
3533
}
3634

src/web/interfaces.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { UserSelect } from '../handlers/gestureHandlerCommon';
12
import { Directions } from '../Directions';
23
import { State } from '../State';
34

@@ -20,6 +21,7 @@ type ConfigArgs =
2021
| number
2122
| boolean
2223
| HitSlop
24+
| UserSelect
2325
| Directions
2426
| Handler[]
2527
| null
@@ -31,6 +33,7 @@ export interface Config extends Record<string, ConfigArgs> {
3133
waitFor?: Handler[] | null;
3234
hitSlop?: HitSlop;
3335
shouldCancelWhenOutside?: boolean;
36+
userSelect?: UserSelect;
3437

3538
activateAfterLongPress?: number;
3639
failOffsetXStart?: number;

0 commit comments

Comments
 (0)