Skip to content

Commit 6b946ad

Browse files
authored
[Flare] Add more functionality to Scroll event resonder (#16036)
1 parent a457e02 commit 6b946ad

2 files changed

Lines changed: 249 additions & 27 deletions

File tree

packages/react-events/src/dom/Scroll.js

Lines changed: 102 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@ type ScrollProps = {
2323
onScroll: ScrollEvent => void,
2424
onScrollDragStart: ScrollEvent => void,
2525
onScrollDragEnd: ScrollEvent => void,
26-
onScrollMomentumStart: ScrollEvent => void,
27-
onScrollMomentumEnd: ScrollEvent => void,
2826
};
2927

3028
type ScrollState = {
29+
direction: ScrollDirection,
3130
pointerType: PointerType,
3231
scrollTarget: null | Element | Document,
33-
isPointerDown: boolean,
32+
isDragging: boolean,
33+
isTouching: boolean,
34+
scrollLeft: number,
35+
scrollTop: number,
3436
};
3537

3638
type ScrollEventType =
@@ -58,15 +60,22 @@ type ScrollEvent = {|
5860
y: null | number,
5961
|};
6062

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'];
6371

6472
function createScrollEvent(
6573
event: ?ReactDOMResponderEvent,
6674
context: ReactDOMResponderContext,
6775
type: ScrollEventType,
6876
target: Element | Document,
6977
pointerType: PointerType,
78+
direction: ScrollDirection,
7079
): ScrollEvent {
7180
let clientX = null;
7281
let clientY = null;
@@ -84,7 +93,7 @@ function createScrollEvent(
8493
target,
8594
type,
8695
pointerType,
87-
direction: '', // TODO
96+
direction,
8897
timeStamp: context.getTimeStamp(),
8998
clientX,
9099
clientY,
@@ -107,12 +116,14 @@ function dispatchEvent(
107116
): void {
108117
const target = ((state.scrollTarget: any): Element | Document);
109118
const pointerType = state.pointerType;
119+
const direction = state.direction;
110120
const syntheticEvent = createScrollEvent(
111121
event,
112122
context,
113123
name,
114124
target,
115125
pointerType,
126+
direction,
116127
);
117128
context.dispatchEvent(syntheticEvent, listener, eventPriority);
118129
}
@@ -122,9 +133,12 @@ const ScrollResponder: ReactDOMEventResponder = {
122133
targetEventTypes,
123134
createInitialState() {
124135
return {
136+
direction: '',
137+
isTouching: false,
125138
pointerType: '',
139+
prevScrollTop: 0,
140+
prevScrollLeft: 0,
126141
scrollTarget: null,
127-
isPointerDown: false,
128142
};
129143
},
130144
allowMultipleHostChildren: true,
@@ -138,17 +152,68 @@ const ScrollResponder: ReactDOMEventResponder = {
138152
const {pointerType, target, type} = event;
139153

140154
if (props.disabled) {
141-
if (state.isPointerDown) {
142-
state.isPointerDown = false;
155+
if (state.isTouching) {
156+
state.isTouching = false;
143157
state.scrollTarget = null;
144-
context.addRootEventTypes(rootEventTypes);
158+
state.isDragging = false;
159+
state.direction = '';
160+
context.removeRootEventTypes(rootEventTypes);
145161
}
146162
return;
147163
}
148164

149165
switch (type) {
150166
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+
}
151200
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+
}
152217
if (props.onScroll) {
153218
dispatchEvent(
154219
event,
@@ -165,13 +230,19 @@ const ScrollResponder: ReactDOMEventResponder = {
165230
state.pointerType = pointerType;
166231
break;
167232
}
233+
case 'wheel': {
234+
state.pointerType = 'mouse';
235+
break;
236+
}
168237
case 'pointerdown': {
169238
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;
172244
context.addRootEventTypes(rootEventTypes);
173245
}
174-
break;
175246
}
176247
}
177248
},
@@ -181,20 +252,28 @@ const ScrollResponder: ReactDOMEventResponder = {
181252
props: ScrollProps,
182253
state: ScrollState,
183254
) {
184-
const {pointerType, type} = event;
255+
const {type} = event;
185256

186257
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 = '';
192275
context.removeRootEventTypes(rootEventTypes);
193276
}
194-
break;
195-
}
196-
case 'pointermove': {
197-
state.pointerType = pointerType;
198277
}
199278
}
200279
},

packages/react-events/src/dom/__tests__/Scroll-test.internal.js

Lines changed: 147 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,31 @@ describe('Scroll event responder', () => {
9090
ref.current.dispatchEvent(createEvent('scroll'));
9191
expect(onScroll).toHaveBeenCalledTimes(1);
9292
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+
}),
94118
);
95119
});
96120

@@ -103,7 +127,31 @@ describe('Scroll event responder', () => {
103127
ref.current.dispatchEvent(createEvent('scroll'));
104128
expect(onScroll).toHaveBeenCalledTimes(1);
105129
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+
}),
107155
);
108156
});
109157

@@ -116,7 +164,31 @@ describe('Scroll event responder', () => {
116164
ref.current.dispatchEvent(createEvent('scroll'));
117165
expect(onScroll).toHaveBeenCalledTimes(1);
118166
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+
}),
120192
);
121193
});
122194

@@ -134,9 +206,80 @@ describe('Scroll event responder', () => {
134206
ref.current.dispatchEvent(createEvent('scroll'));
135207
expect(onScroll).toHaveBeenCalledTimes(1);
136208
expect(onScroll).toHaveBeenCalledWith(
137-
expect.objectContaining({pointerType: 'keyboard', type: 'scroll'}),
209+
expect.objectContaining({
210+
pointerType: 'keyboard',
211+
type: 'scroll',
212+
direction: '',
213+
}),
138214
);
139215
});
140216
});
141217
});
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+
});
142285
});

0 commit comments

Comments
 (0)