diff --git a/README.md b/README.md index b23c05e..09fd67a 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 0000000..f1aa86f --- /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 0000000..958c0f1 --- /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 ad1f4b3..a0e1946 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 8c4fdf6..31fd47d 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 stickyInfo = shouldStickyIndexesAndTop.find((i) => i.index === index); const node = renderFunc(item, eleIndex, { style: { - width: scrollWidth, + ...(scrollWidth + ? { + width: scrollWidth, + } + : {}), + ...(stickyInfo + ? { + // Use sticky when it exists, use absolute when it disappears + position: index >= startIndex && index <= endIndex ? 'sticky' : 'absolute', + top: stickyInfo.top, + } + : {}), }, offsetX, }) as React.ReactElement; diff --git a/src/hooks/useHeights.tsx b/src/hooks/useHeights.tsx index ad45d4a..4d40bb6 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 8e7a97e..bab675f 100644 --- a/src/utils/algorithmUtil.ts +++ b/src/utils/algorithmUtil.ts @@ -84,3 +84,22 @@ export function findListDiffIndex( return diffIndex === null ? null : { index: diffIndex, multiple }; } + +export function generateIndexesWithSticky( + startIndex: number, + endIndex: number, + stickyIndexes: number[], +) { + const indexArray: number[] = []; + for (let i = startIndex; i <= endIndex; i++) { + indexArray.push(i); + } + + stickyIndexes.forEach((index) => { + if (index < startIndex || index > endIndex) { + indexArray.push(index); + } + }); + + return indexArray.sort((a, b) => a - b); +}