Skip to content

Commit

Permalink
Add userSelect prop (#2280)
Browse files Browse the repository at this point in the history
## 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 <[email protected]>
  • Loading branch information
m-bert and m-bert authored Oct 21, 2022
1 parent 4b9b0bc commit 1b57803
Show file tree
Hide file tree
Showing 9 changed files with 49 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/docs/api/gestures/base-gesture-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ API.
Sets a `testID` property for gesture object, allowing for querying for it in tests.

### `cancelsToucesInView(value)` (**iOS only**)

Accepts a boolean value.
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).
Default value is `true`.
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/api/gestures/gesture-detector.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ GestureDetector will decide whether to use Reanimated to process provided gestur

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.
:::

### `userSelect` (**web only**)

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"`.
4 changes: 4 additions & 0 deletions docs/docs/gesture-handlers/api/common-gh.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ Specifying `width` or `height` is useful if we only want the gesture to activate

**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.

### `userSelect` (**web only**)

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"`.

### `onGestureEvent`

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.
Expand Down
9 changes: 9 additions & 0 deletions src/components/DrawerLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import {
GestureEvent,
HandlerStateChangeEvent,
UserSelect,
} from '../handlers/gestureHandlerCommon';
import {
PanGestureHandler,
Expand Down Expand Up @@ -153,6 +154,13 @@ export interface DrawerLayoutProps {
children?:
| React.ReactNode
| ((openValue?: Animated.AnimatedInterpolation) => React.ReactNode);

/**
* @default 'none'
* Defines which userSelect property should be used.
* Values: 'none'|'text'|'auto'
*/
userSelect?: UserSelect;
}

export type DrawerLayoutState = {
Expand Down Expand Up @@ -678,6 +686,7 @@ export default class DrawerLayout extends Component<
return (
<PanGestureHandler
// @ts-ignore could be fixed in handler types
userSelect={this.props.userSelect}
ref={this.setPanGestureRef}
hitSlop={hitSlop}
activeOffsetX={gestureOrientation * minSwipeDistance!}
Expand Down
4 changes: 4 additions & 0 deletions src/handlers/gestureHandlerCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const commonProps = [
'shouldCancelWhenOutside',
'hitSlop',
'cancelsTouchesInView',
'userSelect',
] as const;

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

export type UserSelect = 'none' | 'auto' | 'text';

//TODO(TS) events in handlers

export interface GestureEvent<ExtraEventPayloadT = Record<string, unknown>> {
Expand Down Expand Up @@ -101,6 +104,7 @@ export type CommonGestureConfig = {
enabled?: boolean;
shouldCancelWhenOutside?: boolean;
hitSlop?: HitSlop;
userSelect?: UserSelect;
};

// Events payloads are types instead of interfaces due to TS limitation.
Expand Down
16 changes: 16 additions & 0 deletions src/handlers/gestures/GestureDetector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
GestureStateChangeEvent,
HandlerStateChangeEvent,
scheduleFlushOperations,
UserSelect,
} from '../gestureHandlerCommon';
import {
GestureStateManager,
Expand Down Expand Up @@ -573,12 +574,27 @@ function validateDetectorChildren(ref: any) {
}
}

const applyUserSelectProp = (
userSelect: UserSelect,
gesture: ComposedGesture | GestureType
): void => {
for (const g of gesture.toGestureArray()) {
g.config.userSelect = userSelect;
}
};

interface GestureDetectorProps {
gesture?: ComposedGesture | GestureType;
userSelect?: UserSelect;
children?: React.ReactNode;
}
export const GestureDetector = (props: GestureDetectorProps) => {
const gestureConfig = props.gesture;

if (props.userSelect && gestureConfig) {
applyUserSelectProp(props.userSelect, gestureConfig);
}

const gesture = gestureConfig?.toGestureArray?.() ?? [];
const useReanimatedHook = gesture.some((g) => g.shouldUseReanimated);
const viewRef = useRef(null);
Expand Down
11 changes: 8 additions & 3 deletions src/web/handlers/GestureHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,16 @@ export default abstract class GestureHandler {

this.view = findNodeHandle(this.ref) as unknown as HTMLElement;
this.view.style['touchAction'] = 'none';
this.view.style['webkitUserSelect'] = 'none';
this.view.style['userSelect'] = 'none';

//@ts-ignore This one disables default events on Safari
this.view.style['WebkitTouchCallout'] = 'none';

if (!this.config.userSelect) {
this.view.style['webkitUserSelect'] = 'none';
this.view.style['userSelect'] = 'none';
} else {
this.view.style['webkitUserSelect'] = this.config.userSelect;
this.view.style['userSelect'] = this.config.userSelect;
}
}

private addEventManager(manager: EventManager): void {
Expand Down
2 changes: 0 additions & 2 deletions src/web/handlers/NativeViewGestureHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ export default class NativeViewGestureHandler extends GestureHandler {
this.buttonRole = true;
} else {
this.buttonRole = false;
this.view.style['webkitUserSelect'] = 'auto';
this.view.style['userSelect'] = 'auto';
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/web/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { UserSelect } from '../handlers/gestureHandlerCommon';
import { Directions } from '../Directions';
import { State } from '../State';

Expand All @@ -20,6 +21,7 @@ type ConfigArgs =
| number
| boolean
| HitSlop
| UserSelect
| Directions
| Handler[]
| null
Expand All @@ -31,6 +33,7 @@ export interface Config extends Record<string, ConfigArgs> {
waitFor?: Handler[] | null;
hitSlop?: HitSlop;
shouldCancelWhenOutside?: boolean;
userSelect?: UserSelect;

activateAfterLongPress?: number;
failOffsetXStart?: number;
Expand Down

0 comments on commit 1b57803

Please sign in to comment.