[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)