[Flare] Add more functionality to Scroll event resonder (#16036)
Dominic Gannaway committed
Jul 2, 2019 at 23:51 UTC
6b946ad9daa39fbb4959e34060d58393f46f0d0c
2 files changed
+249
-27
packages/react-events/src/dom/Scroll.js
+102
-23
@@ -23,14 +23,16 @@ type ScrollProps = {
23
onScroll: ScrollEvent => void,
24
onScrollDragStart: ScrollEvent => void,
25
onScrollDragEnd: ScrollEvent => void,
26
- onScrollMomentumStart: ScrollEvent => void,
27
- onScrollMomentumEnd: ScrollEvent => void,
26
};
27
28
type ScrollState = {
29
+ direction: ScrollDirection,
30
pointerType: PointerType,
31
scrollTarget: null | Element | Document,
33
- isPointerDown: boolean,
32
+ isDragging: boolean,
33
+ isTouching: boolean,
34
+ scrollLeft: number,
35
+ scrollTop: number,
36
};
37
38
type ScrollEventType =
@@ -58,8 +60,14 @@ type ScrollEvent = {|
60
y: null | number,
61
|};
62
61
-const targetEventTypes = ['scroll', 'pointerdown', 'keyup'];
62
-const rootEventTypes = ['pointermove', 'pointerup', 'pointercancel'];
63
+const targetEventTypes = [
64
+ 'scroll',
65
+ 'pointerdown',
66
+ 'touchstart',
67
+ 'keyup',
68
+ 'wheel',
69
+];
70
+const rootEventTypes = ['touchcancel', 'touchend'];
71
72
function createScrollEvent(
73
event: ?ReactDOMResponderEvent,
@@ -67,6 +75,7 @@ function createScrollEvent(
75
type: ScrollEventType,
76
target: Element | Document,
77
pointerType: PointerType,
78
+ direction: ScrollDirection,
79
): ScrollEvent {
80
let clientX = null;
81
let clientY = null;
@@ -84,7 +93,7 @@ function createScrollEvent(
93
target,
94
type,
95
pointerType,
87
- direction: '', // TODO
96
+ direction,
97
timeStamp: context.getTimeStamp(),
98
clientX,
99
clientY,
@@ -107,12 +116,14 @@ function dispatchEvent(
116
): void {
117
const target = ((state.scrollTarget: any): Element | Document);
118
const pointerType = state.pointerType;
119
+ const direction = state.direction;
120
const syntheticEvent = createScrollEvent(
121
event,
122
context,
123
name,
124
target,
125
pointerType,
126
+ direction,
127
);
128
context.dispatchEvent(syntheticEvent, listener, eventPriority);
129
}
@@ -122,9 +133,12 @@ const ScrollResponder: ReactDOMEventResponder = {
133
targetEventTypes,
134
createInitialState() {
135
return {
136
+ direction: '',
137
+ isTouching: false,
138
pointerType: '',
139
+ prevScrollTop: 0,
140
+ prevScrollLeft: 0,
141
scrollTarget: null,
127
- isPointerDown: false,
142
};
143
},
144
allowMultipleHostChildren: true,
@@ -138,17 +152,68 @@ const ScrollResponder: ReactDOMEventResponder = {
152
const {pointerType, target, type} = event;
153
154
if (props.disabled) {
141
- if (state.isPointerDown) {
142
- state.isPointerDown = false;
155
+ if (state.isTouching) {
156
+ state.isTouching = false;
157
state.scrollTarget = null;
144
- context.addRootEventTypes(rootEventTypes);
158
+ state.isDragging = false;
159
+ state.direction = '';
160
+ context.removeRootEventTypes(rootEventTypes);
161
}
162
return;
163
}
164
165
switch (type) {
166
case 'scroll': {
167
+ const prevScrollTarget = state.scrollTarget;
168
+ let scrollLeft = 0;
169
+ let scrollTop = 0;
170
+
171
+ // Check if target is the document
172
+ if (target.nodeType === 9) {
173
+ const bodyNode = ((target: any): Document).body;
174
+ if (bodyNode !== null) {
175
+ scrollLeft = bodyNode.offsetLeft;
176
+ scrollTop = bodyNode.offsetTop;
177
+ }
178
+ } else {
179
+ scrollLeft = ((target: any): Element).scrollLeft;
180
+ scrollTop = ((target: any): Element).scrollTop;
181
+ }
182
+
183
+ if (prevScrollTarget !== null) {
184
+ if (scrollTop === state.scrollTop) {
185
+ if (scrollLeft > state.scrollLeft) {
186
+ state.direction = 'right';
187
+ } else {
188
+ state.direction = 'left';
189
+ }
190
+ } else {
191
+ if (scrollTop > state.scrollTop) {
192
+ state.direction = 'down';
193
+ } else {
194
+ state.direction = 'up';
195
+ }
196
+ }
197
+ } else {
198
+ state.direction = '';
199
+ }
200
state.scrollTarget = ((target: any): Element | Document);
201
+ state.scrollLeft = scrollLeft;
202
+ state.scrollTop = scrollTop;
203
+
204
+ if (state.isTouching && !state.isDragging) {
205
+ state.isDragging = true;
206
+ if (props.onScrollDragStart) {
207
+ dispatchEvent(
208
+ event,
209
+ context,
210
+ state,
211
+ 'scrolldragstart',
212
+ props.onScrollDragStart,
213
+ UserBlockingEvent,
214
+ );
215
+ }
216
+ }
217
if (props.onScroll) {
218
dispatchEvent(
219
event,
@@ -165,13 +230,19 @@ const ScrollResponder: ReactDOMEventResponder = {
230
state.pointerType = pointerType;
231
break;
232
}
233
+ case 'wheel': {
234
+ state.pointerType = 'mouse';
235
+ break;
236
+ }
237
case 'pointerdown': {
238
state.pointerType = pointerType;
170
- if (!state.isPointerDown) {
171
- state.isPointerDown = true;
239
+ break;
240
+ }
241
+ case 'touchstart': {
242
+ if (!state.isTouching) {
243
+ state.isTouching = true;
244
context.addRootEventTypes(rootEventTypes);
245
}
174
- break;
246
}
247
}
248
},
@@ -181,20 +252,28 @@ const ScrollResponder: ReactDOMEventResponder = {
252
props: ScrollProps,
253
state: ScrollState,
254
) {
184
- const {pointerType, type} = event;
255
+ const {type} = event;
256
257
switch (type) {
187
- case 'pointercancel':
188
- case 'pointerup': {
189
- state.pointerType = pointerType;
190
- if (state.isPointerDown) {
191
- state.isPointerDown = false;
258
+ case 'touchcancel':
259
+ case 'touchend': {
260
+ if (state.isTouching) {
261
+ if (state.isDragging && props.onScrollDragEnd) {
262
+ dispatchEvent(
263
+ event,
264
+ context,
265
+ state,
266
+ 'scrolldragend',
267
+ props.onScrollDragEnd,
268
+ UserBlockingEvent,
269
+ );
270
+ }
271
+ state.isTouching = false;
272
+ state.isDragging = false;
273
+ state.scrollTarget = null;
274
+ state.pointerType = '';
275
context.removeRootEventTypes(rootEventTypes);
276
}
194
- break;
195
- }
196
- case 'pointermove': {
197
- state.pointerType = pointerType;
277
}
278
}
279
},
packages/react-events/src/dom/__tests__/Scroll-test.internal.js
+147
-4
@@ -90,7 +90,31 @@ describe('Scroll event responder', () => {
90
ref.current.dispatchEvent(createEvent('scroll'));
91
expect(onScroll).toHaveBeenCalledTimes(1);
92
expect(onScroll).toHaveBeenCalledWith(
93
- expect.objectContaining({pointerType: 'mouse', type: 'scroll'}),
93
+ expect.objectContaining({
94
+ pointerType: 'mouse',
95
+ type: 'scroll',
96
+ direction: '',
97
+ }),
98
+ );
99
+ onScroll.mockReset();
100
+ ref.current.scrollTop = -1;
101
+ ref.current.dispatchEvent(createEvent('scroll'));
102
+ expect(onScroll).toHaveBeenCalledWith(
103
+ expect.objectContaining({
104
+ pointerType: 'mouse',
105
+ type: 'scroll',
106
+ direction: 'up',
107
+ }),
108
+ );
109
+ onScroll.mockReset();
110
+ ref.current.scrollTop = 1;
111
+ ref.current.dispatchEvent(createEvent('scroll'));
112
+ expect(onScroll).toHaveBeenCalledWith(
113
+ expect.objectContaining({
114
+ pointerType: 'mouse',
115
+ type: 'scroll',
116
+ direction: 'down',
117
+ }),
118
);
119
});
120
@@ -103,7 +127,31 @@ describe('Scroll event responder', () => {
127
ref.current.dispatchEvent(createEvent('scroll'));
128
expect(onScroll).toHaveBeenCalledTimes(1);
129
expect(onScroll).toHaveBeenCalledWith(
106
- expect.objectContaining({pointerType: 'touch', type: 'scroll'}),
130
+ expect.objectContaining({
131
+ pointerType: 'touch',
132
+ type: 'scroll',
133
+ direction: '',
134
+ }),
135
+ );
136
+ onScroll.mockReset();
137
+ ref.current.scrollTop = -1;
138
+ ref.current.dispatchEvent(createEvent('scroll'));
139
+ expect(onScroll).toHaveBeenCalledWith(
140
+ expect.objectContaining({
141
+ pointerType: 'touch',
142
+ type: 'scroll',
143
+ direction: 'up',
144
+ }),
145
+ );
146
+ onScroll.mockReset();
147
+ ref.current.scrollTop = 1;
148
+ ref.current.dispatchEvent(createEvent('scroll'));
149
+ expect(onScroll).toHaveBeenCalledWith(
150
+ expect.objectContaining({
151
+ pointerType: 'touch',
152
+ type: 'scroll',
153
+ direction: 'down',
154
+ }),
155
);
156
});
157
@@ -116,7 +164,31 @@ describe('Scroll event responder', () => {
164
ref.current.dispatchEvent(createEvent('scroll'));
165
expect(onScroll).toHaveBeenCalledTimes(1);
166
expect(onScroll).toHaveBeenCalledWith(
119
- expect.objectContaining({pointerType: 'pen', type: 'scroll'}),
167
+ expect.objectContaining({
168
+ pointerType: 'pen',
169
+ type: 'scroll',
170
+ direction: '',
171
+ }),
172
+ );
173
+ onScroll.mockReset();
174
+ ref.current.scrollTop = -1;
175
+ ref.current.dispatchEvent(createEvent('scroll'));
176
+ expect(onScroll).toHaveBeenCalledWith(
177
+ expect.objectContaining({
178
+ pointerType: 'pen',
179
+ type: 'scroll',
180
+ direction: 'up',
181
+ }),
182
+ );
183
+ onScroll.mockReset();
184
+ ref.current.scrollTop = 1;
185
+ ref.current.dispatchEvent(createEvent('scroll'));
186
+ expect(onScroll).toHaveBeenCalledWith(
187
+ expect.objectContaining({
188
+ pointerType: 'pen',
189
+ type: 'scroll',
190
+ direction: 'down',
191
+ }),
192
);
193
});
194
@@ -134,9 +206,80 @@ describe('Scroll event responder', () => {
206
ref.current.dispatchEvent(createEvent('scroll'));
207
expect(onScroll).toHaveBeenCalledTimes(1);
208
expect(onScroll).toHaveBeenCalledWith(
137
- expect.objectContaining({pointerType: 'keyboard', type: 'scroll'}),
209
+ expect.objectContaining({
210
+ pointerType: 'keyboard',
211
+ type: 'scroll',
212
+ direction: '',
213
+ }),
214
);
215
});
216
});
217
});
218
+
219
+ describe('onScrollDragStart', () => {
220
+ let onScrollDragStart, ref;
221
+
222
+ beforeEach(() => {
223
+ onScrollDragStart = jest.fn();
224
+ ref = React.createRef();
225
+ const element = (
226
+ <Scroll onScrollDragStart={onScrollDragStart}>
227
+ <div ref={ref} />
228
+ </Scroll>
229
+ );
230
+ ReactDOM.render(element, container);
231
+ });
232
+
233
+ it('works as expected with touch events', () => {
234
+ ref.current.dispatchEvent(
235
+ createEvent('pointerdown', {
236
+ pointerType: 'touch',
237
+ }),
238
+ );
239
+ ref.current.dispatchEvent(createEvent('touchstart'));
240
+ ref.current.dispatchEvent(createEvent('scroll'));
241
+ expect(onScrollDragStart).toHaveBeenCalledTimes(1);
242
+ expect(onScrollDragStart).toHaveBeenCalledWith(
243
+ expect.objectContaining({
244
+ pointerType: 'touch',
245
+ type: 'scrolldragstart',
246
+ direction: '',
247
+ }),
248
+ );
249
+ });
250
+ });
251
+
252
+ describe('onScrollDragEnd', () => {
253
+ let onScrollDragEnd, ref;
254
+
255
+ beforeEach(() => {
256
+ onScrollDragEnd = jest.fn();
257
+ ref = React.createRef();
258
+ const element = (
259
+ <Scroll onScrollDragEnd={onScrollDragEnd}>
260
+ <div ref={ref} />
261
+ </Scroll>
262
+ );
263
+ ReactDOM.render(element, container);
264
+ });
265
+
266
+ it('works as expected with touch events', () => {
267
+ ref.current.dispatchEvent(
268
+ createEvent('pointerdown', {
269
+ pointerType: 'touch',
270
+ }),
271
+ );
272
+ ref.current.dispatchEvent(createEvent('touchstart'));
273
+ ref.current.dispatchEvent(createEvent('scroll'));
274
+ ref.current.dispatchEvent(createEvent('touchend'));
275
+ expect(onScrollDragEnd).toHaveBeenCalledTimes(1);
276
+ expect(onScrollDragEnd).toHaveBeenCalledWith(
277
+ expect.objectContaining({
278
+ pointerType: 'touch',
279
+ type: 'scrolldragend',
280
+ direction: '',
281
+ }),
282
+ );
283
+ });
284
+ });
285
});