Skip to content

Commit 5a8f4a7

Browse files
fix: scroll to event seq (#1943)
1 parent 7e25db3 commit 5a8f4a7

8 files changed

Lines changed: 137 additions & 25 deletions

File tree

‎docs/api/user-event.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,13 +285,13 @@ The sequence of events depends on whether the scroll includes an optional moment
285285

286286
- `contentSizeChange`
287287
- `scrollBeginDrag`
288-
- `scroll` (multiple events)
288+
- `scroll` (zero or more events; none if the scroll offset does not change)
289289
- `scrollEndDrag`
290290

291291
**Momentum scroll (optional)**:
292292

293293
- `momentumScrollBegin`
294-
- `scroll` (multiple events)
294+
- `scroll` (zero or more events; none if the scroll offset does not change)
295295
- `momentumScrollEnd`
296296

297297
## `pullToRefresh()` \

‎src/user-event/scroll/__tests__/__snapshots__/scroll-to-flat-list.test.tsx.snap‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,46 @@ exports[`scrollTo() with FlatList supports vertical drag scroll: scrollTo({ y: 1
162162
"timeStamp": 0,
163163
},
164164
},
165+
{
166+
"name": "scroll",
167+
"payload": {
168+
"currentTarget": {},
169+
"isDefaultPrevented": [Function],
170+
"isPersistent": [Function],
171+
"isPropagationStopped": [Function],
172+
"nativeEvent": {
173+
"contentInset": {
174+
"bottom": 0,
175+
"left": 0,
176+
"right": 0,
177+
"top": 0,
178+
},
179+
"contentOffset": {
180+
"x": 0,
181+
"y": 100,
182+
},
183+
"contentSize": {
184+
"height": 0,
185+
"width": 0,
186+
},
187+
"layoutMeasurement": {
188+
"height": 0,
189+
"width": 0,
190+
},
191+
"responderIgnoreScroll": true,
192+
"target": 0,
193+
"velocity": {
194+
"x": 0,
195+
"y": 0,
196+
},
197+
},
198+
"persist": [Function],
199+
"preventDefault": [Function],
200+
"stopPropagation": [Function],
201+
"target": {},
202+
"timeStamp": 0,
203+
},
204+
},
165205
{
166206
"name": "scrollEndDrag",
167207
"payload": {

‎src/user-event/scroll/__tests__/__snapshots__/scroll-to.test.tsx.snap‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,46 @@ exports[`scrollTo() supports vertical drag scroll: scrollTo({ y: 100 }) 1`] = `
162162
"timeStamp": 0,
163163
},
164164
},
165+
{
166+
"name": "scroll",
167+
"payload": {
168+
"currentTarget": {},
169+
"isDefaultPrevented": [Function],
170+
"isPersistent": [Function],
171+
"isPropagationStopped": [Function],
172+
"nativeEvent": {
173+
"contentInset": {
174+
"bottom": 0,
175+
"left": 0,
176+
"right": 0,
177+
"top": 0,
178+
},
179+
"contentOffset": {
180+
"x": 0,
181+
"y": 100,
182+
},
183+
"contentSize": {
184+
"height": 0,
185+
"width": 0,
186+
},
187+
"layoutMeasurement": {
188+
"height": 0,
189+
"width": 0,
190+
},
191+
"responderIgnoreScroll": true,
192+
"target": 0,
193+
"velocity": {
194+
"x": 0,
195+
"y": 0,
196+
},
197+
},
198+
"persist": [Function],
199+
"preventDefault": [Function],
200+
"stopPropagation": [Function],
201+
"target": {},
202+
"timeStamp": 0,
203+
},
204+
},
165205
{
166206
"name": "scrollEndDrag",
167207
"payload": {

‎src/user-event/scroll/__tests__/scroll-to-flat-list.test.tsx‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async function renderFlatListWithToolkit(props: ScrollViewProps = {}) {
2323
const renderItem = (title: string) => <Text>{title}</Text>;
2424
await render(
2525
<FlatList
26-
testID="flatList"
26+
testID="flat-list"
2727
onScroll={logEvent('scroll')}
2828
onScrollBeginDrag={logEvent('scrollBeginDrag')}
2929
onScrollEndDrag={logEvent('scrollEndDrag')}
@@ -46,12 +46,13 @@ describe('scrollTo() with FlatList', () => {
4646
const { events } = await renderFlatListWithToolkit();
4747
const user = userEvent.setup();
4848

49-
await user.scrollTo(screen.getByTestId('flatList'), { y: 100 });
49+
await user.scrollTo(screen.getByTestId('flat-list'), { y: 100 });
5050
expect(mapEventsToShortForm(events)).toEqual([
5151
['scrollBeginDrag', 0, 0],
5252
['scroll', 25, 0],
5353
['scroll', 50, 0],
5454
['scroll', 75, 0],
55+
['scroll', 100, 0],
5556
['scrollEndDrag', 100, 0],
5657
]);
5758
expect(events).toMatchSnapshot('scrollTo({ y: 100 })');
@@ -61,12 +62,13 @@ describe('scrollTo() with FlatList', () => {
6162
const { events } = await renderFlatListWithToolkit({ horizontal: true });
6263
const user = userEvent.setup();
6364

64-
await user.scrollTo(screen.getByTestId('flatList'), { x: 100 });
65+
await user.scrollTo(screen.getByTestId('flat-list'), { x: 100 });
6566
expect(mapEventsToShortForm(events)).toEqual([
6667
['scrollBeginDrag', 0, 0],
6768
['scroll', 0, 25],
6869
['scroll', 0, 50],
6970
['scroll', 0, 75],
71+
['scroll', 0, 100],
7072
['scrollEndDrag', 0, 100],
7173
]);
7274
});
@@ -117,3 +119,28 @@ test('scrollTo with contentSize and layoutMeasurement update FlatList content',
117119
// Prevent act warning by unmounting the component
118120
await screen.unmount();
119121
});
122+
123+
test('drag scroll delivers final offset to FlatList', async () => {
124+
const onEndReached = jest.fn();
125+
await render(
126+
<FlatList
127+
testID="flat-list"
128+
data={data}
129+
renderItem={(x) => <Item title={x.item} />}
130+
onEndReached={onEndReached}
131+
onEndReachedThreshold={0}
132+
/>,
133+
);
134+
const user = userEvent.setup();
135+
136+
await user.scrollTo(screen.getByTestId('flat-list'), {
137+
y: 520,
138+
contentSize: { width: 240, height: 1000 },
139+
layoutMeasurement: { width: 240, height: 480 },
140+
});
141+
142+
expect(onEndReached).toHaveBeenCalledTimes(1);
143+
144+
// Prevent act warning by unmounting the component
145+
await screen.unmount();
146+
});

‎src/user-event/scroll/__tests__/scroll-to.test.tsx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ describe('scrollTo()', () => {
4949
['scroll', 25, 0],
5050
['scroll', 50, 0],
5151
['scroll', 75, 0],
52+
['scroll', 100, 0],
5253
['scrollEndDrag', 100, 0],
5354
]);
5455
expect(events).toMatchSnapshot('scrollTo({ y: 100 })');
@@ -64,6 +65,7 @@ describe('scrollTo()', () => {
6465
['scroll', 0, 25],
6566
['scroll', 0, 50],
6667
['scroll', 0, 75],
68+
['scroll', 0, 100],
6769
['scrollEndDrag', 0, 100],
6870
]);
6971
});
@@ -81,6 +83,7 @@ describe('scrollTo()', () => {
8183
['scroll', 25, 0],
8284
['scroll', 50, 0],
8385
['scroll', 75, 0],
86+
['scroll', 100, 0],
8487
['scrollEndDrag', 100, 0],
8588
['momentumScrollBegin', 100, 0],
8689
['scroll', 110, 0],
@@ -102,6 +105,7 @@ describe('scrollTo()', () => {
102105
['scroll', 25, 0],
103106
['scroll', 50, 0],
104107
['scroll', 75, 0],
108+
['scroll', 100, 0],
105109
['scrollEndDrag', 100, 0],
106110
]);
107111
});
@@ -117,11 +121,13 @@ describe('scrollTo()', () => {
117121
['scroll', 25, 0],
118122
['scroll', 50, 0],
119123
['scroll', 75, 0],
124+
['scroll', 100, 0],
120125
['scrollEndDrag', 100, 0],
121126
['scrollBeginDrag', 100, 0],
122127
['scroll', 125, 0],
123128
['scroll', 150, 0],
124129
['scroll', 175, 0],
130+
['scroll', 200, 0],
125131
['scrollEndDrag', 200, 0],
126132
]);
127133
});
@@ -140,6 +146,7 @@ describe('scrollTo()', () => {
140146
['scroll', 125, 0],
141147
['scroll', 150, 0],
142148
['scroll', 175, 0],
149+
['scroll', 200, 0],
143150
['scrollEndDrag', 200, 0],
144151
]);
145152
});
@@ -190,9 +197,9 @@ describe('scrollTo()', () => {
190197
['scroll', 25, 0],
191198
['scroll', 50, 0],
192199
['scroll', 75, 0],
200+
['scroll', 100, 0],
193201
['scrollEndDrag', 100, 0],
194202
['momentumScrollBegin', 100, 0],
195-
['scroll', 100, 0],
196203
['momentumScrollEnd', 100, 0],
197204
]);
198205
});
@@ -254,6 +261,7 @@ describe('scrollTo()', () => {
254261
['scroll', 25, 0],
255262
['scroll', 50, 0],
256263
['scroll', 75, 0],
264+
['scroll', 100, 0],
257265
['scrollEndDrag', 100, 0],
258266
]);
259267
});

