Skip to content

Commit d857395

Browse files
committed
feat(prop): add 'sticky'
1 parent 9b80905 commit d857395

File tree

6 files changed

+103
-36
lines changed

6 files changed

+103
-36
lines changed

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
<!doctype html>
1+
<!DOCTYPE html>
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>Vite + React + TS</title>
7+
<title>Virtual List Demo</title>
88
</head>
99
<body>
1010
<div id="root"></div>

src/App.tsx

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,19 @@
1-
import { VirtualList, } from './VirtualList'
2-
import exampleData from './example_data.json'
3-
1+
import BaseExample from "./examples/base";
2+
import StickyExample from "./examples/sticky";
43
function App() {
54
return (
65
<>
7-
<b>@phphe/react-base-virtual-list</b> on <a href="https://github.com/phphe/react-base-virtual-list">Github</a>
8-
<div>
9-
<h1>Virtual List Demo</h1>
10-
<ul>
11-
<li>Dynamic, the list items are not the same height.</li>
12-
<li>1000 items in the demo.</li>
13-
</ul>
6+
<div className='text-center'>
7+
<b>@phphe/react-base-virtual-list</b>
8+
<a className='ml-10' href="https://github.com/phphe/react-base-virtual-list">Github</a>
9+
</div>
10+
<div className='grid grid-cols-2 gap-4'>
11+
<div>
12+
13+
<BaseExample />
14+
</div>
1415
<div>
15-
<VirtualList
16-
items={exampleData}
17-
style={{ height: '600px', width: '600px', border: '1px solid #ccc', padding: '10px' }}
18-
renderItem={(item, index) => <div key={index} style={{ marginBottom: '10px' }}>
19-
<h3>{index}. {item.headline}</h3>
20-
<div>
21-
<div style={{ float: 'left', width: '100px', height: '100px', background: '#f0f0f0', borderRadius: '5px', marginRight: '10px' }}></div>
22-
{item.content}
23-
</div>
24-
</div>}
25-
></VirtualList><a href="https://github.com/phphe/react-base-virtual-list/blob/main/src/App.tsx">Source Code</a>
16+
<StickyExample />
2617
</div>
2718
</div >
2819
</>

src/VirtualList.tsx

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
11
import React, {
22
useState,
33
useMemo,
4-
useEffect, useRef, ReactNode
4+
useEffect, useRef, ReactNode, useLayoutEffect, useImperativeHandle, forwardRef,
55
} from 'react';
66

7+
type OptionalKeys<T> = {
8+
[K in keyof T]?: T[K];
9+
};
10+
711
export type Props<ITEM> = {
812
itemSize?: number,
913
buffer?: number,
1014
items: ITEM[],
11-
renderItem: (item: ITEM, index: number) => ReactNode
12-
} & typeof defaultProps
15+
renderItem: (item: ITEM, index: number) => ReactNode,
16+
sticky?: number[], // index[]
17+
className?: string,
18+
style?: React.CSSProperties,
19+
} & OptionalKeys<typeof defaultProps>
1320

1421
export const defaultProps = {
1522
listSize: 1000,
1623
}
17-
export function VirtualList<ITEM>(
18-
props: Props<ITEM> & React.HTMLProps<HTMLElement>,
24+
25+
export interface VirtualListHandle {
26+
scrollToIndex(index: number): void
27+
}
28+
29+
export const VirtualList = forwardRef(function <ITEM>(
30+
props: Props<ITEM>,
31+
ref: React.ForwardedRef<VirtualListHandle>
1932
) {
2033
const [itemSize, setitemSize] = useState(props.itemSize || 100);
2134
const buffer = useMemo(() => props.buffer || Math.max(itemSize * 5, 100), [props.buffer, itemSize]);
@@ -24,7 +37,7 @@ export function VirtualList<ITEM>(
2437
const listInner = useRef<HTMLDivElement>(null);
2538
const prevScrollTop = useRef(0);
2639
const [scrollTop, setscrollTop] = useState(0);
27-
const [listSize, setlistSize] = useState(props.listSize);
40+
const [listSize, setlistSize] = useState(props.listSize!);
2841

2942
//
3043
const totalSpace = itemSize * count
@@ -38,22 +51,29 @@ export function VirtualList<ITEM>(
3851
} else {
3952
startIndex = Math.floor(topSpace / itemSize)
4053
}
54+
if (bottomSpace < 0) {
55+
bottomSpace = 0
56+
}
4157
if (totalSpace <= listSize) {
4258
endIndex = count
4359
} else {
4460
endIndex = count - Math.floor(bottomSpace / itemSize)
4561
}
46-
if (bottomSpace < 0) {
47-
bottomSpace = 0
62+
const mainVisibleIndexes = Array.from({ length: endIndex - startIndex }, (_, index) => index + startIndex);
63+
let visibleIndexes = mainVisibleIndexes.concat(props.sticky || [])
64+
if (props.sticky?.length) {
65+
visibleIndexes = [...new Set(visibleIndexes)].sort((a, b) => a - b)
4866
}
49-
const visible = props.items.slice(startIndex, endIndex)
67+
const visible = visibleIndexes.map(i => props.items[i])
68+
69+
//
5070
const listInnerStyle: any = { paddingTop: `${topSpace}px`, boxSizing: 'border-box' }
5171
if (bottomSpace < itemSize * 5) {
5272
listInnerStyle['paddingBottom'] = `${bottomSpace}px`
5373
} else {
5474
listInnerStyle['height'] = `${totalSpace}px`
5575
}
56-
useEffect(() => {
76+
useLayoutEffect(() => {
5777
setlistSize(list.current!.clientHeight)
5878
if (props.itemSize == null) {
5979
// get avg item size
@@ -77,11 +97,17 @@ export function VirtualList<ITEM>(
7797
}
7898
}
7999
//
100+
useImperativeHandle(ref, () => ({
101+
scrollToIndex: (index: number) => {
102+
list.current!.scrollTop = index * itemSize
103+
},
104+
}), []);
105+
//
80106
return <div ref={list} onScroll={handleScroll} className={props.className} style={{ overflow: 'auto', ...props.style }}>
81107
<div ref={listInner} style={{ display: 'flex', flexDirection: 'column', ...listInnerStyle }}>
82-
{visible.map((item, i) => props.renderItem(item, i + startIndex))}
108+
{visible.map((item, i) => props.renderItem(item, visibleIndexes[i]))}
83109
</div>
84110
</div>
85-
}
111+
})
86112

87-
VirtualList.defaultProps = defaultProps
113+
VirtualList.defaultProps = defaultProps

src/examples/base.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import exampleData from './example_data.json'
2+
import { VirtualList, } from '../VirtualList'
3+
4+
export default function BaseExample() {
5+
return <>
6+
<h2>Virtual List Demo</h2>
7+
<ul>
8+
<li>Dynamic, the list items are not the same height.</li>
9+
<li>1000 items in the demo.</li>
10+
</ul>
11+
<VirtualList
12+
items={exampleData}
13+
style={{ height: '600px', border: '1px solid #ccc', padding: '10px' }}
14+
renderItem={(item, index) => <div key={index} style={{ marginBottom: '10px', }}>
15+
<h3>{index}. {item.headline}</h3>
16+
<div>
17+
<div style={{ float: 'left', width: '100px', height: '100px', background: '#f0f0f0', borderRadius: '5px', marginRight: '10px' }}></div>
18+
{item.content}
19+
</div>
20+
</div>}
21+
></VirtualList>
22+
<br />
23+
<a href="https://github.com/phphe/react-base-virtual-list/blob/main/src/examples/base.tsx">Source Code</a>
24+
</>
25+
}
File renamed without changes.

src/examples/sticky.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import exampleData from './example_data.json'
2+
import { VirtualList, } from '../VirtualList'
3+
4+
export default function StickyExample() {
5+
return <>
6+
<h2>Sticky</h2>
7+
<ul>
8+
<li>"sticky" support multiple items.</li>
9+
</ul>
10+
<VirtualList
11+
items={exampleData}
12+
style={{ height: '600px', border: '1px solid #ccc', padding: '10px' }}
13+
sticky={[1]} // sticky the second item
14+
renderItem={(item, index) => <div key={index} style={{ marginBottom: '10px', ...index === 1 && { position: 'sticky', top: 0, background: '#fff', zIndex: 2 } }}>
15+
<h3>{index}. {item.headline}</h3>
16+
<div>
17+
<div style={{ float: 'left', width: '100px', height: '100px', background: '#f0f0f0', borderRadius: '5px', marginRight: '10px' }}></div>
18+
{item.content}
19+
</div>
20+
</div>}
21+
></VirtualList>
22+
<br />
23+
<a href="https://github.com/phphe/react-base-virtual-list/blob/main/src/examples/sticky.tsx">Source Code</a>
24+
</>
25+
}

0 commit comments

Comments
 (0)