@samitouri / QOS-React-2 / commits / 73c27d8b49

[Flare] Add basic Scroll event responder module (#15827)

Dominic Gannaway committed Jun 5, 2019 at 18:20 UTC 73c27d8b4987aecc9e960f29b329c3ac8f46cff3
6 files changed +389
packages/react-events/npm/scroll.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/react-events-scroll.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/react-events-scroll.development.js');
7 +}
packages/react-events/package.json
+1
@@ -17,6 +17,7 @@
17 "focus.js",
18 "swipe.js",
19 "drag.js",
20 + "scroll.js",
21 "focus-scope.js",
22 "index.js",
23 "build-info.json",
packages/react-events/scroll.js new
+14
@@ -0,0 +1,14 @@
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 +const Scroll = require('./src/Scroll');
13 +
14 +module.exports = Scroll.default || Scroll;
packages/react-events/src/Scroll.js new
+218
@@ -0,0 +1,218 @@
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 + ReactResponderEvent,
12 + ReactResponderContext,
13 +} from 'shared/ReactTypes';
14 +import {UserBlockingEvent} from 'shared/ReactTypes';
15 +import type {EventPriority} from 'shared/ReactTypes';
16 +
17 +import React from 'react';
18 +
19 +type ScrollProps = {
20 + disabled: boolean,
21 + onScroll: ScrollEvent => void,
22 + onScrollDragStart: ScrollEvent => void,
23 + onScrollDragEnd: ScrollEvent => void,
24 + onScrollMomentumStart: ScrollEvent => void,
25 + onScrollMomentumEnd: ScrollEvent => void,
26 +};
27 +
28 +type ScrollState = {
29 + pointerType: PointerType,
30 + scrollTarget: null | Element | Document,
31 + isPointerDown: boolean,
32 +};
33 +
34 +type ScrollEventType =
35 + | 'scroll'
36 + | 'scrolldragstart'
37 + | 'scrolldragend'
38 + | 'scrollmomentumstart'
39 + | 'scrollmomentumend';
40 +
41 +type PointerType = '' | 'mouse' | 'keyboard' | 'pen' | 'touch';
42 +
43 +type ScrollDirection = '' | 'up' | 'down' | 'left' | 'right';
44 +
45 +type ScrollEvent = {|
46 + direction: ScrollDirection,
47 + target: Element | Document,
48 + type: ScrollEventType,
49 + pointerType: PointerType,
50 + timeStamp: number,
51 + clientX: null | number,
52 + clientY: null | number,
53 + pageX: null | number,
54 + pageY: null | number,
55 + screenX: null | number,
56 + screenY: null | number,
57 + x: null | number,
58 + y: null | number,
59 +|};
60 +
61 +const targetEventTypes = ['scroll', 'pointerdown', 'keyup'];
62 +const rootEventTypes = ['pointermove', 'pointerup', 'pointercancel'];
63 +
64 +function createScrollEvent(
65 + event: ?ReactResponderEvent,
66 + context: ReactResponderContext,
67 + type: ScrollEventType,
68 + target: Element | Document,
69 + pointerType: PointerType,
70 +): ScrollEvent {
71 + let clientX = null;
72 + let clientY = null;
73 + let pageX = null;
74 + let pageY = null;
75 + let screenX = null;
76 + let screenY = null;
77 +
78 + if (event) {
79 + const nativeEvent = (event.nativeEvent: any);
80 + ({clientX, clientY, pageX, pageY, screenX, screenY} = nativeEvent);
81 + }
82 +
83 + return {
84 + target,
85 + type,
86 + pointerType,
87 + direction: '', // TODO
88 + timeStamp: context.getTimeStamp(),
89 + clientX,
90 + clientY,
91 + pageX,
92 + pageY,
93 + screenX,
94 + screenY,
95 + x: clientX,
96 + y: clientY,
97 + };
98 +}
99 +
100 +function dispatchEvent(
101 + event: ?ReactResponderEvent,
102 + context: ReactResponderContext,
103 + state: ScrollState,
104 + name: ScrollEventType,
105 + listener: (e: Object) => void,
106 + eventPriority: EventPriority,
107 +): void {
108 + const target = ((state.scrollTarget: any): Element | Document);
109 + const pointerType = state.pointerType;
110 + const syntheticEvent = createScrollEvent(
111 + event,
112 + context,
113 + name,
114 + target,
115 + pointerType,
116 + );
117 + context.dispatchEvent(syntheticEvent, listener, eventPriority);
118 +}
119 +
120 +const ScrollResponder = {
121 + targetEventTypes,
122 + createInitialState() {
123 + return {
124 + pointerType: '',
125 + scrollTarget: null,
126 + isPointerDown: false,
127 + };
128 + },
129 + allowMultipleHostChildren: true,
130 + stopLocalPropagation: true,
131 + onEvent(
132 + event: ReactResponderEvent,
133 + context: ReactResponderContext,
134 + props: ScrollProps,
135 + state: ScrollState,
136 + ): void {
137 + const {target, type} = event;
138 +
139 + if (props.disabled) {
140 + if (state.isPointerDown) {
141 + state.isPointerDown = false;
142 + state.scrollTarget = null;
143 + context.addRootEventTypes(rootEventTypes);
144 + }
145 + return;
146 + }
147 + const pointerType = context.getEventPointerType(event);
148 +
149 + switch (type) {
150 + case 'scroll': {
151 + state.scrollTarget = ((target: any): Element | Document);
152 + if (props.onScroll) {
153 + dispatchEvent(
154 + event,
155 + context,
156 + state,
157 + 'scroll',
158 + props.onScroll,
159 + UserBlockingEvent,
160 + );
161 + }
162 + break;
163 + }
164 + case 'keyup': {
165 + state.pointerType = pointerType;
166 + break;
167 + }
168 + case 'pointerdown': {
169 + state.pointerType = pointerType;
170 + if (!state.isPointerDown) {
171 + state.isPointerDown = true;
172 + context.addRootEventTypes(rootEventTypes);
173 + }
174 + break;
175 + }
176 + }
177 + },
178 + onRootEvent(
179 + event: ReactResponderEvent,
180 + context: ReactResponderContext,
181 + props: ScrollProps,
182 + state: ScrollState,
183 + ) {
184 + const {type} = event;
185 + const pointerType = context.getEventPointerType(event);
186 +
187 + switch (type) {
188 + case 'pointercancel':
189 + case 'pointerup': {
190 + state.pointerType = pointerType;
191 + if (state.isPointerDown) {
192 + state.isPointerDown = false;
193 + context.removeRootEventTypes(rootEventTypes);
194 + }
195 + break;
196 + }
197 + case 'pointermove': {
198 + state.pointerType = pointerType;
199 + }
200 + }
201 + },
202 + onUnmount(
203 + context: ReactResponderContext,
204 + props: ScrollProps,
205 + state: ScrollState,
206 + ) {
207 + // TODO
208 + },
209 + onOwnershipChange(
210 + context: ReactResponderContext,
211 + props: ScrollProps,
212 + state: ScrollState,
213 + ) {
214 + // TODO
215 + },
216 +};
217 +
218 +export default React.unstable_createEventComponent(ScrollResponder, 'Scroll');
packages/react-events/src/__tests__/Scroll-test.internal.js new
+134
@@ -0,0 +1,134 @@
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 Scroll;
16 +
17 +const 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 +describe('Scroll event responder', () => {
29 + let container;
30 +
31 + beforeEach(() => {
32 + jest.resetModules();
33 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
34 + ReactFeatureFlags.enableEventAPI = true;
35 + React = require('react');
36 + ReactDOM = require('react-dom');
37 + Scroll = require('react-events/scroll');
38 +
39 + container = document.createElement('div');
40 + document.body.appendChild(container);
41 + });
42 +
43 + afterEach(() => {
44 + ReactDOM.render(null, container);
45 + document.body.removeChild(container);
46 + container = null;
47 + });
48 +
49 + describe('disabled', () => {
50 + let onScroll, ref;
51 +
52 + beforeEach(() => {
53 + onScroll = jest.fn();
54 + ref = React.createRef();
55 + const element = (
56 + <Scroll disabled={true} onScroll={onScroll}>
57 + <div ref={ref} />
58 + </Scroll>
59 + );
60 + ReactDOM.render(element, container);
61 + });
62 +
63 + it('prevents custom events being dispatched', () => {
64 + ref.current.dispatchEvent(createEvent('scroll'));
65 + expect(onScroll).not.toBeCalled();
66 + });
67 + });
68 +
69 + describe('onScroll', () => {
70 + let onScroll, ref;
71 +
72 + beforeEach(() => {
73 + onScroll = jest.fn();
74 + ref = React.createRef();
75 + const element = (
76 + <Scroll onScroll={onScroll}>
77 + <div ref={ref} />
78 + </Scroll>
79 + );
80 + ReactDOM.render(element, container);
81 + });
82 +
83 + describe('is called after "scroll" event', () => {
84 + it('with a mouse pointerType', () => {
85 + ref.current.dispatchEvent(
86 + createEvent('pointerdown', {
87 + pointerType: 'mouse',
88 + }),
89 + );
90 + ref.current.dispatchEvent(createEvent('scroll'));
91 + expect(onScroll).toHaveBeenCalledTimes(1);
92 + expect(onScroll).toHaveBeenCalledWith(
93 + expect.objectContaining({pointerType: 'mouse', type: 'scroll'}),
94 + );
95 + });
96 +
97 + it('with a touch pointerType', () => {
98 + ref.current.dispatchEvent(
99 + createEvent('pointerdown', {
100 + pointerType: 'touch',
101 + }),
102 + );
103 + ref.current.dispatchEvent(createEvent('scroll'));
104 + expect(onScroll).toHaveBeenCalledTimes(1);
105 + expect(onScroll).toHaveBeenCalledWith(
106 + expect.objectContaining({pointerType: 'touch', type: 'scroll'}),
107 + );
108 + });
109 +
110 + it('with a pen pointerType', () => {
111 + ref.current.dispatchEvent(
112 + createEvent('pointerdown', {
113 + pointerType: 'pen',
114 + }),
115 + );
116 + ref.current.dispatchEvent(createEvent('scroll'));
117 + expect(onScroll).toHaveBeenCalledTimes(1);
118 + expect(onScroll).toHaveBeenCalledWith(
119 + expect.objectContaining({pointerType: 'pen', type: 'scroll'}),
120 + );
121 + });
122 +
123 + it('with a keyboard pointerType', () => {
124 + ref.current.dispatchEvent(createEvent('keydown'));
125 + ref.current.dispatchEvent(createEvent('keyup'));
126 + ref.current.dispatchEvent(createEvent('scroll'));
127 + expect(onScroll).toHaveBeenCalledTimes(1);
128 + expect(onScroll).toHaveBeenCalledWith(
129 + expect.objectContaining({pointerType: 'keyboard', type: 'scroll'}),
130 + );
131 + });
132 + });
133 + });
134 +});
scripts/rollup/bundles.js
+15
@@ -542,6 +542,21 @@ const bundles = [
542 global: 'ReactEventsDrag',
543 externals: ['react'],
544 },
545 +
546 + {
547 + bundleTypes: [
548 + UMD_DEV,
549 + UMD_PROD,
550 + NODE_DEV,
551 + NODE_PROD,
552 + FB_WWW_DEV,
553 + FB_WWW_PROD,
554 + ],
555 + moduleType: NON_FIBER_RENDERER,
556 + entry: 'react-events/scroll',
557 + global: 'ReactEventsScroll',
558 + externals: ['react'],
559 + },
560 ];
561
562 // Based on deep-freeze by substack (public domain)