@samitouri / QOS-React-2 / commits / 23405c9c4c

[react-events] Add ContextMenu responder (#16296)

A module for responding to contextmenu events. This functionality will be removed from the Press responder in the future.

Nicolas Gallagher committed Aug 6, 2019 at 09:16 UTC 23405c9c4c30b07892e0721de9e5c59620b34bc4
5 files changed +414 -17
packages/react-events/context-menu.js new
+12
@@ -0,0 +1,12 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +'use strict';
11 +
12 +module.exports = require('./src/dom/ContextMenu');
packages/react-events/package.json
+6 -5
@@ -12,14 +12,15 @@
12 "files": [
13 "LICENSE",
14 "README.md",
15 - "press.js",
16 - "hover.js",
17 - "focus.js",
18 - "swipe.js",
15 + "context-menu.js",
16 "drag.js",
20 - "scroll.js",
17 + "focus.js",
18 + "hover.js",
19 "input.js",
20 "keyboard.js",
21 + "press.js",
22 + "scroll.js",
23 + "swipe.js",
24 "build-info.json",
25 "cjs/",
26 "umd/"
packages/react-events/src/dom/ContextMenu.js new
+128
@@ -0,0 +1,128 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {
11 + ReactDOMResponderEvent,
12 + ReactDOMResponderContext,
13 + PointerType,
14 +} from 'shared/ReactDOMTypes';
15 +import type {ReactEventResponderListener} from 'shared/ReactTypes';
16 +
17 +import React from 'react';
18 +import {DiscreteEvent} from 'shared/ReactTypes';
19 +
20 +type ContextMenuProps = {|
21 + disabled: boolean,
22 + onContextMenu: (e: ContextMenuEvent) => void,
23 + preventDefault: boolean,
24 +|};
25 +
26 +type ContextMenuState = {
27 + pointerType: PointerType,
28 +};
29 +
30 +type ContextMenuEvent = {|
31 + target: Element | Document,
32 + type: 'contextmenu',
33 + pointerType: PointerType,
34 + timeStamp: number,
35 + clientX: null | number,
36 + clientY: null | number,
37 + pageX: null | number,
38 + pageY: null | number,
39 + x: null | number,
40 + y: null | number,
41 + altKey: boolean,
42 + ctrlKey: boolean,
43 + metaKey: boolean,
44 + shiftKey: boolean,
45 +|};
46 +
47 +const hasPointerEvents =
48 + typeof window !== 'undefined' && window.PointerEvent != null;
49 +
50 +function dispatchContextMenuEvent(
51 + event: ReactDOMResponderEvent,
52 + context: ReactDOMResponderContext,
53 + props: ContextMenuProps,
54 + state: ContextMenuState,
55 +): void {
56 + const nativeEvent: any = event.nativeEvent;
57 + const target = event.target;
58 + const timeStamp = context.getTimeStamp();
59 + const pointerType = state.pointerType;
60 +
61 + const gestureState = {
62 + altKey: nativeEvent.altKey,
63 + button: nativeEvent.button === 0 ? 'primary' : 'auxillary',
64 + ctrlKey: nativeEvent.altKey,
65 + metaKey: nativeEvent.altKey,
66 + pageX: nativeEvent.altKey,
67 + pageY: nativeEvent.altKey,
68 + pointerType,
69 + shiftKey: nativeEvent.altKey,
70 + target,
71 + timeStamp,
72 + type: 'contextmenu',
73 + x: nativeEvent.clientX,
74 + y: nativeEvent.clientY,
75 + };
76 +
77 + context.dispatchEvent(gestureState, props.onContextMenu, DiscreteEvent);
78 +}
79 +
80 +const contextMenuImpl = {
81 + targetEventTypes: hasPointerEvents
82 + ? ['contextmenu_active', 'pointerdown']
83 + : ['contextmenu_active', 'touchstart', 'mousedown'],
84 + getInitialState(): ContextMenuState {
85 + return {
86 + pointerType: '',
87 + };
88 + },
89 + onEvent(
90 + event: ReactDOMResponderEvent,
91 + context: ReactDOMResponderContext,
92 + props: ContextMenuProps,
93 + state: ContextMenuState,
94 + ): void {
95 + const nativeEvent: any = event.nativeEvent;
96 + const pointerType = event.pointerType;
97 + const type = event.type;
98 +
99 + if (props.disabled) {
100 + return;
101 + }
102 +
103 + if (type === 'contextmenu') {
104 + const onContextMenu = props.onContextMenu;
105 + const preventDefault = props.preventDefault;
106 + if (preventDefault !== false && !nativeEvent.defaultPrevented) {
107 + nativeEvent.preventDefault();
108 + }
109 + if (typeof onContextMenu === 'function') {
110 + dispatchContextMenuEvent(event, context, props, state);
111 + }
112 + state.pointerType = '';
113 + } else {
114 + state.pointerType = pointerType;
115 + }
116 + },
117 +};
118 +
119 +export const ContextMenuResponder = React.unstable_createResponder(
120 + 'ContextMenu',
121 + contextMenuImpl,
122 +);
123 +
124 +export function useContextMenuResponder(
125 + props: ContextMenuProps,
126 +): ReactEventResponderListener<any, any> {
127 + return React.unstable_useResponder(ContextMenuResponder, props);
128 +}
packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js new
+240
@@ -0,0 +1,240 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +let ReactFeatureFlags;
14 +let ReactDOM;
15 +let useContextMenuResponder;
16 +
17 +function createEvent(type, data) {
18 + const event = document.createEvent('CustomEvent');
19 + event.initCustomEvent(type, true, true);
20 + if (data != null) {
21 + Object.entries(data).forEach(([key, value]) => {
22 + event[key] = value;
23 + });
24 + }
25 + return event;
26 +}
27 +
28 +function init(hasPointerEvents) {
29 + global.PointerEvents = hasPointerEvents ? function() {} : undefined;
30 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
31 + ReactFeatureFlags.enableFlareAPI = true;
32 + React = require('react');
33 + ReactDOM = require('react-dom');
34 + useContextMenuResponder = require('react-events/context-menu')
35 + .useContextMenuResponder;
36 +}
37 +
38 +const platformGetter = jest.spyOn(global.navigator, 'platform', 'get');
39 +function setPlatform(platform: 'mac' | 'windows') {
40 + jest.resetModules();
41 + switch (platform) {
42 + case 'mac': {
43 + platformGetter.mockReturnValue('MacIntel');
44 + break;
45 + }
46 + case 'windows': {
47 + platformGetter.mockReturnValue('Win32');
48 + break;
49 + }
50 + default: {
51 + break;
52 + }
53 + }
54 + init();
55 +}
56 +
57 +function clearPlatform() {
58 + platformGetter.mockClear();
59 +}
60 +
61 +function dispatchContextMenuEvents(ref, options) {
62 + const preventDefault = options.preventDefault || function() {};
63 + const variant = (options.variant: 'mouse' | 'touch' | 'modified');
64 + const dispatchEvent = arg => ref.current.dispatchEvent(arg);
65 +
66 + if (variant === 'mouse') {
67 + // right-click
68 + dispatchEvent(
69 + createEvent('pointerdown', {pointerType: 'mouse', button: 2}),
70 + );
71 + dispatchEvent(createEvent('mousedown', {button: 2}));
72 + dispatchEvent(createEvent('contextmenu', {button: 2, preventDefault}));
73 + } else if (variant === 'modified') {
74 + // left-click + ctrl
75 + dispatchEvent(
76 + createEvent('pointerdown', {pointerType: 'mouse', button: 0}),
77 + );
78 + dispatchEvent(createEvent('mousedown', {button: 0}));
79 + if (global.navigator.platform === 'MacIntel') {
80 + dispatchEvent(
81 + createEvent('contextmenu', {button: 0, ctrlKey: true, preventDefault}),
82 + );
83 + }
84 + } else if (variant === 'touch') {
85 + // long-press
86 + dispatchEvent(
87 + createEvent('pointerdown', {pointerType: 'touch', button: 0}),
88 + );
89 + dispatchEvent(
90 + createEvent('touchstart', {
91 + changedTouches: [],
92 + targetTouches: [],
93 + }),
94 + );
95 + dispatchEvent(createEvent('contextmenu', {button: 0, preventDefault}));
96 + }
97 +}
98 +
99 +const forcePointerEvents = true;
100 +
101 +describe.each([[forcePointerEvents], [!forcePointerEvents]])(
102 + 'ContextMenu responder',
103 + hasPointerEvents => {
104 + let container;
105 +
106 + beforeEach(() => {
107 + jest.resetModules();
108 + init(hasPointerEvents);
109 + container = document.createElement('div');
110 + document.body.appendChild(container);
111 + });
112 +
113 + afterEach(() => {
114 + ReactDOM.render(null, container);
115 + document.body.removeChild(container);
116 + container = null;
117 + });
118 +
119 + describe('all platforms', () => {
120 + it('mouse right-click', () => {
121 + const onContextMenu = jest.fn();
122 + const preventDefault = jest.fn();
123 + const ref = React.createRef();
124 + const Component = () => {
125 + const listener = useContextMenuResponder({onContextMenu});
126 + return <div ref={ref} listeners={listener} />;
127 + };
128 + ReactDOM.render(<Component />, container);
129 +
130 + dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault});
131 + expect(preventDefault).toHaveBeenCalledTimes(1);
132 + expect(onContextMenu).toHaveBeenCalledTimes(1);
133 + expect(onContextMenu).toHaveBeenCalledWith(
134 + expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
135 + );
136 + });
137 +
138 + it('touch long-press', () => {
139 + const onContextMenu = jest.fn();
140 + const preventDefault = jest.fn();
141 + const ref = React.createRef();
142 + const Component = () => {
143 + const listener = useContextMenuResponder({onContextMenu});
144 + return <div ref={ref} listeners={listener} />;
145 + };
146 + ReactDOM.render(<Component />, container);
147 +
148 + dispatchContextMenuEvents(ref, {variant: 'touch', preventDefault});
149 + expect(preventDefault).toHaveBeenCalledTimes(1);
150 + expect(onContextMenu).toHaveBeenCalledTimes(1);
151 + expect(onContextMenu).toHaveBeenCalledWith(
152 + expect.objectContaining({pointerType: 'touch', type: 'contextmenu'}),
153 + );
154 + });
155 +
156 + it('"disabled" is true', () => {
157 + const onContextMenu = jest.fn();
158 + const ref = React.createRef();
159 + const Component = () => {
160 + const listener = useContextMenuResponder({
161 + onContextMenu,
162 + disabled: true,
163 + });
164 + return <div ref={ref} listeners={listener} />;
165 + };
166 + ReactDOM.render(<Component />, container);
167 +
168 + dispatchContextMenuEvents(ref, 'mouse');
169 + expect(onContextMenu).toHaveBeenCalledTimes(0);
170 + });
171 +
172 + it('"preventDefault" is false', () => {
173 + const preventDefault = jest.fn();
174 + const onContextMenu = jest.fn();
175 + const ref = React.createRef();
176 + const Component = () => {
177 + const listener = useContextMenuResponder({
178 + onContextMenu,
179 + preventDefault: false,
180 + });
181 + return <div ref={ref} listeners={listener} />;
182 + };
183 + ReactDOM.render(<Component />, container);
184 +
185 + dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault});
186 + expect(preventDefault).toHaveBeenCalledTimes(0);
187 + expect(onContextMenu).toHaveBeenCalledTimes(1);
188 + });
189 + });
190 +
191 + describe('mac platform', () => {
192 + beforeEach(() => {
193 + setPlatform('mac');
194 + });
195 +
196 + afterEach(() => {
197 + clearPlatform();
198 + });
199 +
200 + it('mouse modified left-click', () => {
201 + const onContextMenu = jest.fn();
202 + const ref = React.createRef();
203 + const Component = () => {
204 + const listener = useContextMenuResponder({onContextMenu});
205 + return <div ref={ref} listeners={listener} />;
206 + };
207 + ReactDOM.render(<Component />, container);
208 +
209 + dispatchContextMenuEvents(ref, {variant: 'modified'});
210 + expect(onContextMenu).toHaveBeenCalledTimes(1);
211 + expect(onContextMenu).toHaveBeenCalledWith(
212 + expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
213 + );
214 + });
215 + });
216 +
217 + describe('windows platform', () => {
218 + beforeEach(() => {
219 + setPlatform('windows');
220 + });
221 +
222 + afterEach(() => {
223 + clearPlatform();
224 + });
225 +
226 + it('mouse modified left-click', () => {
227 + const onContextMenu = jest.fn();
228 + const ref = React.createRef();
229 + const Component = () => {
230 + const listener = useContextMenuResponder({onContextMenu});
231 + return <div ref={ref} listeners={listener} />;
232 + };
233 + ReactDOM.render(<Component />, container);
234 +
235 + dispatchContextMenuEvents(ref, {variant: 'modified'});
236 + expect(onContextMenu).toHaveBeenCalledTimes(0);
237 + });
238 + });
239 + },
240 +);
scripts/rollup/bundles.js
+28 -12
@@ -419,6 +419,7 @@ const bundles = [
419 },
420
421 /******* React Events (experimental) *******/
422 +
423 {
424 bundleTypes: [
425 UMD_DEV,
@@ -429,8 +430,8 @@ const bundles = [
430 FB_WWW_PROD,
431 ],
432 moduleType: NON_FIBER_RENDERER,
432 - entry: 'react-events/press',
433 - global: 'ReactEventsPress',
433 + entry: 'react-events/context-menu',
434 + global: 'ReactEventsContextMenu',
435 externals: ['react'],
436 },
437
@@ -444,8 +445,8 @@ const bundles = [
445 FB_WWW_PROD,
446 ],
447 moduleType: NON_FIBER_RENDERER,
447 - entry: 'react-events/hover',
448 - global: 'ReactEventsHover',
448 + entry: 'react-events/drag',
449 + global: 'ReactEventsDrag',
450 externals: ['react'],
451 },
452
@@ -474,8 +475,8 @@ const bundles = [
475 FB_WWW_PROD,
476 ],
477 moduleType: NON_FIBER_RENDERER,
477 - entry: 'react-events/swipe',
478 - global: 'ReactEventsSwipe',
478 + entry: 'react-events/hover',
479 + global: 'ReactEventsHover',
480 externals: ['react'],
481 },
482
@@ -489,8 +490,8 @@ const bundles = [
490 FB_WWW_PROD,
491 ],
492 moduleType: NON_FIBER_RENDERER,
492 - entry: 'react-events/drag',
493 - global: 'ReactEventsDrag',
493 + entry: 'react-events/input',
494 + global: 'ReactEventsInput',
495 externals: ['react'],
496 },
497
@@ -504,8 +505,23 @@ const bundles = [
505 FB_WWW_PROD,
506 ],
507 moduleType: NON_FIBER_RENDERER,
507 - entry: 'react-events/input',
508 - global: 'ReactEventsInput',
508 + entry: 'react-events/keyboard',
509 + global: 'ReactEventsKeyboard',
510 + externals: ['react'],
511 + },
512 +
513 + {
514 + bundleTypes: [
515 + UMD_DEV,
516 + UMD_PROD,
517 + NODE_DEV,
518 + NODE_PROD,
519 + FB_WWW_DEV,
520 + FB_WWW_PROD,
521 + ],
522 + moduleType: NON_FIBER_RENDERER,
523 + entry: 'react-events/press',
524 + global: 'ReactEventsPress',
525 externals: ['react'],
526 },
527
@@ -534,8 +550,8 @@ const bundles = [
550 FB_WWW_PROD,
551 ],
552 moduleType: NON_FIBER_RENDERER,
537 - entry: 'react-events/keyboard',
538 - global: 'ReactEventsKeyboard',
553 + entry: 'react-events/swipe',
554 + global: 'ReactEventsSwipe',
555 externals: ['react'],
556 },
557 ];