diff --git a/src/List.tsx b/src/List.tsx index 63816da..046cae2 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -15,7 +15,12 @@ import useHeights from './hooks/useHeights'; import useMobileTouchMove from './hooks/useMobileTouchMove'; import useOriginScroll from './hooks/useOriginScroll'; import useScrollDrag from './hooks/useScrollDrag'; -import type { ScrollPos, ScrollTarget } from './hooks/useScrollTo'; +import type { + ScrollOffset, + ScrollOffsetInfo, + ScrollPos, + ScrollTarget, +} from './hooks/useScrollTo'; import useScrollTo from './hooks/useScrollTo'; import type { ExtraRenderInfo, GetKey, RenderFunc, SharedConfig } from './interface'; import type { ScrollBarDirectionType, ScrollBarRef } from './ScrollBar'; @@ -38,6 +43,8 @@ export type ScrollConfig = ScrollTarget | ScrollPos; export type ScrollTo = (arg?: number | ScrollConfig | null) => void; +export type { ScrollOffset, ScrollOffsetInfo }; + export type ListRef = { nativeElement: HTMLDivElement; scrollTo: ScrollTo; @@ -506,12 +513,15 @@ export function RawList(props: ListProps, ref: React.Ref) { horizontalScrollBarRef.current?.delayHidden(); }; + const getSize = useGetSize(mergedData, getKey, heights, itemHeight); + const scrollTo = useScrollTo( componentRef, mergedData, heights, itemHeight, getKey, + getSize, () => collectHeight(true), syncScrollTop, delayHideScrollBar, @@ -550,8 +560,6 @@ export function RawList(props: ListProps, ref: React.Ref) { }, [start, end, mergedData]); // ================================ Extra ================================= - const getSize = useGetSize(mergedData, getKey, heights, itemHeight); - const extraContent = extraRender?.({ start, end, diff --git a/src/hooks/useScrollTo.tsx b/src/hooks/useScrollTo.tsx index 3dd8518..8a67806 100644 --- a/src/hooks/useScrollTo.tsx +++ b/src/hooks/useScrollTo.tsx @@ -1,7 +1,7 @@ /* eslint-disable no-param-reassign */ import * as React from 'react'; import { raf, useLayoutEffect, warning } from '@rc-component/util'; -import type { GetKey } from '../interface'; +import type { GetKey, GetSize } from '../interface'; import type CacheMap from '../utils/CacheMap'; const MAX_TIMES = 10; @@ -13,24 +13,41 @@ export type ScrollPos = { top?: number; }; +export interface ScrollOffsetInfo { + /** + * Get item size range by key. + * 通过 key 获取元素在虚拟列表中的尺寸范围。 + */ + getSize: GetSize; +} + +export type ScrollOffset = number | ((info: ScrollOffsetInfo) => number); + export type ScrollTarget = | { index: number; align?: ScrollAlign; - offset?: number; + offset?: ScrollOffset; } | { key: React.Key; align?: ScrollAlign; - offset?: number; + offset?: ScrollOffset; }; +function getOffset(rawOffset: ScrollOffset, info: ScrollOffsetInfo) { + const resolvedOffset = typeof rawOffset === 'function' ? rawOffset(info) : rawOffset; + + return Number.isFinite(resolvedOffset) ? resolvedOffset : 0; +} + export default function useScrollTo( containerRef: React.RefObject, data: T[], heights: CacheMap, itemHeight: number, getKey: GetKey, + getSize: GetSize, collectHeight: () => void, syncScrollTop: (newTop: number) => void, triggerFlash: () => void, @@ -40,7 +57,7 @@ export default function useScrollTo( const [syncState, setSyncState] = React.useState<{ times: number; index: number; - offset: number; + offset: ScrollOffset; originAlign: ScrollAlign; targetAlign?: 'top' | 'bottom'; lastTop?: number; @@ -57,7 +74,8 @@ export default function useScrollTo( collectHeight(); - const { targetAlign, originAlign, index, offset } = syncState; + const { targetAlign, originAlign, index, offset: rawOffset } = syncState; + const offset = getOffset(rawOffset, { getSize }); const height = containerRef.current.clientHeight; let needCollectHeight = false; @@ -171,12 +189,12 @@ export default function useScrollTo( index = data.findIndex((item) => getKey(item) === arg.key); } - const { offset = 0 } = arg; + const { offset: rawOffset = 0 } = arg; setSyncState({ times: 0, index, - offset, + offset: rawOffset, originAlign: align, }); } diff --git a/src/index.ts b/src/index.ts index bbcd8f3..c470797 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,13 @@ import List from './List'; -export type { ListRef, ListProps, ScrollConfig, ScrollTo } from './List'; +export type { + ListRef, + ListProps, + ScrollConfig, + ScrollOffset, + ScrollOffsetInfo, + ScrollTo, +} from './List'; export { default as MockList } from './mock'; export default List; diff --git a/tests/scroll.test.js b/tests/scroll.test.js index b156a78..a7bb170 100644 --- a/tests/scroll.test.js +++ b/tests/scroll.test.js @@ -203,6 +203,28 @@ describe('List.Scroll', () => { expect(container.querySelector('ul').scrollTop).toEqual(520); }); + it('supports function offset with getSize info', () => { + const { scrollTo, container } = presetList(); + const offset = jest.fn(({ getSize }) => getSize('2').bottom); + + scrollTo({ key: '30', align: 'top', offset }); + + expect(offset).toHaveBeenCalledWith({ + getSize: expect.any(Function), + }); + expect(offset).toHaveBeenCalledTimes(2); + expect(container.querySelector('ul').scrollTop).toEqual(540); + }); + + it('fallbacks invalid function offset to zero', () => { + const { scrollTo, container } = presetList(); + const offset = jest.fn(() => NaN); + + scrollTo({ key: '30', align: 'top', offset }); + + expect(container.querySelector('ul').scrollTop).toEqual(600); + }); + it('smart', () => { const { scrollTo, container } = presetList(); scrollTo(0);