Skip to content

Commit 4f05dd8

Browse files
committed
chore: Add resizable and padding lanes with dynamic lane count calculation
1 parent 0ccb3cb commit 4f05dd8

File tree

3 files changed

+238
-6
lines changed

3 files changed

+238
-6
lines changed

examples/react/lanes/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"serve": "vite preview"
99
},
1010
"dependencies": {
11+
"@tanstack/react-pacer": "^0.2.0",
1112
"@tanstack/react-virtual": "^3.13.6",
1213
"react": "^18.3.1",
1314
"react-dom": "^18.3.1"

examples/react/lanes/src/main.tsx

Lines changed: 184 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as ReactDOM from 'react-dom/client'
44
import './index.css'
55

66
import { useVirtualizer } from '@tanstack/react-virtual'
7+
import { debounce } from '@tanstack/react-pacer'
78

89
function App() {
910
return (
@@ -16,7 +17,17 @@ function App() {
1617
<br />
1718

1819
<h3>Lanes</h3>
19-
<LanesVirtualizer />
20+
<LanesVirtualizer />
21+
<br />
22+
<br />
23+
<h3>Padding Lanes</h3>
24+
<PaddingVirtualizer />
25+
<br />
26+
<br />
27+
<h3>Resizable Lanes</h3>
28+
<ResizeVirtualizer />
29+
<br />
30+
<br />
2031
<br />
2132
<br />
2233
{process.env.NODE_ENV === 'development' ? (
@@ -30,21 +41,25 @@ function App() {
3041
)
3142
}
3243

33-
const NUM_LANES = 5
34-
3544
function LanesVirtualizer() {
45+
const [numLanes, setNumLanes] = React.useState(4)
3646
const parentRef = React.useRef(null)
3747

3848
const rowVirtualizer = useVirtualizer({
3949
count: 10000,
4050
getScrollElement: () => parentRef.current,
4151
estimateSize: () => 35,
4252
overscan: 5,
43-
lanes: NUM_LANES,
53+
lanes: numLanes,
4454
})
4555

4656
return (
4757
<>
58+
<div style={{ display: 'grid', gridTemplateColumns: '80px 200px', gap: '10px' }}>
59+
<label htmlFor="numLanes1">Num Lanes</label>
60+
<input type="number" id="numLanes1" value={numLanes} onChange={(e) => {setNumLanes(Number(e.target.value)); rowVirtualizer.measure()}} />
61+
</div>
62+
<br />
4863
<div
4964
ref={parentRef}
5065
className="List"
@@ -68,8 +83,8 @@ function LanesVirtualizer() {
6883
style={{
6984
position: 'absolute',
7085
top: 0,
71-
left: `calc(${virtualRow.index % NUM_LANES} * 100% / ${NUM_LANES})`,
72-
width: `calc(100% / ${NUM_LANES})`,
86+
left: `calc(${virtualRow.index % numLanes} * 100% / ${numLanes})`,
87+
width: `calc(100% / ${numLanes})`,
7388
height: `${virtualRow.size}px`,
7489
transform: `translateY(${virtualRow.start}px)`,
7590
}}
@@ -83,6 +98,169 @@ function LanesVirtualizer() {
8398
)
8499
}
85100

101+
function PaddingVirtualizer() {
102+
const parentRef = React.useRef<HTMLDivElement>(null)
103+
const [numLanes, setNumLanes] = React.useState(4)
104+
const [rowGap, setRowGap] = React.useState(10)
105+
const [columnGap, setColumnGap] = React.useState(10)
106+
107+
const rowVirtualizer = useVirtualizer({
108+
count: 10000,
109+
getScrollElement: () => parentRef.current,
110+
estimateSize: () => 35,
111+
overscan: 5,
112+
lanes: numLanes,
113+
gap: rowGap,
114+
})
115+
116+
return (
117+
<>
118+
<div style={{ display: 'grid', gridTemplateColumns: '80px 200px', gap: '10px' }}>
119+
<label htmlFor="numLanes2">Num Lanes</label>
120+
<input type="number" id="numLanes2" value={numLanes} onChange={(e) => {setNumLanes(Number(e.target.value)); rowVirtualizer.measure()}} />
121+
<label htmlFor="rowGap" >Row Gap</label>
122+
<input type="number" id="rowGap" value={rowGap} onChange={(e) => {setRowGap(Number(e.target.value)); rowVirtualizer.measure()}} />
123+
<label htmlFor="columnGap">Column Gap</label>
124+
<input type="number" id="columnGap" value={columnGap} onChange={(e) => {setColumnGap(Number(e.target.value)); rowVirtualizer.measure()}} />
125+
</div>
126+
<br />
127+
128+
<div
129+
ref={parentRef}
130+
className="List"
131+
style={{
132+
height: "200px",
133+
width: "400px",
134+
overflow: "auto",
135+
}}
136+
>
137+
<div
138+
style={{
139+
height: `${rowVirtualizer.getTotalSize()}px`,
140+
width: '100%',
141+
position: 'relative',
142+
}}
143+
>
144+
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
145+
return (
146+
<div
147+
key={virtualRow.index}
148+
className={virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'}
149+
style={{
150+
position: 'absolute',
151+
top: 0,
152+
left: `calc((${virtualRow.index % numLanes} * 100% / ${numLanes}) + (${columnGap}px * (${virtualRow.index % numLanes}) / ${numLanes}))`,
153+
width: `calc((100% / ${numLanes}) - (${columnGap}px * (${numLanes} - 1) / ${numLanes}))`,
154+
height: `${virtualRow.size}px`,
155+
transform: `translateY(${virtualRow.start}px)`,
156+
outline: '1px solid red',
157+
}}
158+
>
159+
Cell {virtualRow.index}
160+
</div>
161+
)
162+
})}
163+
</div>
164+
</div>
165+
</>
166+
)
167+
}
168+
169+
const CELL_WIDTH = 100
170+
function ResizeVirtualizer() {
171+
const parentRef = React.useRef<HTMLDivElement>(null)
172+
const [numLanes, setNumLanes] = React.useState(4)
173+
const [rowGap, setRowGap] = React.useState(10)
174+
const [columnGap, setColumnGap] = React.useState(10)
175+
176+
const rowVirtualizer = useVirtualizer({
177+
count: 10000,
178+
getScrollElement: () => parentRef.current,
179+
estimateSize: () => 35,
180+
overscan: 5,
181+
lanes: numLanes,
182+
gap: rowGap,
183+
})
184+
185+
React.useEffect(() => {
186+
if (!parentRef.current) return
187+
const debouncedOnResize = debounce((entries: Array<ResizeObserverEntry>) => {
188+
const rect = entries.at(0)?.contentRect
189+
if (!rect) return
190+
const { width } = rect
191+
setNumLanes(Math.floor(width / CELL_WIDTH))
192+
rowVirtualizer.measure()
193+
}, {
194+
wait: 100,
195+
196+
})
197+
const resizeObserver = new ResizeObserver((entries) => {
198+
debouncedOnResize(entries)
199+
})
200+
resizeObserver.observe(parentRef.current)
201+
return () => {
202+
resizeObserver.disconnect()
203+
}
204+
}, [rowVirtualizer])
205+
206+
207+
208+
return (
209+
<>
210+
<div style={{ display: 'grid', gridTemplateColumns: '80px 200px', gap: '10px' }}>
211+
<label htmlFor="numLanes2">Num Lanes</label>
212+
<input type="number" id="numLanes2" value={numLanes} readOnly disabled/>
213+
<label htmlFor="rowGap" >Row Gap</label>
214+
<input type="number" id="rowGap" value={rowGap} onChange={(e) => {setRowGap(Number(e.target.value)); rowVirtualizer.measure()}} />
215+
<label htmlFor="columnGap">Column Gap</label>
216+
<input type="number" id="columnGap" value={columnGap} onChange={(e) => {setColumnGap(Number(e.target.value)); rowVirtualizer.measure()}} />
217+
</div>
218+
<br />
219+
220+
<div
221+
ref={parentRef}
222+
className="List"
223+
style={{
224+
height: "200px",
225+
width: "400px",
226+
overflow: "auto",
227+
minWidth: CELL_WIDTH,
228+
minHeight: "35px",
229+
resize: 'horizontal',
230+
}}
231+
>
232+
<div
233+
style={{
234+
height: `${rowVirtualizer.getTotalSize()}px`,
235+
width: '100%',
236+
position: 'relative',
237+
}}
238+
>
239+
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
240+
return (
241+
<div
242+
key={virtualRow.index}
243+
className={virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'}
244+
style={{
245+
position: 'absolute',
246+
top: 0,
247+
left: `calc((${virtualRow.index % numLanes} * 100% / ${numLanes}) + (${columnGap}px * (${virtualRow.index % numLanes}) / ${numLanes}))`,
248+
width: `calc((100% / ${numLanes}) - (${columnGap}px * (${numLanes} - 1) / ${numLanes}))`,
249+
height: `${virtualRow.size}px`,
250+
transform: `translateY(${virtualRow.start}px)`,
251+
outline: '1px solid red',
252+
}}
253+
>
254+
Cell {virtualRow.index}
255+
</div>
256+
)
257+
})}
258+
</div>
259+
</div>
260+
</>
261+
)
262+
}
263+
86264
// eslint-disable-next-line
87265
ReactDOM.createRoot(document.getElementById('root')!).render(
88266
<React.StrictMode>

pnpm-lock.yaml

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)