Skip to content

Commit 5bfcd85

Browse files
thezeroberickson1
authored andcommitted
[Windows] Add onContextMenu callback to GestureView (microsoft#599)
* add isRightButton property to GestureView tap * generic handling of mouse tap on GestureView * add onContextMenu handling * add conversion for Mac * cleanup whitespace changes * PR fixes: change MouseEvent.button back to number, unify comment formatting * rename onContextMenuGesture to onContextMenu; add e.stopPropagation to web
1 parent 0ef52b7 commit 5bfcd85

6 files changed

Lines changed: 118 additions & 15 deletions

File tree

docs/docs/components/gestureview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ onPanVertical: (gestureState: PanGestureState) => void = undefined;
2727
onPanHorizontal: (gestureState: PanGestureState) => void = undefined;
2828
onTap: (gestureState: TapGestureState) => void = undefined;
2929
onDoubleTap: (gestureState: TapGestureState) => void = undefined;
30+
onContextMenu: (gestureState: TapGestureState) => void = undefined;
3031

3132
// We can set vertical or horizontal as preferred
3233
preferredPan: PreferredPanGesture = undefined; // Horizontal or vertical

samples/RXPTest/src/Tests/GestureViewTest.tsx

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Tests the functionality of a GestureView component
2+
* Tests the functionality of a GestureView component
33
* through user interaction.
44
*/
55

@@ -38,10 +38,12 @@ const _styles = {
3838
};
3939

4040
const _colors = ['red', 'green', 'blue'];
41+
const _shades = ['#000', '#333', '#666', '#999', '#CCC', '#FFF'];
4142

4243
interface GestureViewState {
4344
test1ColorIndex?: number;
4445
test2ColorIndex?: number;
46+
test4ColorIndex?: number;
4547
}
4648

4749
class GestureViewView extends RX.Component<RX.CommonProps, GestureViewState> {
@@ -51,7 +53,7 @@ class GestureViewView extends RX.Component<RX.CommonProps, GestureViewState> {
5153
private _test1AnimatedStyle = RX.Styles.createAnimatedViewStyle({
5254
transform: [{
5355
scale: this._test1ScaleValue
54-
}, {
56+
}, {
5557
rotate: this._test1RotateValue.interpolate({
5658
inputRange: [0, 360],
5759
outputRange: ['0deg', '360deg']
@@ -79,12 +81,23 @@ class GestureViewView extends RX.Component<RX.CommonProps, GestureViewState> {
7981
}]
8082
});
8183

84+
private _test4HorizontalOffset = new RX.Animated.Value(0);
85+
private _test4VerticalOffset = new RX.Animated.Value(0);
86+
private _test4AnimatedStyle = RX.Styles.createAnimatedViewStyle({
87+
transform: [{
88+
translateX: this._test4HorizontalOffset
89+
}, {
90+
translateY: this._test4VerticalOffset
91+
}]
92+
});
93+
8294
constructor(props: RX.CommonProps) {
8395
super(props);
8496

8597
this.state = {
8698
test1ColorIndex: 0,
87-
test2ColorIndex: 1
99+
test2ColorIndex: 1,
100+
test4ColorIndex: 2
88101
};
89102
}
90103

@@ -98,6 +111,10 @@ class GestureViewView extends RX.Component<RX.CommonProps, GestureViewState> {
98111
backgroundColor: _colors[this.state.test2ColorIndex]
99112
}, false);
100113

114+
let test4ColorStyle = RX.Styles.createViewStyle({
115+
backgroundColor: _shades[this.state.test4ColorIndex]
116+
}, false);
117+
101118
return (
102119
<RX.View style={ _styles.container}>
103120
<RX.View style={ _styles.explainTextContainer } key={ 'explanation1' }>
@@ -154,6 +171,23 @@ class GestureViewView extends RX.Component<RX.CommonProps, GestureViewState> {
154171
style={ [_styles.smallBox, this._test3AnimatedStyle] }
155172
/>
156173
</RX.GestureView>
174+
175+
<RX.View style={ _styles.explainTextContainer } key={ 'explanation4' }>
176+
<RX.Text style={ _styles.explainText }>
177+
{ 'Desktop platforms: Left click will make the box lighter. ' +
178+
'Right click (context menu) will make the box darker.' }
179+
</RX.Text>
180+
</RX.View>
181+
<RX.GestureView
182+
style={ _styles.gestureView }
183+
onTap={ state => this._onTapTest4(state) }
184+
onContextMenu={ e => this._onContextMenu4(e) }
185+
mouseOverCursor={ RX.Types.GestureMouseCursor.Pointer }
186+
>
187+
<RX.Animated.View
188+
style={ [_styles.smallBox, test4ColorStyle, this._test4AnimatedStyle] }
189+
/>
190+
</RX.GestureView>
157191
</RX.View>
158192
);
159193
}
@@ -222,6 +256,19 @@ class GestureViewView extends RX.Component<RX.CommonProps, GestureViewState> {
222256
this._test3VerticalOffset.setValue(state.pageY - state.initialPageY);
223257
}
224258
}
259+
260+
private _onTapTest4(gestureState: RX.Types.TapGestureState) {
261+
// Change the color.
262+
this.setState({
263+
test4ColorIndex: Math.min(this.state.test4ColorIndex + 1, _shades.length - 1)
264+
});
265+
}
266+
267+
private _onContextMenu4(gestureState: RX.Types.TapGestureState) {
268+
this.setState({
269+
test4ColorIndex: Math.max(this.state.test4ColorIndex - 1, 0)
270+
});
271+
}
225272
}
226273

227274
class GestureViewTest implements Test {
@@ -230,7 +277,7 @@ class GestureViewTest implements Test {
230277
getPath(): string {
231278
return 'Components/GestureView';
232279
}
233-
280+
234281
getTestType(): TestType {
235282
return TestType.Interactive;
236283
}

src/common/Types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,7 @@ export interface GestureViewProps extends CommonStyledProps<ViewStyleRuleSet>, C
732732
onPanHorizontal?: (gestureState: PanGestureState) => void;
733733
onTap?: (gestureState: TapGestureState) => void;
734734
onDoubleTap?: (gestureState: TapGestureState) => void;
735+
onContextMenu?: (gestureState: TapGestureState) => void;
735736

736737
// We can set vertical or horizontal as preferred
737738
preferredPan?: PreferredPanGesture;

src/native-common/GestureView.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import React = require('react');
1515
import RN = require('react-native');
1616

1717
import AccessibilityUtil from './AccessibilityUtil';
18+
import EventHelpers from './utils/EventHelpers';
1819

1920
import Types = require('../common/Types');
2021
import UserInterface from './UserInterface';
@@ -63,7 +64,7 @@ export abstract class GestureView extends React.Component<Types.GestureViewProps
6364

6465
this._lastGestureStartEvent = event;
6566
// If we're trying to detect a tap, set this as the responder immediately.
66-
if (this.props.onTap || this.props.onDoubleTap) {
67+
if (this.props.onTap || this.props.onDoubleTap || this.props.onContextMenu) {
6768
return true;
6869
}
6970
return false;
@@ -501,7 +502,21 @@ export abstract class GestureView extends React.Component<Types.GestureViewProps
501502
}
502503

503504
private _sendTapEvent(e: Types.TouchEvent) {
504-
if (this.props.onTap) {
505+
const button = EventHelpers.toMouseButton(e);
506+
if (button === 2) {
507+
// Always handle secondary button, even if context menu is not set - it shouldn't trigger onTap.
508+
if (this.props.onContextMenu) {
509+
const tapEvent: Types.TapGestureState = {
510+
pageX: e.pageX!!!,
511+
pageY: e.pageY!!!,
512+
clientX: e.locationX!!!,
513+
clientY: e.locationY!!!,
514+
timeStamp: e.timeStamp
515+
};
516+
517+
this.props.onContextMenu(tapEvent);
518+
}
519+
} else if (this.props.onTap) {
505520
const tapEvent: Types.TapGestureState = {
506521
pageX: e.pageX!!!,
507522
pageY: e.pageY!!!,
@@ -515,6 +530,16 @@ export abstract class GestureView extends React.Component<Types.GestureViewProps
515530
}
516531

517532
private _sendDoubleTapEvent(e: Types.TouchEvent) {
533+
// If user did a double click with different mouse buttons, eg. left (50ms) right
534+
// both clicks need to be registered as separate events.
535+
const lastButton = EventHelpers.toMouseButton(this._lastTapEvent!!!);
536+
const button = EventHelpers.toMouseButton(e);
537+
if (lastButton !== button || button === 2) {
538+
this._sendTapEvent(this._lastTapEvent!!!);
539+
this._sendTapEvent(e);
540+
return;
541+
}
542+
518543
if (this.props.onDoubleTap) {
519544
const tapEvent: Types.TapGestureState = {
520545
pageX: e.pageX!!!,

src/native-common/utils/EventHelpers.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class EventHelpers {
9898
case 'ArrowDown':
9999
keyCode = 20;
100100
break;
101-
101+
102102
case 'Number0':
103103
keyCode = 48;
104104
break;
@@ -190,7 +190,7 @@ export class EventHelpers {
190190
// reuses events, so we're not allowed to modify the original.
191191
// Instead, we'll clone it.
192192
let mouseEvent = _.clone(e as Types.MouseEvent);
193-
193+
194194
const nativeEvent = e.nativeEvent;
195195

196196
// We keep pageX/Y and clientX/Y coordinates in sync, similar to the React web behavior
@@ -203,13 +203,7 @@ export class EventHelpers {
203203
mouseEvent.clientY = mouseEvent.pageY = nativeEvent.pageY;
204204
}
205205

206-
if (!!nativeEvent.IsRightButton) {
207-
mouseEvent.button = 2;
208-
} else if (!!nativeEvent.IsMiddleButton) {
209-
mouseEvent.button = 1;
210-
} else {
211-
mouseEvent.button = 0;
212-
}
206+
mouseEvent.button = this.toMouseButton(e.nativeEvent as Types.TouchEvent);
213207

214208
if (nativeEvent.shiftKey) {
215209
mouseEvent.shiftKey = nativeEvent.shiftKey;
@@ -239,6 +233,19 @@ export class EventHelpers {
239233
return mouseEvent;
240234
}
241235

236+
toMouseButton(e: Types.TouchEvent): number {
237+
const nativeEvent = e as any;
238+
if (nativeEvent.button !== undefined) {
239+
return nativeEvent.button;
240+
} else if (nativeEvent.isRightButton || nativeEvent.IsRightButton) {
241+
return 2;
242+
} else if (nativeEvent.isMiddleButton || nativeEvent.IsMiddleButton) {
243+
return 1;
244+
}
245+
246+
return 0;
247+
}
248+
242249
isRightMouseButton(e: Types.SyntheticEvent): boolean {
243250
return !!e.nativeEvent.isRightButton;
244251
}

src/web/GestureView.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export class GestureView extends React.Component<Types.GestureViewProps, Types.S
7777
role={ ariaRole }
7878
aria-label={ this.props.accessibilityLabel }
7979
aria-hidden={ isAriaHidden }
80+
onContextMenu={ this.props.onContextMenu ? this._sendContextMenuEvent : undefined }
8081
>
8182
{ this.props.children }
8283
</div>
@@ -182,6 +183,27 @@ export class GestureView extends React.Component<Types.GestureViewProps, Types.S
182183
}
183184
}
184185

186+
private _sendContextMenuEvent = (e: React.MouseEvent<any>) => {
187+
if (this.props.onContextMenu) {
188+
e.preventDefault();
189+
e.stopPropagation();
190+
191+
const clientRect = this._getGestureViewClientRect();
192+
193+
if (clientRect) {
194+
const tapEvent: Types.TapGestureState = {
195+
pageX: e.pageX,
196+
pageY: e.pageY,
197+
clientX: e.clientX - clientRect.left,
198+
clientY: e.clientY - clientRect.top,
199+
timeStamp: e.timeStamp
200+
};
201+
202+
this.props.onContextMenu(tapEvent);
203+
}
204+
}
205+
}
206+
185207
private _detectGestureType = (gestureState: Types.PanGestureState) => {
186208
// we need to lock gesture type until it's completed
187209
if (this._gestureTypeLocked) {

0 commit comments

Comments
 (0)