Skip to content

Commit

Permalink
feat: scrollTo support { left } (#215)
Browse files Browse the repository at this point in the history
* chore: patch hook info

* feat: scrollTo support x

* refactor: same as native scrollTo

* test: more test case
  • Loading branch information
zombieJ committed Aug 17, 2023
1 parent 324de08 commit 1e0c170
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 50 deletions.
83 changes: 53 additions & 30 deletions src/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import ScrollBar from './ScrollBar';
import type { RenderFunc, SharedConfig, GetKey, ExtraRenderInfo } from './interface';
import useChildren from './hooks/useChildren';
import useHeights from './hooks/useHeights';
import useScrollTo from './hooks/useScrollTo';
import useScrollTo, { type ScrollPos, type ScrollTarget } from './hooks/useScrollTo';
import useDiffItem from './hooks/useDiffItem';
import useFrameWheel from './hooks/useFrameWheel';
import useMobileTouchMove from './hooks/useMobileTouchMove';
Expand All @@ -27,21 +27,18 @@ const ScrollStyle: React.CSSProperties = {
overflowAnchor: 'none',
};

export type ScrollAlign = 'top' | 'bottom' | 'auto';
export type ScrollConfig =
| {
index: number;
align?: ScrollAlign;
offset?: number;
}
| {
key: React.Key;
align?: ScrollAlign;
offset?: number;
};
export interface ScrollInfo {
x: number;
y: number;
}

export type ScrollConfig = ScrollTarget | ScrollPos;

export type ScrollTo = (arg: number | ScrollConfig) => void;

export type ListRef = {
scrollTo: ScrollTo;
getScrollInfo: () => ScrollInfo;
};

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

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

// ================================ Scroll ================================
const lastVirtualScrollInfoRef = useRef<[number, number]>([0, 0]);
const getVirtualScrollInfo = () => ({
x: isRTL ? -offsetLeft : offsetLeft,
y: offsetTop,
});

const lastVirtualScrollInfoRef = useRef(getVirtualScrollInfo());

const triggerScroll = useEvent(() => {
if (onVirtualScroll) {
const x = isRTL ? -offsetLeft : offsetLeft;
const y = offsetTop;
const nextInfo = getVirtualScrollInfo();

// Trigger when offset changed
if (lastVirtualScrollInfoRef.current[0] !== x || lastVirtualScrollInfoRef.current[1] !== y) {
onVirtualScroll({
x,
y,
});
if (
lastVirtualScrollInfoRef.current.x !== nextInfo.x ||
lastVirtualScrollInfoRef.current.y !== nextInfo.y
) {
onVirtualScroll(nextInfo);

lastVirtualScrollInfoRef.current = [x, y];
lastVirtualScrollInfoRef.current = nextInfo;
}
}
});
Expand Down Expand Up @@ -331,19 +332,24 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
triggerScroll();
}

const keepInHorizontalRange = (nextOffsetLeft: number) => {
let tmpOffsetLeft = nextOffsetLeft;
const max = scrollWidth - size.width;
tmpOffsetLeft = Math.max(tmpOffsetLeft, 0);
tmpOffsetLeft = Math.min(tmpOffsetLeft, max);

return tmpOffsetLeft;
};

const onWheelDelta: Parameters<typeof useFrameWheel>[4] = useEvent((offsetXY, fromHorizontal) => {
if (fromHorizontal) {
// Horizontal scroll no need sync virtual position

flushSync(() => {
setOffsetLeft((left) => {
let newLeft = left + (isRTL ? -offsetXY : offsetXY);

const max = scrollWidth - size.width;
newLeft = Math.max(newLeft, 0);
newLeft = Math.min(newLeft, max);
const nextOffsetLeft = left + (isRTL ? -offsetXY : offsetXY);

return newLeft;
return keepInHorizontalRange(nextOffsetLeft);
});
});

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

React.useImperativeHandle(ref, () => ({
scrollTo,
getScrollInfo: getVirtualScrollInfo,
scrollTo: (config) => {
function isPosScroll(arg: any): arg is ScrollPos {
return arg && typeof arg === 'object' && ('left' in arg || 'top' in arg);
}

if (isPosScroll(config)) {
// Scroll X
if (config.left !== undefined) {
setOffsetLeft(keepInHorizontalRange(config.left));
}

// Scroll Y
scrollTo(config.top);
} else {
scrollTo(config);
}
},
}));

// ================================ Effect ================================
Expand Down
22 changes: 20 additions & 2 deletions src/hooks/useScrollTo.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
/* eslint-disable no-param-reassign */
import * as React from 'react';
import raf from 'rc-util/lib/raf';
import type { ScrollTo } from '../List';
import type { GetKey } from '../interface';
import type CacheMap from '../utils/CacheMap';

export type ScrollAlign = 'top' | 'bottom' | 'auto';

export type ScrollPos = {
left?: number;
top?: number;
};

export type ScrollTarget =
| {
index: number;
align?: ScrollAlign;
offset?: number;
}
| {
key: React.Key;
align?: ScrollAlign;
offset?: number;
};

export default function useScrollTo<T>(
containerRef: React.RefObject<HTMLDivElement>,
data: T[],
Expand All @@ -14,7 +32,7 @@ export default function useScrollTo<T>(
collectHeight: () => void,
syncScrollTop: (newTop: number) => void,
triggerFlash: () => void,
): ScrollTo {
): (arg: number | ScrollTarget) => void {
const scrollRef = React.useRef<number>();

return (arg) => {
Expand Down
60 changes: 42 additions & 18 deletions tests/scrollWidth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { act, fireEvent, render } from '@testing-library/react';
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
import {} from 'rc-resize-observer';
import type { ListRef } from '../src';
import List, { type ListProps } from '../src';
import { _rs as onLibResize } from 'rc-resize-observer/lib/utils/observerUtil';
import '@testing-library/jest-dom';
Expand Down Expand Up @@ -50,16 +51,28 @@ describe('List.scrollWidth', () => {
jest.useRealTimers();
});

function genList(props: Partial<ListProps<any>>) {
return render(
async function genList(props: Partial<ListProps<any>> & { ref?: any }) {
const ret = render(
<List component="ul" itemKey="id" {...(props as any)}>
{({ id }) => <li>{id}</li>}
</List>,
);

await act(async () => {
onLibResize([
{
target: ret.container.querySelector('.rc-virtual-list-holder')!,
} as ResizeObserverEntry,
]);

await Promise.resolve();
});

return ret;
}

it('work', () => {
const { container } = genList({
it('work', async () => {
const { container } = await genList({
itemHeight: 20,
height: 100,
data: genData(100),
Expand All @@ -72,13 +85,15 @@ describe('List.scrollWidth', () => {
describe('trigger offset', () => {
it('drag scrollbar', async () => {
const onVirtualScroll = jest.fn();
const listRef = React.createRef<ListRef>();

const { container } = genList({
const { container } = await genList({
itemHeight: 20,
height: 100,
data: genData(100),
scrollWidth: 1000,
onVirtualScroll,
ref: listRef,
});

await act(async () => {
Expand Down Expand Up @@ -114,29 +129,20 @@ describe('List.scrollWidth', () => {
});

expect(onVirtualScroll).toHaveBeenCalledWith({ x: 900, y: 0 });
expect(listRef.current.getScrollInfo()).toEqual({ x: 900, y: 0 });
});

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

const { container } = genList({
const { container } = await genList({
itemHeight: 20,
height: 100,
data: genData(100),
scrollWidth: 1000,
onVirtualScroll,
});

await act(async () => {
onLibResize([
{
target: container.querySelector('.rc-virtual-list-holder')!,
} as ResizeObserverEntry,
]);

await Promise.resolve();
});

// Wheel
fireEvent.wheel(container.querySelector('.rc-virtual-list-holder')!, {
deltaX: 123,
Expand All @@ -145,8 +151,26 @@ describe('List.scrollWidth', () => {
});
});

it('support extraRender', () => {
const { container } = genList({
it('ref scrollTo', async () => {
const listRef = React.createRef<ListRef>();

await genList({
itemHeight: 20,
height: 100,
data: genData(100),
scrollWidth: 1000,
ref: listRef,
});

listRef.current.scrollTo({ left: 135 });
expect(listRef.current.getScrollInfo()).toEqual({ x: 135, y: 0 });

listRef.current.scrollTo({ left: -99 });
expect(listRef.current.getScrollInfo()).toEqual({ x: 0, y: 0 });
});

it('support extraRender', async () => {
const { container } = await genList({
itemHeight: 20,
height: 100,
data: genData(100),
Expand Down

0 comments on commit 1e0c170

Please sign in to comment.