@samitouri / QOS-React-2 / commits / 6b946ad9da

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