Skip to content

Commit 1e0c170

Browse files
authored
feat: scrollTo support { left } (#215)
* chore: patch hook info * feat: scrollTo support x * refactor: same as native scrollTo * test: more test case
1 parent 324de08 commit 1e0c170

File tree

3 files changed

+115
-50
lines changed

3 files changed

+115
-50
lines changed

src/List.tsx

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import ScrollBar from './ScrollBar';
1111
import type { RenderFunc, SharedConfig, GetKey, ExtraRenderInfo } from './interface';
1212
import useChildren from './hooks/useChildren';
1313
import useHeights from './hooks/useHeights';
14-
import useScrollTo from './hooks/useScrollTo';
14+
import useScrollTo, { type ScrollPos, type ScrollTarget } from './hooks/useScrollTo';
1515
import useDiffItem from './hooks/useDiffItem';
1616
import useFrameWheel from './hooks/useFrameWheel';
1717
import useMobileTouchMove from './hooks/useMobileTouchMove';
@@ -27,21 +27,18 @@ const ScrollStyle: React.CSSProperties = {
2727
overflowAnchor: 'none',
2828
};
2929

30-
export type ScrollAlign = 'top' | 'bottom' | 'auto';
31-
export type ScrollConfig =
32-
| {
33-
index: number;
34-
align?: ScrollAlign;
35-
offset?: number;
36-
}
37-
| {
38-
key: React.Key;
39-
align?: ScrollAlign;
40-
offset?: number;
41-
};
30+
export interface ScrollInfo {
31+
x: number;
32+
y: number;
33+
}
34+
35+
export type ScrollConfig = ScrollTarget | ScrollPos;
36+
4237
export type ScrollTo = (arg: number | ScrollConfig) => void;
38+
4339
export type ListRef = {
4440
scrollTo: ScrollTo;
41+
getScrollInfo: () => ScrollInfo;
4542
};
4643

4744
export interface ListProps<T> extends Omit<React.HTMLAttributes<any>, 'children'> {
@@ -70,7 +67,7 @@ export interface ListProps<T> extends Omit<React.HTMLAttributes<any>, 'children'
7067
* Given the virtual offset value.
7168
* It's the logic offset from start position.
7269
*/
73-
onVirtualScroll?: (info: { x: number; y: number }) => void;
70+
onVirtualScroll?: (info: ScrollInfo) => void;
7471

7572
/** Trigger when render list item changed */
7673
onVisibleChange?: (visibleList: T[], fullList: T[]) => void;
@@ -287,21 +284,25 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
287284
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
288285

289286
// ================================ Scroll ================================
290-
const lastVirtualScrollInfoRef = useRef<[number, number]>([0, 0]);
287+
const getVirtualScrollInfo = () => ({
288+
x: isRTL ? -offsetLeft : offsetLeft,
289+
y: offsetTop,
290+
});
291+
292+
const lastVirtualScrollInfoRef = useRef(getVirtualScrollInfo());
291293

292294
const triggerScroll = useEvent(() => {
293295
if (onVirtualScroll) {
294-
const x = isRTL ? -offsetLeft : offsetLeft;
295-
const y = offsetTop;
296+
const nextInfo = getVirtualScrollInfo();
296297

297298
// Trigger when offset changed
298-
if (lastVirtualScrollInfoRef.current[0] !== x || lastVirtualScrollInfoRef.current[1] !== y) {
299-
onVirtualScroll({
300-
x,
301-
y,
302-
});
299+
if (
300+
lastVirtualScrollInfoRef.current.x !== nextInfo.x ||
301+
lastVirtualScrollInfoRef.current.y !== nextInfo.y
302+
) {
303+
onVirtualScroll(nextInfo);
303304

304-
lastVirtualScrollInfoRef.current = [x, y];
305+
lastVirtualScrollInfoRef.current = nextInfo;
305306
}
306307
}
307308
});
@@ -331,19 +332,24 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
331332
triggerScroll();
332333
}
333334

335+
const keepInHorizontalRange = (nextOffsetLeft: number) => {
336+
let tmpOffsetLeft = nextOffsetLeft;
337+
const max = scrollWidth - size.width;
338+
tmpOffsetLeft = Math.max(tmpOffsetLeft, 0);
339+
tmpOffsetLeft = Math.min(tmpOffsetLeft, max);
340+
341+
return tmpOffsetLeft;
342+
};
343+
334344
const onWheelDelta: Parameters<typeof useFrameWheel>[4] = useEvent((offsetXY, fromHorizontal) => {
335345
if (fromHorizontal) {
336346
// Horizontal scroll no need sync virtual position
337347

338348
flushSync(() => {
339349
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);
350+
const nextOffsetLeft = left + (isRTL ? -offsetXY : offsetXY);
345351

346-
return newLeft;
352+
return keepInHorizontalRange(nextOffsetLeft);
347353
});
348354
});
349355

@@ -413,7 +419,24 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
413419
);
414420

415421
React.useImperativeHandle(ref, () => ({
416-
scrollTo,
422+
getScrollInfo: getVirtualScrollInfo,
423+
scrollTo: (config) => {
424+
function isPosScroll(arg: any): arg is ScrollPos {
425+
return arg && typeof arg === 'object' && ('left' in arg || 'top' in arg);
426+
}
427+
428+
if (isPosScroll(config)) {
429+
// Scroll X
430+
if (config.left !== undefined) {
431+
setOffsetLeft(keepInHorizontalRange(config.left));
432+
}
433+
434+
// Scroll Y
435+
scrollTo(config.top);
436+
} else {
437+
scrollTo(config);
438+
}
439+
},
417440
}));
418441

419442
// ================================ Effect ================================

src/hooks/useScrollTo.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
11
/* eslint-disable no-param-reassign */
22
import * as React from 'react';
33
import raf from 'rc-util/lib/raf';
4-
import type { ScrollTo } from '../List';
54
import type { GetKey } from '../interface';
65
import type CacheMap from '../utils/CacheMap';
76

7+
export type ScrollAlign = 'top' | 'bottom' | 'auto';
8+
9+
export type ScrollPos = {
10+
left?: number;
11+
top?: number;
12+
};
13+
14+
export type ScrollTarget =
15+
| {
16+
index: number;
17+
align?: ScrollAlign;
18+
offset?: number;
19+
}
20+
| {
21+
key: React.Key;
22+
align?: ScrollAlign;
23+
offset?: number;
24+
};
25+
826
export default function useScrollTo<T>(
927
containerRef: React.RefObject<HTMLDivElement>,
1028
data: T[],
@@ -14,7 +32,7 @@ export default function useScrollTo<T>(
1432
collectHeight: () => void,
1533
syncScrollTop: (newTop: number) => void,
1634
triggerFlash: () => void,
17-
): ScrollTo {
35+
): (arg: number | ScrollTarget) => void {
1836
const scrollRef = React.useRef<number>();
1937

2038
return (arg) => {

tests/scrollWidth.test.tsx

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { act, fireEvent, render } from '@testing-library/react';
33
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
44
import {} from 'rc-resize-observer';
5+
import type { ListRef } from '../src';
56
import List, { type ListProps } from '../src';
67
import { _rs as onLibResize } from 'rc-resize-observer/lib/utils/observerUtil';
78
import '@testing-library/jest-dom';
@@ -50,16 +51,28 @@ describe('List.scrollWidth', () => {
5051
jest.useRealTimers();
5152
});
5253

53-
function genList(props: Partial<ListProps<any>>) {
54-
return render(
54+
async function genList(props: Partial<ListProps<any>> & { ref?: any }) {
55+
const ret = render(
5556
<List component="ul" itemKey="id" {...(props as any)}>
5657
{({ id }) => <li>{id}</li>}
5758
</List>,
5859
);
60+
61+
await act(async () => {
62+
onLibResize([
63+
{
64+
target: ret.container.querySelector('.rc-virtual-list-holder')!,
65+
} as ResizeObserverEntry,
66+
]);
67+
68+
await Promise.resolve();
69+
});
70+
71+
return ret;
5972
}
6073

61-
it('work', () => {
62-
const { container } = genList({
74+
it('work', async () => {
75+
const { container } = await genList({
6376
itemHeight: 20,
6477
height: 100,
6578
data: genData(100),
@@ -72,13 +85,15 @@ describe('List.scrollWidth', () => {
7285
describe('trigger offset', () => {
7386
it('drag scrollbar', async () => {
7487
const onVirtualScroll = jest.fn();
88+
const listRef = React.createRef<ListRef>();
7589

76-
const { container } = genList({
90+
const { container } = await genList({
7791
itemHeight: 20,
7892
height: 100,
7993
data: genData(100),
8094
scrollWidth: 1000,
8195
onVirtualScroll,
96+
ref: listRef,
8297
});
8398

8499
await act(async () => {
@@ -114,29 +129,20 @@ describe('List.scrollWidth', () => {
114129
});
115130

116131
expect(onVirtualScroll).toHaveBeenCalledWith({ x: 900, y: 0 });
132+
expect(listRef.current.getScrollInfo()).toEqual({ x: 900, y: 0 });
117133
});
118134

119135
it('wheel', async () => {
120136
const onVirtualScroll = jest.fn();
121137

122-
const { container } = genList({
138+
const { container } = await genList({
123139
itemHeight: 20,
124140
height: 100,
125141
data: genData(100),
126142
scrollWidth: 1000,
127143
onVirtualScroll,
128144
});
129145

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-
140146
// Wheel
141147
fireEvent.wheel(container.querySelector('.rc-virtual-list-holder')!, {
142148
deltaX: 123,
@@ -145,8 +151,26 @@ describe('List.scrollWidth', () => {
145151
});
146152
});
147153

148-
it('support extraRender', () => {
149-
const { container } = genList({
154+
it('ref scrollTo', async () => {
155+
const listRef = React.createRef<ListRef>();
156+
157+
await genList({
158+
itemHeight: 20,
159+
height: 100,
160+
data: genData(100),
161+
scrollWidth: 1000,
162+
ref: listRef,
163+
});
164+
165+
listRef.current.scrollTo({ left: 135 });
166+
expect(listRef.current.getScrollInfo()).toEqual({ x: 135, y: 0 });
167+
168+
listRef.current.scrollTo({ left: -99 });
169+
expect(listRef.current.getScrollInfo()).toEqual({ x: 0, y: 0 });
170+
});
171+
172+
it('support extraRender', async () => {
173+
const { container } = await genList({
150174
itemHeight: 20,
151175
height: 100,
152176
data: genData(100),

0 commit comments

Comments
 (0)