From 803fd75fb26c4219b0fe4fec2ead3e859cbca52b Mon Sep 17 00:00:00 2001 From: linxianxi <904492381@qq.com> Date: Thu, 23 May 2024 16:30:42 +0800 Subject: [PATCH 1/4] feat: support sticky indexes --- README.md | 23 +++++++------ docs/demo/sticky.md | 8 +++++ examples/sticky.tsx | 69 ++++++++++++++++++++++++++++++++++++++ src/List.tsx | 5 +++ src/hooks/useChildren.tsx | 54 +++++++++++++++++++++++++++-- src/hooks/useHeights.tsx | 4 +-- src/utils/algorithmUtil.ts | 23 +++++++++++++ 7 files changed, 171 insertions(+), 15 deletions(-) create mode 100644 docs/demo/sticky.md create mode 100644 examples/sticky.tsx diff --git a/README.md b/README.md index b23c05e6..09fd67a8 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ open http://localhost:9001/ import List from 'rc-virtual-list'; - {index =>
{index}
} + {(index) =>
{index}
}
; ``` @@ -51,16 +51,17 @@ import List from 'rc-virtual-list'; ## List -| Prop | Description | Type | Default | -| ---------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| children | Render props of item | (item, index, props) => ReactElement | - | -| component | Customize List dom element | string \| Component | div | -| data | Data list | Array | - | -| disabled | Disable scroll check. Usually used on animation control | boolean | false | -| height | List height | number | - | -| itemHeight | Item minium height | number | - | -| itemKey | Match key with item | string | - | -| styles | style | { horizontalScrollBar?: React.CSSProperties; horizontalScrollBarThumb?: React.CSSProperties; verticalScrollBar?: React.CSSProperties; verticalScrollBarThumb?: React.CSSProperties; } | - | +| Prop | Description | Type | Default | +| ------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| children | Render props of item | (item, index, props) => ReactElement | - | +| component | Customize List dom element | string \| Component | div | +| data | Data list | Array | - | +| disabled | Disable scroll check. Usually used on animation control | boolean | false | +| height | List height | number | - | +| itemHeight | Item minium height | number | - | +| itemKey | Match key with item | string | - | +| stickyIndexes | sticky indexes | number[] | - | +| styles | style | { horizontalScrollBar?: React.CSSProperties; horizontalScrollBarThumb?: React.CSSProperties; verticalScrollBar?: React.CSSProperties; verticalScrollBarThumb?: React.CSSProperties; } | - | `children` provides additional `props` argument to support IE 11 scroll shaking. It will set `style` to `visibility: hidden` when measuring. You can ignore this if no requirement on IE. diff --git a/docs/demo/sticky.md b/docs/demo/sticky.md new file mode 100644 index 00000000..f1aa86f9 --- /dev/null +++ b/docs/demo/sticky.md @@ -0,0 +1,8 @@ +--- +title: Sticky +nav: + title: Demo + path: /demo +--- + + diff --git a/examples/sticky.tsx b/examples/sticky.tsx new file mode 100644 index 00000000..958c0f14 --- /dev/null +++ b/examples/sticky.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import List from '../src/List'; + +interface Item { + id: number; + height: number; + style?: React.CSSProperties; +} + +const MyItem: React.ForwardRefRenderFunction = ({ id, height, style }, ref) => { + return ( + + {id} + + ); +}; + +const ForwardMyItem = React.forwardRef(MyItem); + +const data: Item[] = []; +for (let i = 0; i < 100; i += 1) { + data.push({ + id: i, + height: 30 + (i % 2 ? 70 : 0), + }); +} + +const Demo = () => { + return ( + +
+

Sticky

+ + + {(item, _, { style }) => } + +
+
+ ); +}; + +export default Demo; diff --git a/src/List.tsx b/src/List.tsx index ad1f4b34..a0e19461 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -64,6 +64,7 @@ export interface ListProps extends Omit, 'children' * When set, `virtual` will always be enabled. */ scrollWidth?: number; + stickyIndexes?: number[]; styles?: { horizontalScrollBar?: React.CSSProperties; @@ -104,6 +105,7 @@ export function RawList(props: ListProps, ref: React.Ref) { virtual, direction, scrollWidth, + stickyIndexes, component: Component = 'div', onScroll, onVirtualScroll, @@ -521,9 +523,12 @@ export function RawList(props: ListProps, ref: React.Ref) { end, scrollWidth, offsetLeft, + offsetTop, setInstanceRef, children, sharedConfig, + heights, + stickyIndexes, ); let componentStyle: React.CSSProperties = null; diff --git a/src/hooks/useChildren.tsx b/src/hooks/useChildren.tsx index 8c4fdf6d..8f6e9ea4 100644 --- a/src/hooks/useChildren.tsx +++ b/src/hooks/useChildren.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; import type { RenderFunc, SharedConfig } from '../interface'; import { Item } from '../Item'; +import { generateIndexesWithSticky } from '../utils/algorithmUtil'; +import type CacheMap from '../utils/CacheMap'; export default function useChildren( list: T[], @@ -8,15 +10,63 @@ export default function useChildren( endIndex: number, scrollWidth: number, offsetX: number, + offsetY: number, setNodeRef: (item: T, element: HTMLElement) => void, renderFunc: RenderFunc, { getKey }: SharedConfig, + heights: CacheMap, + stickyIndexes: number[] = [], ) { - return list.slice(startIndex, endIndex + 1).map((item, index) => { + // Distance beyond the top of the container + const startOverContainerTop = + offsetY - + list.slice(0, startIndex).reduce((total, item) => total + heights.get(getKey(item)), 0); + + const shouldStickyIndexesAndTop = stickyIndexes + .sort((a, b) => a - b) + .reduce<{ index: number; top: number }[]>((total, index) => { + // The sum of the heights of all sticky elements + const beforeStickyTotalHeight = total.reduce( + (height, item) => height + heights.get(getKey(list[item.index])), + 0, + ); + if (index <= startIndex) { + total.push({ index, top: startOverContainerTop + beforeStickyTotalHeight }); + } else if (index > startIndex && index < endIndex) { + // Distance from top of container + const offsetContainerTop = + list.slice(0, index).reduce((height, item) => height + heights.get(getKey(item)), 0) - + (offsetY + startOverContainerTop); + + if (offsetContainerTop <= beforeStickyTotalHeight) { + total.push({ index, top: startOverContainerTop + beforeStickyTotalHeight }); + } + } + return total; + }, []); + + return generateIndexesWithSticky( + startIndex, + endIndex, + shouldStickyIndexesAndTop.map((i) => i.index), + ).map((index) => { + const item = list[index]; const eleIndex = startIndex + index; + const isSticky = shouldStickyIndexesAndTop.some((i) => i.index === index); const node = renderFunc(item, eleIndex, { style: { - width: scrollWidth, + ...(scrollWidth + ? { + width: scrollWidth, + } + : {}), + ...(isSticky + ? { + // Use sticky when it exists, use absolute when it disappears + position: index >= startIndex && index <= endIndex ? 'sticky' : 'absolute', + top: shouldStickyIndexesAndTop.find((i) => i.index === index).top, + } + : {}), }, offsetX, }) as React.ReactElement; diff --git a/src/hooks/useHeights.tsx b/src/hooks/useHeights.tsx index ad45d4a2..4d40bb6e 100644 --- a/src/hooks/useHeights.tsx +++ b/src/hooks/useHeights.tsx @@ -1,7 +1,7 @@ -import * as React from 'react'; -import { useRef, useEffect } from 'react'; import findDOMNode from 'rc-util/lib/Dom/findDOMNode'; import raf from 'rc-util/lib/raf'; +import * as React from 'react'; +import { useEffect, useRef } from 'react'; import type { GetKey } from '../interface'; import CacheMap from '../utils/CacheMap'; diff --git a/src/utils/algorithmUtil.ts b/src/utils/algorithmUtil.ts index 8e7a97e8..1d1558a2 100644 --- a/src/utils/algorithmUtil.ts +++ b/src/utils/algorithmUtil.ts @@ -84,3 +84,26 @@ export function findListDiffIndex( return diffIndex === null ? null : { index: diffIndex, multiple }; } + +export function generateIndexesWithSticky( + startIndex: number, + endIndex: number, + stickyIndexes: number[], +) { + // 生成从 startIndex 到 endIndex 的范围数组 + const indexArray: number[] = []; + for (let i = startIndex; i <= endIndex; i++) { + indexArray.push(i); + } + + // 添加 stickyIndexes 中不在范围内的索引 + stickyIndexes.forEach((index) => { + if (index < startIndex || index > endIndex) { + indexArray.push(index); + } + }); + + // 返回排序后的结果以确保顺序正确 + indexArray.sort((a, b) => a - b); + return indexArray; +} From 4d1e9eb73ae36954a4f1f9f50e270ed4c412fae8 Mon Sep 17 00:00:00 2001 From: linxianxi <904492381@qq.com> Date: Thu, 23 May 2024 16:40:06 +0800 Subject: [PATCH 2/4] chore: remove comment --- src/utils/algorithmUtil.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/utils/algorithmUtil.ts b/src/utils/algorithmUtil.ts index 1d1558a2..28a5fd6b 100644 --- a/src/utils/algorithmUtil.ts +++ b/src/utils/algorithmUtil.ts @@ -90,20 +90,17 @@ export function generateIndexesWithSticky( endIndex: number, stickyIndexes: number[], ) { - // 生成从 startIndex 到 endIndex 的范围数组 const indexArray: number[] = []; for (let i = startIndex; i <= endIndex; i++) { indexArray.push(i); } - // 添加 stickyIndexes 中不在范围内的索引 stickyIndexes.forEach((index) => { if (index < startIndex || index > endIndex) { indexArray.push(index); } }); - // 返回排序后的结果以确保顺序正确 indexArray.sort((a, b) => a - b); return indexArray; } From c06a8bca544237f22af268b55a7948abc51d3610 Mon Sep 17 00:00:00 2001 From: linxianxi <904492381@qq.com> Date: Thu, 23 May 2024 16:48:54 +0800 Subject: [PATCH 3/4] chore: update logic --- src/hooks/useChildren.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useChildren.tsx b/src/hooks/useChildren.tsx index 8f6e9ea4..31fd47d6 100644 --- a/src/hooks/useChildren.tsx +++ b/src/hooks/useChildren.tsx @@ -52,7 +52,7 @@ export default function useChildren( ).map((index) => { const item = list[index]; const eleIndex = startIndex + index; - const isSticky = shouldStickyIndexesAndTop.some((i) => i.index === index); + const stickyInfo = shouldStickyIndexesAndTop.find((i) => i.index === index); const node = renderFunc(item, eleIndex, { style: { ...(scrollWidth @@ -60,11 +60,11 @@ export default function useChildren( width: scrollWidth, } : {}), - ...(isSticky + ...(stickyInfo ? { // Use sticky when it exists, use absolute when it disappears position: index >= startIndex && index <= endIndex ? 'sticky' : 'absolute', - top: shouldStickyIndexesAndTop.find((i) => i.index === index).top, + top: stickyInfo.top, } : {}), }, From 19b1ef72f7a87a8cbfc374fc8682004489a0a114 Mon Sep 17 00:00:00 2001 From: linxianxi <904492381@qq.com> Date: Thu, 23 May 2024 17:04:30 +0800 Subject: [PATCH 4/4] chore: update logic --- src/utils/algorithmUtil.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils/algorithmUtil.ts b/src/utils/algorithmUtil.ts index 28a5fd6b..bab675fb 100644 --- a/src/utils/algorithmUtil.ts +++ b/src/utils/algorithmUtil.ts @@ -101,6 +101,5 @@ export function generateIndexesWithSticky( } }); - indexArray.sort((a, b) => a - b); - return indexArray; + return indexArray.sort((a, b) => a - b); }