[Flare] Adds onContextMenu and fixes some contextmenu related issues (#15761)
Dominic Gannaway committed
May 29, 2019 at 17:53 UTC
142cf56cbfe6d470f57e675e4e7079e904832cdd
2 files changed
+145
-25
packages/react-events/src/Press.js
+39
-18
@@ -20,6 +20,7 @@ type PressProps = {
20
delayLongPress: number,
21
delayPressEnd: number,
22
delayPressStart: number,
23
+ onContextMenu: (e: PressEvent) => void,
24
onLongPress: (e: PressEvent) => void,
25
onLongPressChange: boolean => void,
26
onLongPressShouldCancelPress: () => boolean,
@@ -78,7 +79,8 @@ type PressEventType =
79
| 'pressend'
80
| 'presschange'
81
| 'longpress'
81
- | 'longpresschange';
82
+ | 'longpresschange'
83
+ | 'contextmenu';
84
85
type PressEvent = {|
86
target: Element | Document,
@@ -99,6 +101,7 @@ type PressEvent = {|
101
shiftKey: boolean,
102
|};
103
104
+const isMac = /^Mac/.test(navigator.platform);
105
const DEFAULT_PRESS_END_DELAY_MS = 0;
106
const DEFAULT_PRESS_START_DELAY_MS = 0;
107
const DEFAULT_LONG_PRESS_DELAY_MS = 500;
@@ -400,17 +403,10 @@ function dispatchCancel(
403
props: PressProps,
404
state: PressState,
405
): void {
403
- const nativeEvent: any = event.nativeEvent;
404
- const type = event.type;
405
-
406
if (state.isPressed) {
407
- if (type === 'contextmenu' && props.preventDefault !== false) {
408
- nativeEvent.preventDefault();
409
- } else {
410
- state.ignoreEmulatedMouseEvents = false;
411
- removeRootEventTypes(context, state);
412
- dispatchPressEndEvents(event, context, props, state);
413
- }
407
+ state.ignoreEmulatedMouseEvents = false;
408
+ removeRootEventTypes(context, state);
409
+ dispatchPressEndEvents(event, context, props, state);
410
} else if (state.allowPressReentry) {
411
removeRootEventTypes(context, state);
412
}
@@ -683,8 +679,9 @@ const PressResponder = {
679
return;
680
}
681
// Ignore mouse/pen pressing on touch hit target area
682
+ const isMouseType = pointerType === 'mouse';
683
if (
687
- (pointerType === 'mouse' || pointerType === 'pen') &&
684
+ (isMouseType || pointerType === 'pen') &&
685
isEventPositionWithinTouchHitTarget(event, context)
686
) {
687
// We need to prevent the native event to block the focus
@@ -692,14 +689,22 @@ const PressResponder = {
689
return;
690
}
691
695
- // Ignore any device buttons except left-mouse and touch/pen contact
696
- if (nativeEvent.button > 0) {
692
+ // We set these here, before the button check so we have this
693
+ // data around for handling of the context menu
694
+ state.pointerType = pointerType;
695
+ state.pressTarget = context.getEventCurrentTarget(event);
696
+
697
+ // Ignore any device buttons except left-mouse and touch/pen contact.
698
+ // Additionally we ignore left-mouse + ctrl-key with Macs as that
699
+ // acts like right-click and opens the contextmenu.
700
+ if (
701
+ nativeEvent.button > 0 ||
702
+ (isMac && isMouseType && nativeEvent.ctrlKey)
703
+ ) {
704
return;
705
}
706
707
state.allowPressReentry = true;
701
- state.pointerType = pointerType;
702
- state.pressTarget = context.getEventCurrentTarget(event);
708
state.responderRegionOnActivation = calculateResponderRegion(
709
context,
710
state.pressTarget,
@@ -717,9 +722,25 @@ const PressResponder = {
722
break;
723
}
724
720
- // CANCEL
725
case 'contextmenu': {
722
- dispatchCancel(event, context, props, state);
726
+ if (state.isPressed) {
727
+ dispatchCancel(event, context, props, state);
728
+ if (props.preventDefault !== false) {
729
+ // Skip dispatching of onContextMenu below
730
+ nativeEvent.preventDefault();
731
+ return;
732
+ }
733
+ }
734
+ if (props.onContextMenu) {
735
+ dispatchEvent(
736
+ event,
737
+ context,
738
+ state,
739
+ 'contextmenu',
740
+ props.onContextMenu,
741
+ true,
742
+ );
743
+ }
744
break;
745
}
746
packages/react-events/src/__tests__/Press-test.internal.js
+106
-7
@@ -36,18 +36,21 @@ const createKeyboardEvent = (type, data) => {
36
});
37
};
38
39
+function init() {
40
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
41
+ ReactFeatureFlags.enableEventAPI = true;
42
+ React = require('react');
43
+ ReactDOM = require('react-dom');
44
+ Press = require('react-events/press');
45
+ Scheduler = require('scheduler');
46
+}
47
+
48
describe('Event responder: Press', () => {
49
let container;
50
51
beforeEach(() => {
52
jest.resetModules();
44
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
45
- ReactFeatureFlags.enableEventAPI = true;
46
- React = require('react');
47
- ReactDOM = require('react-dom');
48
- Press = require('react-events/press');
49
- Scheduler = require('scheduler');
50
-
53
+ init();
54
container = document.createElement('div');
55
document.body.appendChild(container);
56
});
@@ -2579,4 +2582,100 @@ describe('Event responder: Press', () => {
2582
Scheduler.flushAll();
2583
document.body.removeChild(newContainer);
2584
});
2585
+
2586
+ describe('onContextMenu', () => {
2587
+ it('is called after a right mouse click', () => {
2588
+ const onContextMenu = jest.fn();
2589
+ const ref = React.createRef();
2590
+ const element = (
2591
+ <Press onContextMenu={onContextMenu}>
2592
+ <div ref={ref} />
2593
+ </Press>
2594
+ );
2595
+ ReactDOM.render(element, container);
2596
+
2597
+ ref.current.dispatchEvent(
2598
+ createEvent('pointerdown', {pointerType: 'mouse', button: 2}),
2599
+ );
2600
+ ref.current.dispatchEvent(createEvent('contextmenu'));
2601
+ expect(onContextMenu).toHaveBeenCalledTimes(1);
2602
+ expect(onContextMenu).toHaveBeenCalledWith(
2603
+ expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
2604
+ );
2605
+ });
2606
+
2607
+ it('is called after a left mouse click + ctrl key on Mac', () => {
2608
+ jest.resetModules();
2609
+ const platformGetter = jest.spyOn(global.navigator, 'platform', 'get');
2610
+ platformGetter.mockReturnValue('MacIntel');
2611
+ init();
2612
+
2613
+ const onContextMenu = jest.fn();
2614
+ const ref = React.createRef();
2615
+ const element = (
2616
+ <Press onContextMenu={onContextMenu}>
2617
+ <div ref={ref} />
2618
+ </Press>
2619
+ );
2620
+ ReactDOM.render(element, container);
2621
+
2622
+ ref.current.dispatchEvent(
2623
+ createEvent('pointerdown', {
2624
+ pointerType: 'mouse',
2625
+ button: 0,
2626
+ ctrlKey: true,
2627
+ }),
2628
+ );
2629
+ ref.current.dispatchEvent(createEvent('contextmenu'));
2630
+ expect(onContextMenu).toHaveBeenCalledTimes(1);
2631
+ expect(onContextMenu).toHaveBeenCalledWith(
2632
+ expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
2633
+ );
2634
+ platformGetter.mockClear();
2635
+ });
2636
+
2637
+ it('is not called after a left mouse click + ctrl key on Windows', () => {
2638
+ jest.resetModules();
2639
+ const platformGetter = jest.spyOn(global.navigator, 'platform', 'get');
2640
+ platformGetter.mockReturnValue('Win32');
2641
+ init();
2642
+
2643
+ const onContextMenu = jest.fn();
2644
+ const ref = React.createRef();
2645
+ const element = (
2646
+ <Press onContextMenu={onContextMenu}>
2647
+ <div ref={ref} />
2648
+ </Press>
2649
+ );
2650
+ ReactDOM.render(element, container);
2651
+
2652
+ ref.current.dispatchEvent(
2653
+ createEvent('pointerdown', {
2654
+ pointerType: 'mouse',
2655
+ button: 0,
2656
+ ctrlKey: true,
2657
+ }),
2658
+ );
2659
+ ref.current.dispatchEvent(createEvent('contextmenu'));
2660
+ expect(onContextMenu).toHaveBeenCalledTimes(0);
2661
+ platformGetter.mockClear();
2662
+ });
2663
+
2664
+ it('is not called after a right mouse click occurs during an active press', () => {
2665
+ const onContextMenu = jest.fn();
2666
+ const ref = React.createRef();
2667
+ const element = (
2668
+ <Press onContextMenu={onContextMenu}>
2669
+ <div ref={ref} />
2670
+ </Press>
2671
+ );
2672
+ ReactDOM.render(element, container);
2673
+
2674
+ ref.current.dispatchEvent(
2675
+ createEvent('pointerdown', {pointerType: 'mouse', button: 0}),
2676
+ );
2677
+ ref.current.dispatchEvent(createEvent('contextmenu'));
2678
+ expect(onContextMenu).toHaveBeenCalledTimes(0);
2679
+ });
2680
+ });
2681
});