Skip to content

Commit a30f42f

Browse files
authored
Add mouseButton prop (#2676)
## Description After giving it some thought, we decided to add new modifier to our handlers - `mouseButton`. This way users will be able to choose mouse buttons that handler will respond to. For now this prop is available on all handlers, but it is a topic for a short discussion. I think it is also worth to mention, that right now all three mouse buttons work on web, so this prop will allow us to 'disable' all buttons except those provided as arguments. This PR also adds two small examples (one for buttons and one for context menu) that can be used to test given prop on other platforms (when this functionality will be added) ## Test plan Tested on example app.
1 parent 876e9ce commit a30f42f

19 files changed

+393
-20
lines changed

example/src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { ComboWithGHScroll } from './release_tests/combo';
1616
import { TouchablesIndex, TouchableExample } from './release_tests/touchables';
1717
import Rows from './release_tests/rows';
1818
import Fling from './release_tests/fling';
19+
import MouseButtons from './release_tests/mouseButtons';
20+
import ContextMenu from './release_tests/contextMenu';
1921
import NestedTouchables from './release_tests/nestedTouchables';
2022
import NestedButtons from './release_tests/nestedButtons';
2123
import NestedGestureHandlerRootViewWithModal from './release_tests/nestedGHRootViewWithModal';
@@ -116,6 +118,8 @@ const EXAMPLES: ExamplesSection[] = [
116118
{ name: 'Fling', component: Fling },
117119
{ name: 'Combo', component: ComboWithGHScroll },
118120
{ name: 'Touchables', component: TouchablesIndex as React.ComponentType },
121+
{ name: 'MouseButtons', component: MouseButtons },
122+
{ name: 'ContextMenu (web only)', component: ContextMenu },
119123
{ name: 'Rounded buttons', component: RoundedButtons },
120124
],
121125
},
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React from 'react';
2+
import { StyleSheet, View } from 'react-native';
3+
import {
4+
Gesture,
5+
GestureDetector,
6+
MouseButton,
7+
} from 'react-native-gesture-handler';
8+
9+
export default function ContextMenuExample() {
10+
const p1 = Gesture.Pan().mouseButton(MouseButton.RIGHT);
11+
const p2 = Gesture.Pan();
12+
const p3 = Gesture.Pan();
13+
14+
return (
15+
<View style={styles.container}>
16+
<GestureDetector gesture={p1}>
17+
<View style={[styles.box, styles.grandParent]}>
18+
<GestureDetector gesture={p2} enableContextMenu={true}>
19+
<View style={[styles.box, styles.parent]}>
20+
<GestureDetector gesture={p3} enableContextMenu={false}>
21+
<View style={[styles.box, styles.child]} />
22+
</GestureDetector>
23+
</View>
24+
</GestureDetector>
25+
</View>
26+
</GestureDetector>
27+
</View>
28+
);
29+
}
30+
31+
const styles = StyleSheet.create({
32+
container: {
33+
flex: 1,
34+
justifyContent: 'space-around',
35+
alignItems: 'center',
36+
},
37+
38+
grandParent: {
39+
width: 300,
40+
height: 300,
41+
backgroundColor: 'lightblue',
42+
},
43+
44+
parent: {
45+
width: 200,
46+
height: 200,
47+
backgroundColor: 'lightgreen',
48+
},
49+
50+
child: {
51+
width: 100,
52+
height: 100,
53+
backgroundColor: 'crimson',
54+
},
55+
56+
box: {
57+
display: 'flex',
58+
justifyContent: 'space-around',
59+
alignItems: 'center',
60+
},
61+
});
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import React from 'react';
2+
import {
3+
Gesture,
4+
GestureDetector,
5+
GestureType,
6+
MouseButton,
7+
Directions,
8+
ScrollView,
9+
} from 'react-native-gesture-handler';
10+
import { StyleSheet, View, Text } from 'react-native';
11+
12+
const COLORS = ['darkmagenta', 'darkgreen', 'darkblue', 'crimson', 'pink'];
13+
14+
type TestProps = {
15+
name: string;
16+
gestureHandlers: GestureType[];
17+
};
18+
19+
function Test({ name, gestureHandlers }: TestProps) {
20+
return (
21+
<View style={styles.center}>
22+
<Text>{name}</Text>
23+
<View
24+
style={[
25+
{ margin: 10, width: '100%', flexDirection: 'row' },
26+
styles.center,
27+
]}>
28+
{gestureHandlers.map((handler, index) => {
29+
return (
30+
<GestureDetector gesture={handler} key={index}>
31+
<View style={[styles.box, { backgroundColor: COLORS[index] }]} />
32+
</GestureDetector>
33+
);
34+
})}
35+
</View>
36+
</View>
37+
);
38+
}
39+
40+
function TapTests() {
41+
const leftTap = Gesture.Tap()
42+
.mouseButton(MouseButton.LEFT)
43+
.onEnd(() => console.log('Tap with left'));
44+
45+
const middleTap = Gesture.Tap()
46+
.mouseButton(MouseButton.MIDDLE)
47+
.onEnd(() => console.log('Tap with middle'));
48+
49+
const rightTap = Gesture.Tap()
50+
.mouseButton(MouseButton.RIGHT)
51+
.onEnd(() => console.log('Tap with right'));
52+
53+
const leftRightTap = Gesture.Tap()
54+
.mouseButton(MouseButton.LEFT | MouseButton.RIGHT)
55+
.onEnd(() => console.log('Tap with left | right'));
56+
57+
const allTap = Gesture.Tap()
58+
.mouseButton(MouseButton.ALL)
59+
.onEnd(() => console.log('Tap with any button'));
60+
61+
const gestureHandlers = [leftTap, middleTap, rightTap, leftRightTap, allTap];
62+
63+
return <Test name={'Tap'} gestureHandlers={gestureHandlers} />;
64+
}
65+
66+
function PanTests() {
67+
const leftPan = Gesture.Pan()
68+
.mouseButton(MouseButton.LEFT)
69+
.onChange(() => console.log('Panning with left'));
70+
71+
const middlePan = Gesture.Pan()
72+
.mouseButton(MouseButton.MIDDLE)
73+
.onChange(() => console.log('Panning with middle'));
74+
75+
const rightPan = Gesture.Pan()
76+
.mouseButton(MouseButton.RIGHT)
77+
.onChange(() => console.log('Panning with right'));
78+
79+
const leftRightPan = Gesture.Pan()
80+
.mouseButton(MouseButton.LEFT | MouseButton.RIGHT)
81+
.onChange(() => console.log('Panning with left | right'));
82+
83+
const allPan = Gesture.Pan()
84+
.mouseButton(MouseButton.ALL)
85+
.onChange(() => console.log('Panning with any button'));
86+
87+
const gestureHandlers = [leftPan, middlePan, rightPan, leftRightPan, allPan];
88+
89+
return <Test name={'Pan'} gestureHandlers={gestureHandlers} />;
90+
}
91+
92+
function LongPressTests() {
93+
const leftLongPress = Gesture.LongPress()
94+
.mouseButton(MouseButton.LEFT)
95+
.onStart(() => console.log('LongPress with left'));
96+
97+
const middleLongPress = Gesture.LongPress()
98+
.mouseButton(MouseButton.MIDDLE)
99+
.onStart(() => console.log('LongPress with middle'));
100+
101+
const rightLongPress = Gesture.LongPress()
102+
.mouseButton(MouseButton.RIGHT)
103+
.onStart(() => console.log('LongPress with right'));
104+
105+
const leftRightLongPress = Gesture.LongPress()
106+
.mouseButton(MouseButton.LEFT | MouseButton.RIGHT)
107+
.onStart(() => console.log('LongPress with left | right'));
108+
109+
const allLongPress = Gesture.LongPress()
110+
.mouseButton(MouseButton.ALL)
111+
.onStart(() => console.log('LongPress with any button'));
112+
113+
const gestureHandlers = [
114+
leftLongPress,
115+
middleLongPress,
116+
rightLongPress,
117+
leftRightLongPress,
118+
allLongPress,
119+
];
120+
121+
return <Test name={'LongPress'} gestureHandlers={gestureHandlers} />;
122+
}
123+
124+
function FlingTests() {
125+
const leftFling = Gesture.Fling()
126+
.direction(Directions.LEFT | Directions.RIGHT)
127+
.mouseButton(MouseButton.LEFT)
128+
.onStart(() => console.log('Fling with left'));
129+
130+
const middleFling = Gesture.Fling()
131+
.direction(Directions.LEFT | Directions.RIGHT)
132+
.mouseButton(MouseButton.MIDDLE)
133+
.onStart(() => console.log('Fling with middle'));
134+
135+
const rightFling = Gesture.Fling()
136+
.direction(Directions.LEFT | Directions.RIGHT)
137+
.mouseButton(MouseButton.RIGHT)
138+
.onStart(() => console.log('Fling with right'));
139+
140+
const leftRightFling = Gesture.Fling()
141+
.direction(Directions.LEFT | Directions.RIGHT)
142+
.mouseButton(MouseButton.LEFT | MouseButton.RIGHT)
143+
.onStart(() => console.log('Fling with left | right'));
144+
145+
const allFling = Gesture.Fling()
146+
.direction(Directions.LEFT | Directions.RIGHT)
147+
.mouseButton(MouseButton.ALL)
148+
.onStart(() => console.log('Fling with any button'));
149+
150+
const gestureHandlers = [
151+
leftFling,
152+
middleFling,
153+
rightFling,
154+
leftRightFling,
155+
allFling,
156+
];
157+
158+
return <Test name={'Fling'} gestureHandlers={gestureHandlers} />;
159+
}
160+
161+
export default function Buttons() {
162+
return (
163+
<ScrollView style={{ flex: 1 }}>
164+
<TapTests />
165+
<PanTests />
166+
<LongPressTests />
167+
<FlingTests />
168+
</ScrollView>
169+
);
170+
}
171+
172+
const styles = StyleSheet.create({
173+
center: {
174+
alignItems: 'center',
175+
justifyContent: 'space-around',
176+
},
177+
box: {
178+
width: 75,
179+
height: 75,
180+
},
181+
});

