Skip to content

Commit d21ed98

Browse files
authored
fix(virtual-core): scroll to index doesn't scroll to bottom correctly (#1029)
1 parent 2c70382 commit d21ed98

File tree

16 files changed

+283
-66
lines changed

16 files changed

+283
-66
lines changed

.changeset/chilled-falcons-battle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/virtual-core': patch
3+
---
4+
5+
fix(virtual-core): scroll to index doesn't scroll to bottom correctly

.github/workflows/pr.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ jobs:
2828
uses: nrwl/[email protected]
2929
with:
3030
main-branch-name: main
31+
- name: Install Playwright browsers
32+
run: pnpm exec playwright install chromium
3133
- name: Run Checks
3234
run: pnpm run test:pr
3335
preview:

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,8 @@ stats.html
4141

4242
vite.config.js.timestamp-*
4343
vite.config.ts.timestamp-*
44+
45+
# Playwright test artifacts
46+
test-results/
47+
playwright-report/
48+
*.log

knip.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"$schema": "https://unpkg.com/knip@5/schema.json",
3-
"ignoreWorkspaces": ["examples/**"]
3+
"ignoreWorkspaces": ["examples/**", "packages/react-virtual/e2e/**"]
44
}

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@
1111
"clean": "pnpm --filter \"./packages/**\" run clean",
1212
"preinstall": "node -e \"if(process.env.CI == 'true') {console.log('Skipping preinstall...'); process.exit(1)}\" || npx -y only-allow pnpm",
1313
"test": "pnpm run test:ci",
14-
"test:pr": "nx affected --targets=test:sherif,test:knip,test:eslint,test:lib,test:types,test:build,build",
15-
"test:ci": "nx run-many --targets=test:sherif,test:knip,test:eslint,test:lib,test:types,test:build,build",
14+
"test:pr": "nx affected --targets=test:sherif,test:knip,test:eslint,test:lib,test:e2e,test:types,test:build,build",
15+
"test:ci": "nx run-many --targets=test:sherif,test:knip,test:eslint,test:lib,test:e2e,test:types,test:build,build",
1616
"test:eslint": "nx affected --target=test:eslint",
1717
"test:format": "pnpm run prettier --check",
1818
"test:sherif": "sherif",
1919
"test:lib": "nx affected --target=test:lib --exclude=examples/**",
2020
"test:lib:dev": "pnpm run test:lib && nx watch --all -- pnpm run test:lib",
2121
"test:build": "nx affected --target=test:build --exclude=examples/**",
2222
"test:types": "nx affected --target=test:types --exclude=examples/**",
23+
"test:e2e": "nx affected --target=test:e2e --exclude=examples/**",
2324
"test:knip": "knip",
2425
"build": "nx affected --target=build --exclude=examples/**",
2526
"build:all": "nx run-many --target=build --exclude=examples/**",
@@ -39,6 +40,7 @@
3940
},
4041
"devDependencies": {
4142
"@changesets/cli": "^2.29.4",
43+
"@playwright/test": "^1.53.1",
4244
"@svitejs/changesets-changelog-github-compact": "^1.2.0",
4345
"@tanstack/config": "^0.18.2",
4446
"@testing-library/jest-dom": "^6.6.3",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
</head>
6+
<body>
7+
<div id="root"></div>
8+
<script type="module" src="/main.tsx"></script>
9+
</body>
10+
</html>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from 'react'
2+
import ReactDOM from 'react-dom/client'
3+
import { useVirtualizer } from '../../src/index'
4+
5+
function getRandomInt(min: number, max: number) {
6+
return Math.floor(Math.random() * (max - min + 1)) + min
7+
}
8+
9+
const randomHeight = (() => {
10+
const cache = new Map()
11+
return (id: string) => {
12+
const value = cache.get(id)
13+
if (value !== undefined) {
14+
return value
15+
}
16+
const v = getRandomInt(25, 100)
17+
cache.set(id, v)
18+
return v
19+
}
20+
})()
21+
22+
const App = () => {
23+
const parentRef = React.useRef<HTMLDivElement>(null)
24+
const rowVirtualizer = useVirtualizer({
25+
count: 1002,
26+
getScrollElement: () => parentRef.current,
27+
estimateSize: () => 50,
28+
debug: true,
29+
})
30+
31+
return (
32+
<div>
33+
<button
34+
id="scroll-to-1000"
35+
onClick={() => rowVirtualizer.scrollToIndex(1000)}
36+
>
37+
Scroll to 1000
38+
</button>
39+
40+
<div
41+
ref={parentRef}
42+
id="scroll-container"
43+
style={{ height: 400, overflow: 'auto' }}
44+
>
45+
<div
46+
style={{
47+
height: rowVirtualizer.getTotalSize(),
48+
position: 'relative',
49+
}}
50+
>
51+
{rowVirtualizer.getVirtualItems().map((v) => (
52+
<div
53+
key={v.key}
54+
data-testid={`item-${v.index}`}
55+
ref={rowVirtualizer.measureElement}
56+
data-index={v.index}
57+
style={{
58+
position: 'absolute',
59+
top: 0,
60+
left: 0,
61+
transform: `translateY(${v.start}px)`,
62+
width: '100%',
63+
}}
64+
>
65+
<div style={{ height: randomHeight(String(v.key)) }}>
66+
Row {v.index}
67+
</div>
68+
</div>
69+
))}
70+
</div>
71+
</div>
72+
</div>
73+
)
74+
}
75+
76+
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { expect, test } from '@playwright/test'
2+
3+
const check = () => {
4+
const item = document.querySelector('[data-testid="item-1000"]')
5+
const container = document.querySelector('#scroll-container')
6+
7+
if (!item || !container) throw new Error('Elements not found')
8+
9+
const itemRect = item.getBoundingClientRect()
10+
const containerRect = container.getBoundingClientRect()
11+
const scrollTop = container.scrollTop
12+
13+
const top = itemRect.top + scrollTop - containerRect.top
14+
const botttom = top + itemRect.height
15+
16+
const containerBottom = scrollTop + container.clientHeight
17+
18+
return Math.abs(botttom - containerBottom)
19+
}
20+
21+
test('scrolls to index 1000', async ({ page }) => {
22+
await page.goto('/')
23+
await page.click('#scroll-to-1000')
24+
25+
// Wait for scroll effect (including retries)
26+
await page.waitForTimeout(1000)
27+
28+
await expect(page.locator('[data-testid="item-1000"]')).toBeVisible()
29+
30+
const delta = await page.evaluate(check)
31+
console.log('bootom element detla', delta)
32+
expect(delta).toBeLessThan(1.01)
33+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"esModuleInterop": true,
5+
"jsx": "react-jsx",
6+
"target": "ESNext",
7+
"moduleResolution": "Bundler",
8+
"module": "ESNext",
9+
"resolveJsonModule": true,
10+
"allowJs": true,
11+
"skipLibCheck": true
12+
},
13+
"exclude": ["node_modules", "dist"]
14+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from 'vite'
2+
import react from '@vitejs/plugin-react'
3+
4+
export default defineConfig({
5+
root: __dirname,
6+
plugins: [react()],
7+
})

0 commit comments

Comments
 (0)