Skip to content

Commit bc4c181

Browse files
authored
feat: support onVirtualScroll event (#214)
* feat: support onVirtualScroll * chore: use scroll event * fix: not repeat trigger
1 parent c3ee0f6 commit bc4c181

File tree

4 files changed

+113
-41
lines changed

4 files changed

+113
-41
lines changed

examples/horizontal-scroll.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ const Demo = () => {
119119
</div>
120120
);
121121
}}
122-
onScroll={(e) => {
123-
// console.log('Scroll:', e);
122+
onVirtualScroll={(e) => {
123+
console.warn('Scroll:', e);
124124
}}
125125
>
126126
{(item, _, props) => <ForwardMyItem {...item} {...props} />}

src/Filler.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ const Filler = React.forwardRef(
8888
>
8989
{children}
9090
</div>
91-
92-
{extra}
9391
</ResizeObserver>
92+
93+
{extra}
9494
</div>
9595
);
9696
},

src/List.tsx

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
22
import { useRef, useState } from 'react';
3+
import { flushSync } from 'react-dom';
34
import classNames from 'classnames';
45
import type { ResizeObserverProps } from 'rc-resize-observer';
56
import ResizeObserver from 'rc-resize-observer';
@@ -64,6 +65,13 @@ export interface ListProps<T> extends Omit<React.HTMLAttributes<any>, 'children'
6465
scrollWidth?: number;
6566

6667
onScroll?: React.UIEventHandler<HTMLElement>;
68+
69+
/**
70+
* Given the virtual offset value.
71+
* It's the logic offset from start position.
72+
*/
73+
onVirtualScroll?: (info: { x: number; y: number }) => void;
74+
6775
/** Trigger when render list item changed */
6876
onVisibleChange?: (visibleList: T[], fullList: T[]) => void;
6977

@@ -90,6 +98,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
9098
scrollWidth,
9199
component: Component = 'div',
92100
onScroll,
101+
onVirtualScroll,
93102
onVisibleChange,
94103
innerProps,
95104
extraRender,
@@ -278,11 +287,33 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
278287
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
279288

280289
// ================================ Scroll ================================
290+
const lastVirtualScrollInfoRef = useRef<[number, number]>([0, 0]);
291+
292+
const triggerScroll = useEvent(() => {
293+
if (onVirtualScroll) {
294+
const x = isRTL ? -offsetLeft : offsetLeft;
295+
const y = offsetTop;
296+
297+
// Trigger when offset changed
298+
if (lastVirtualScrollInfoRef.current[0] !== x || lastVirtualScrollInfoRef.current[1] !== y) {
299+
onVirtualScroll({
300+
x,
301+
y,
302+
});
303+
304+
lastVirtualScrollInfoRef.current = [x, y];
305+
}
306+
}
307+
});
308+
281309
function onScrollBar(newScrollOffset: number, horizontal?: boolean) {
282310
const newOffset = newScrollOffset;
283311

284312
if (horizontal) {
285-
setOffsetLeft(newOffset);
313+
flushSync(() => {
314+
setOffsetLeft(newOffset);
315+
});
316+
triggerScroll();
286317
} else {
287318
syncScrollTop(newOffset);
288319
}
@@ -297,20 +328,26 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
297328

298329
// Trigger origin onScroll
299330
onScroll?.(e);
331+
triggerScroll();
300332
}
301333

302-
const onWheelDelta = useEvent((offsetXY, fromHorizontal) => {
334+
const onWheelDelta: Parameters<typeof useFrameWheel>[4] = useEvent((offsetXY, fromHorizontal) => {
303335
if (fromHorizontal) {
304336
// Horizontal scroll no need sync virtual position
305-
setOffsetLeft((left) => {
306-
let newLeft = left + offsetXY;
307337

308-
const max = scrollWidth - size.width;
309-
newLeft = Math.max(newLeft, 0);
310-
newLeft = Math.min(newLeft, max);
338+
flushSync(() => {
339+
setOffsetLeft((left) => {
340+
let newLeft = left + (isRTL ? -offsetXY : offsetXY);
341+
342+
const max = scrollWidth - size.width;
343+
newLeft = Math.max(newLeft, 0);
344+
newLeft = Math.min(newLeft, max);
311345

312-
return newLeft;
346+
return newLeft;
347+
});
313348
});
349+
350+
triggerScroll();
314351
} else {
315352
syncScrollTop((top) => {
316353
const newTop = top + offsetXY;

tests/scrollWidth.test.tsx

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -69,44 +69,79 @@ describe('List.scrollWidth', () => {
6969
expect(container.querySelector('.rc-virtual-list-scrollbar-horizontal')).toBeTruthy();
7070
});
7171

72-
it('trigger offset', async () => {
73-
const { container } = genList({
74-
itemHeight: 20,
75-
height: 100,
76-
data: genData(100),
77-
scrollWidth: 1000,
78-
});
72+
describe('trigger offset', () => {
73+
it('drag scrollbar', async () => {
74+
const onVirtualScroll = jest.fn();
7975

80-
await act(async () => {
81-
onLibResize([
82-
{
83-
target: container.querySelector('.rc-virtual-list-holder')!,
84-
} as ResizeObserverEntry,
85-
]);
76+
const { container } = genList({
77+
itemHeight: 20,
78+
height: 100,
79+
data: genData(100),
80+
scrollWidth: 1000,
81+
onVirtualScroll,
82+
});
8683

87-
await Promise.resolve();
88-
});
84+
await act(async () => {
85+
onLibResize([
86+
{
87+
target: container.querySelector('.rc-virtual-list-holder')!,
88+
} as ResizeObserverEntry,
89+
]);
90+
91+
await Promise.resolve();
92+
});
93+
94+
// Drag
95+
const thumb = container.querySelector(
96+
'.rc-virtual-list-scrollbar-horizontal .rc-virtual-list-scrollbar-thumb',
97+
)!;
8998

90-
// Drag
91-
const thumb = container.querySelector(
92-
'.rc-virtual-list-scrollbar-horizontal .rc-virtual-list-scrollbar-thumb',
93-
)!;
99+
pageX = 10;
100+
fireEvent.mouseDown(thumb);
94101

95-
pageX = 10;
96-
fireEvent.mouseDown(thumb);
102+
pageX = 100000;
103+
fireEvent.mouseMove(window);
97104

98-
pageX = 100000;
99-
fireEvent.mouseMove(window);
105+
act(() => {
106+
jest.runAllTimers();
107+
});
100108

101-
act(() => {
102-
jest.runAllTimers();
109+
fireEvent.mouseUp(window);
110+
111+
expect(thumb).toHaveStyle({
112+
left: '80px',
113+
width: '20px',
114+
});
115+
116+
expect(onVirtualScroll).toHaveBeenCalledWith({ x: 900, y: 0 });
103117
});
104118

105-
fireEvent.mouseUp(window);
119+
it('wheel', async () => {
120+
const onVirtualScroll = jest.fn();
106121

107-
expect(thumb).toHaveStyle({
108-
left: '80px',
109-
width: '20px',
122+
const { container } = genList({
123+
itemHeight: 20,
124+
height: 100,
125+
data: genData(100),
126+
scrollWidth: 1000,
127+
onVirtualScroll,
128+
});
129+
130+
await act(async () => {
131+
onLibResize([
132+
{
133+
target: container.querySelector('.rc-virtual-list-holder')!,
134+
} as ResizeObserverEntry,
135+
]);
136+
137+
await Promise.resolve();
138+
});
139+
140+
// Wheel
141+
fireEvent.wheel(container.querySelector('.rc-virtual-list-holder')!, {
142+
deltaX: 123,
143+
});
144+
expect(onVirtualScroll).toHaveBeenCalledWith({ x: 123, y: 0 });
110145
});
111146
});
112147

0 commit comments

Comments
 (0)