Skip to content

Commit be8b09a

Browse files
committed
feat: add scrollToIndex, forceUpdate
1 parent 946ff62 commit be8b09a

File tree

3 files changed

+85
-13
lines changed

3 files changed

+85
-13
lines changed

src/App.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import BaseExample from "./examples/base";
22
import StickyExample from "./examples/sticky";
3+
import ScrollToIndexExample from "./examples/scrollToIndex";
34

45
function App() {
56
return (
@@ -8,14 +9,16 @@ function App() {
89
<b>@phphe/react-base-virtual-list</b>
910
<a className='ml-10' href="https://github.com/phphe/react-base-virtual-list">Github</a>
1011
</div>
11-
<div className='grid grid-cols-2 gap-4'>
12+
<div className='grid grid-cols-3 gap-4'>
1213
<div>
13-
1414
<BaseExample />
1515
</div>
1616
<div>
1717
<StickyExample />
1818
</div>
19+
<div>
20+
<ScrollToIndexExample />
21+
</div>
1922
</div >
2023
</>
2124
)

src/VirtualList.tsx

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export const defaultProps = {
2828
}
2929

3030
export interface VirtualListHandle {
31-
scrollToIndex(index: number): void
31+
scrollToIndex(index: number, block?: 'start' | 'end' | 'center' | 'nearest'): void
32+
forceUpdate(): void
3233
}
3334

3435
export const VirtualList = forwardRef(function <ITEM>(
@@ -41,9 +42,11 @@ export const VirtualList = forwardRef(function <ITEM>(
4142
const list = useRef<HTMLDivElement>(null);
4243
const listInner = useRef<HTMLDivElement>(null);
4344
const prevScrollTop = useRef(0);
45+
const scrollToIndexRef = useRef<{ index: number, block: string }>();
4446
const [scrollTop, setscrollTop] = useState(0);
4547
const [listSize, setlistSize] = useState(props.listSize!);
46-
48+
const [forceRerender, setforceRerender] = useState([]); // change value to force rerender
49+
const ignoreScrollOnce = useRef(false);
4750
//
4851
const totalSpace = itemSize * count
4952
let topSpace = scrollTop - buffer
@@ -64,12 +67,12 @@ export const VirtualList = forwardRef(function <ITEM>(
6467
} else {
6568
endIndex = count - Math.floor(bottomSpace / itemSize)
6669
}
67-
const mainVisibleIndexes = Array.from({ length: endIndex - startIndex }, (_, index) => index + startIndex);
68-
let visibleIndexes = mainVisibleIndexes.concat(props.persistentIndices || [])
70+
const mainVisibleIndices = Array.from({ length: endIndex - startIndex }, (_, index) => index + startIndex);
71+
let visibleIndices = mainVisibleIndices.concat(props.persistentIndices || [])
6972
if (props.persistentIndices?.length) {
70-
visibleIndexes = [...new Set(visibleIndexes)].sort((a, b) => a - b)
73+
visibleIndices = [...new Set(visibleIndices)].sort((a, b) => a - b)
7174
}
72-
const visible = visibleIndexes.map(i => props.items[i])
75+
const visible = visibleIndices.map(i => props.items[i])
7376

7477
//
7578
const listInnerStyle: any = { paddingTop: `${topSpace}px`, boxSizing: 'border-box' }
@@ -78,39 +81,72 @@ export const VirtualList = forwardRef(function <ITEM>(
7881
} else {
7982
listInnerStyle['height'] = `${totalSpace}px`
8083
}
84+
8185
useLayoutEffect(() => {
8286
setlistSize(list.current!.clientHeight)
87+
// get avg item size
8388
if (props.itemSize == null) {
84-
// get avg item size
8589
let count = 0
8690
let totalHeight = 0
91+
const persistentIndices = new Set(props.persistentIndices || [])
92+
let i = -1
8793
for (const el of listInner.current!.children) {
94+
i++
95+
if (persistentIndices.has(visibleIndices[i])) {
96+
continue
97+
}
8898
const style = getComputedStyle(el)
8999
totalHeight += (el as HTMLElement).offsetHeight + parseFloat(style.marginTop) + parseFloat(style.marginBottom)
90100
count++
91101
}
92102
setitemSize(totalHeight / count)
93103
}
94-
}, [props.itemSize, props.items]);
104+
}, [props.itemSize, props.items, forceRerender]);
95105
//
96106
const handleScroll = () => {
107+
if (ignoreScrollOnce.current) {
108+
ignoreScrollOnce.current = false
109+
return
110+
}
97111
setlistSize(list.current!.clientHeight)
98112
const scrollTop2 = list.current!.scrollTop
99113
if (Math.abs(prevScrollTop.current - scrollTop2) > itemSize) {
100114
setscrollTop(scrollTop2)
101115
prevScrollTop.current = scrollTop2
116+
} else if (scrollToIndexRef.current) {
117+
setforceRerender([])
102118
}
103119
}
104120
//
105121
useImperativeHandle(ref, () => ({
106-
scrollToIndex: (index: number) => {
122+
scrollToIndex(index, block = 'start') {
123+
scrollToIndexRef.current = {
124+
index,
125+
block
126+
}
107127
list.current!.scrollTop = index * itemSize
108128
},
109-
}), []);
129+
forceUpdate() {
130+
setforceRerender([])
131+
}
132+
}), [itemSize]);
133+
useLayoutEffect(() => {
134+
if (scrollToIndexRef.current) {
135+
const { index, block } = scrollToIndexRef.current;
136+
scrollToIndexRef.current = undefined
137+
const indexInVisible = visibleIndices.indexOf(index)
138+
const el = listInner.current!.children[indexInVisible] as HTMLElement
139+
if (el) {
140+
// @ts-ignore
141+
el.scrollIntoView({ block })
142+
ignoreScrollOnce.current = true
143+
}
144+
}
145+
}, [visibleIndices])
110146
//
111147
return <div ref={list} onScroll={handleScroll} className={props.className} style={{ overflow: 'auto', ...props.style }}>
112148
<div ref={listInner} style={{ display: 'flex', flexDirection: 'column', ...listInnerStyle }}>
113-
{visible.map((item, i) => props.renderItem(item, visibleIndexes[i]))}
149+
{visible.map((item, i) => props.renderItem(item, visibleIndices[i]))}
114150
</div>
115151
</div>
116152
})

src/examples/scrollToIndex.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React, {
2+
useRef
3+
} from 'react';
4+
import exampleData from './example_data.json'
5+
import { VirtualList, VirtualListHandle } from '../VirtualList'
6+
7+
export default function ScrollToIndexExample() {
8+
const ref = useRef<VirtualListHandle>(null);
9+
return <>
10+
<h2>scroll to index</h2>
11+
<ul>
12+
<li><button onClick={() => ref.current!.scrollToIndex(100)}>scroll to 100</button></li>
13+
<li><button onClick={() => ref.current!.scrollToIndex(233)}>scroll to 233</button></li>
14+
<li><button onClick={() => ref.current!.scrollToIndex(567)}>scroll to 567</button></li>
15+
<li><button onClick={() => ref.current!.scrollToIndex(761)}>scroll to 761</button></li>
16+
<li><button onClick={() => ref.current!.scrollToIndex(999)}>scroll to 999</button></li>
17+
</ul>
18+
<VirtualList
19+
ref={ref}
20+
items={exampleData}
21+
style={{ height: '600px', border: '1px solid #ccc', padding: '10px' }}
22+
renderItem={(item, index) => <div key={index} style={{ marginBottom: '10px' }}>
23+
<h3>{index}. {item.headline}</h3>
24+
<div>
25+
<div style={{ float: 'left', width: '100px', height: '100px', background: '#f0f0f0', borderRadius: '5px', marginRight: '10px' }}></div>
26+
{item.content}
27+
</div>
28+
</div>}
29+
></VirtualList>
30+
<br />
31+
<a href="https://github.com/phphe/react-base-virtual-list/blob/main/src/examples/scrollToIndex.tsx">Source Code</a>
32+
</>
33+
}

0 commit comments

Comments
 (0)