src/components/DrawerLayout.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
TapGestureHandlerEventPayload,
3939
} from '../handlers/TapGestureHandler';
4040
import { State } from '../State';
41+
import { MouseButton } from '../web/interfaces';
4142

4243
const DRAG_TOSS = 0.05;
4344

@@ -173,6 +174,18 @@ export interface DrawerLayoutProps {
173174
* Values: see CSS cursor values
174175
*/
175176
activeCursor?: ActiveCursor;
177+
178+
/**
179+
* @default 'MouseButton.LEFT'
180+
* Allows to choose which mouse button should underlying pan handler react to.
181+
*/
182+
mouseButton?: MouseButton;
183+
184+
/**
185+
* @default 'false if MouseButton.RIGHT is specified'
186+
* Allows to enable/disable context menu.
187+
*/
188+
enableContextMenu?: boolean;
176189
}
177190

178191
export type DrawerLayoutState = {
@@ -700,6 +713,8 @@ export default class DrawerLayout extends Component<
700713
// @ts-ignore could be fixed in handler types
701714
userSelect={this.props.userSelect}
702715
activeCursor={this.props.activeCursor}
716+
mouseButton={this.props.mouseButton}
717+
enableContextMenu={this.props.enableContextMenu}
703718
ref={this.setPanGestureRef}
704719
hitSlop={hitSlop}
705720
activeOffsetX={gestureOrientation * minSwipeDistance!}

src/handlers/gestureHandlerCommon.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { handlerIDToTag } from './handlersRegistry';
1212
import { toArray } from '../utils';
1313
import RNGestureHandlerModule from '../RNGestureHandlerModule';
1414
import { ghQueueMicrotask } from '../ghQueueMicrotask';
15+
import { MouseButton } from '../web/interfaces';
1516

1617
const commonProps = [
1718
'id',
@@ -21,6 +22,8 @@ const commonProps = [
2122
'cancelsTouchesInView',
2223
'userSelect',
2324
'activeCursor',
25+
'mouseButton',
26+
'enableContextMenu',
2427
] as const;
2528

2629
const componentInteractionProps = [
@@ -149,6 +152,8 @@ export type CommonGestureConfig = {
149152
hitSlop?: HitSlop;
150153
userSelect?: UserSelect;
151154
activeCursor?: ActiveCursor;
155+
mouseButton?: MouseButton;
156+
enableContextMenu?: boolean;
152157
};
153158

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

src/handlers/gestures/GestureDetector.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,10 +605,20 @@ const applyUserSelectProp = (
605605
}
606606
};
607607

608+
const applyEnableContextMenuProp = (
609+
enableContextMenu: boolean,
610+
gesture: ComposedGesture | GestureType
611+
): void => {
612+
for (const g of gesture.toGestureArray()) {
613+
g.config.enableContextMenu = enableContextMenu;
614+
}
615+
};
616+
608617
interface GestureDetectorProps {
609618
gesture: ComposedGesture | GestureType;
610-
userSelect?: UserSelect;
611619
children?: React.ReactNode;
620+
userSelect?: UserSelect;
621+
enableContextMenu?: boolean;
612622
}
613623
interface GestureDetectorState {
614624
firstRender: boolean;
@@ -630,6 +640,10 @@ export const GestureDetector = (props: GestureDetectorProps) => {
630640
applyUserSelectProp(props.userSelect, gestureConfig);
631641
}
632642

643+
if (props.enableContextMenu !== undefined) {
644+
applyEnableContextMenuProp(props.enableContextMenu, gestureConfig);
645+
}
646+
633647
const gesture = gestureConfig.toGestureArray();
634648
const useReanimatedHook = gesture.some((g) => g.shouldUseReanimated);
635649

src/handlers/gestures/gesture.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { RotationGestureHandlerEventPayload } from '../RotationGestureHandler';
1717
import { TapGestureHandlerEventPayload } from '../TapGestureHandler';
1818
import { NativeViewGestureHandlerPayload } from '../NativeViewGestureHandler';
1919
import { isRemoteDebuggingEnabled } from '../../utils';
20+
import { MouseButton } from '../../web/interfaces';
2021

2122
export type GestureType =
2223
| BaseGesture<Record<string, unknown>>
@@ -257,6 +258,11 @@ export abstract class BaseGesture<
257258
return this;
258259
}
259260

261+
mouseButton(mouseButton: MouseButton) {
262+
this.config.mouseButton = mouseButton;
263+
return this;
264+
}
265+
260266
runOnJS(runOnJS: boolean) {
261267
this.config.runOnJS = runOnJS;
262268
return this;

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { initialize } from './init';
22

33
export { Directions } from './Directions';
44
export { State } from './State';
5+
export { MouseButton } from './web/interfaces';
56
export { default as gestureHandlerRootHOC } from './components/gestureHandlerRootHOC';
67
export { default as GestureHandlerRootView } from './components/GestureHandlerRootView';
78
export type {

src/web/handlers/FlingGestureHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ export default class FlingGestureHandler extends GestureHandler {
8383
}
8484

8585
protected onPointerDown(event: AdaptedEvent): void {
86+
if (!this.isButtonInConfig(event.button)) {
87+
return;
88+
}
89+
8690
this.tracker.addToTracker(event);
8791
this.keyPointer = event.pointerId;
8892

0 commit comments

Comments
 (0)