‎src/user-event/scroll/scroll-to.ts‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,8 @@ async function emitDragScrollEvents(
103103
buildScrollEvent(scrollSteps[0], scrollOptions),
104104
);
105105

106-
// Note: experimentally, in case of drag scroll the last scroll step
107-
// will not trigger `scroll` event.
108-
// See: https://github.com/callstack/react-native-testing-library/wiki/ScrollView-Events
109-
for (let i = 1; i < scrollSteps.length - 1; i += 1) {
106+
// Native emits `scroll` for every drag step, including the last; `scrollEndDrag` repeats its offset.
107+
for (let i = 1; i < scrollSteps.length; i += 1) {
110108
await wait(config);
111109
await interaction.dispatchEvent('scroll', buildScrollEvent(scrollSteps[i], scrollOptions));
112110
}
@@ -132,9 +130,7 @@ async function emitMomentumScrollEvents(
132130
buildScrollEvent(scrollSteps[0], scrollOptions),
133131
);
134132

135-
// Note: experimentally, in case of momentum scroll the last scroll step
136-
// will trigger `scroll` event.
137-
// See: https://github.com/callstack/react-native-testing-library/wiki/ScrollView-Events
133+
// Native emits `scroll` for every momentum step, including the last; `momentumScrollEnd` repeats its offset.
138134
for (let i = 1; i < scrollSteps.length; i += 1) {
139135
await wait(config);
140136
await interaction.dispatchEvent('scroll', buildScrollEvent(scrollSteps[i], scrollOptions));

‎src/user-event/scroll/utils.ts‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ export function createScrollSteps(
1010
interpolator: InterpolatorFn,
1111
): Point[] {
1212
if (target.y != null) {
13-
return interpolator(target.y, initialOffset.y, DEFAULT_STEPS_COUNT).map((y) => ({
13+
return interpolate(target.y, initialOffset.y, interpolator).map((y) => ({
1414
y,
1515
x: initialOffset.x,
1616
}));
1717
}
1818

1919
if (target.x != null) {
20-
return interpolator(target.x, initialOffset.x, DEFAULT_STEPS_COUNT).map((x) => ({
20+
return interpolate(target.x, initialOffset.x, interpolator).map((x) => ({
2121
x,
2222
y: initialOffset.y,
2323
}));
@@ -26,14 +26,19 @@ export function createScrollSteps(
2626
return [];
2727
}
2828

29+
function interpolate(end: number, start: number, interpolator: InterpolatorFn): number[] {
30+
// No movement: a single step, so no `scroll` event is emitted for an unchanged offset.
31+
if (end === start) {
32+
return [start];
33+
}
34+
35+
return interpolator(end, start, DEFAULT_STEPS_COUNT);
36+
}
37+
2938
/**
3039
* Generate linear scroll values (with equal steps).
3140
*/
3241
export function linearInterpolator(end: number, start: number, steps: number): number[] {
33-
if (end === start) {
34-
return [end, start];
35-
}
36-
3742
const result = [];
3843
for (let i = 0; i < steps; i += 1) {
3944
result.push(lerp(start, end, i / (steps - 1)));
@@ -46,10 +51,6 @@ export function linearInterpolator(end: number, start: number, steps: number): n
4651
* Generate inertial scroll values (exponentially slowing down).
4752
*/
4853
export function inertialInterpolator(end: number, start: number, steps: number): number[] {
49-
if (end === start) {
50-
return [end, start];
51-
}
52-
5354
const result = [];
5455
let factor = 1;
5556
for (let i = 0; i < steps - 1; i += 1) {

‎website/docs/14.x/docs/api/events/user-event.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,13 @@ The sequence of events depends on whether the scroll includes an optional moment
286286

287287
- `contentSizeChange`
288288
- `scrollBeginDrag`
289-
- `scroll` (multiple events)
289+
- `scroll` (zero or more events; none if the scroll offset does not change)
290290
- `scrollEndDrag`
291291

292292
**Momentum scroll (optional)**:
293293

294294
- `momentumScrollBegin`
295-
- `scroll` (multiple events)
295+
- `scroll` (zero or more events; none if the scroll offset does not change)
296296
- `momentumScrollEnd`
297297

298298
## `pullToRefresh()` \{#pull-to-refresh}

0 commit comments

Comments
 (0)