Skip to content

Commit 0df4675

Browse files
authored
docs: Spreadsheet example (TanStack#6457)
* docs: add experimental spreadsheet example * add autofit
1 parent dbff432 commit 0df4675

18 files changed

Lines changed: 4759 additions & 0 deletions

docs/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,7 @@
13811381
{ "label": "Composable Tables (createTableHook)", "to": "framework/react/examples/composable-tables" },
13821382
{ "label": "Custom Plugin", "to": "framework/react/examples/custom-plugin" },
13831383
{ "label": "Experimental Web Workers Plugin", "to": "framework/react/examples/web-worker-row-models" },
1384+
{ "label": "Experimental Spreadsheet", "to": "framework/react/examples/spreadsheet" },
13841385
{ "label": "With TanStack Virtual - Columns", "to": "framework/react/examples/virtualized-columns" },
13851386
{ "label": "With TanStack Virtual - Columns (Exp)", "to": "framework/react/examples/virtualized-columns-experimental" },
13861387
{ "label": "With TanStack Virtual - Rows", "to": "framework/react/examples/virtualized-rows" },
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>TanStack Table Spreadsheet</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "tanstack-react-table-example-spreadsheet",
3+
"private": true,
4+
"scripts": {
5+
"dev": "vite",
6+
"build": "vite build",
7+
"serve": "vite preview",
8+
"start": "vite",
9+
"lint": "eslint ./src",
10+
"test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts",
11+
"test:types": "tsc"
12+
},
13+
"dependencies": {
14+
"@faker-js/faker": "^10.5.0",
15+
"@tanstack/react-store": "^0.11.0",
16+
"@tanstack/react-table": "^9.0.0-beta.58",
17+
"@tanstack/react-virtual": "^3.14.5",
18+
"react": "^19.2.7",
19+
"react-dom": "^19.2.7"
20+
},
21+
"devDependencies": {
22+
"@rolldown/plugin-babel": "^0.2.3",
23+
"@rollup/plugin-replace": "^6.0.3",
24+
"@types/react": "^19.2.17",
25+
"@types/react-dom": "^19.2.3",
26+
"@vitejs/plugin-react": "^6.0.3",
27+
"babel-plugin-react-compiler": "^1.0.0",
28+
"typescript": "6.0.3",
29+
"vite": "^8.1.0"
30+
}
31+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React from 'react'
2+
import { createPortal } from 'react-dom'
3+
import type { GridInteractions } from './useGridInteractions'
4+
import type {
5+
SpreadsheetTable,
6+
SpreadsheetTableColumn,
7+
} from './spreadsheetTable'
8+
9+
interface CellContextMenuProps {
10+
x: number
11+
y: number
12+
column: SpreadsheetTableColumn
13+
table: SpreadsheetTable
14+
interactions: GridInteractions
15+
onClose: () => void
16+
}
17+
18+
export function CellContextMenu({
19+
x,
20+
y,
21+
column,
22+
table,
23+
interactions,
24+
onClose,
25+
}: CellContextMenuProps) {
26+
const menuRef = React.useRef<HTMLDivElement>(null)
27+
28+
React.useEffect(() => {
29+
const handlePointerDown = (event: PointerEvent) => {
30+
if (!menuRef.current?.contains(event.target as Node)) onClose()
31+
}
32+
const handleKeyDown = (event: KeyboardEvent) => {
33+
if (event.key === 'Escape') onClose()
34+
}
35+
36+
document.addEventListener('pointerdown', handlePointerDown)
37+
document.addEventListener('keydown', handleKeyDown)
38+
return () => {
39+
document.removeEventListener('pointerdown', handlePointerDown)
40+
document.removeEventListener('keydown', handleKeyDown)
41+
}
42+
}, [onClose])
43+
44+
const run = (action: () => void | Promise<void>) => {
45+
onClose()
46+
void action()
47+
}
48+
49+
return createPortal(
50+
<div
51+
ref={menuRef}
52+
className="cell-context-menu"
53+
role="menu"
54+
aria-label="Cell actions"
55+
style={{
56+
left: Math.min(x, window.innerWidth - 218),
57+
top: Math.min(y, window.innerHeight - 286),
58+
}}
59+
>
60+
<button
61+
type="button"
62+
role="menuitem"
63+
onClick={() => run(interactions.cutToClipboard)}
64+
>
65+
<span></span> Cut <kbd>Ctrl+X</kbd>
66+
</button>
67+
<button
68+
type="button"
69+
role="menuitem"
70+
onClick={() => run(interactions.copyToClipboard)}
71+
>
72+
<span></span> Copy <kbd>Ctrl+C</kbd>
73+
</button>
74+
<button
75+
type="button"
76+
role="menuitem"
77+
onClick={() => run(interactions.pasteFromClipboard)}
78+
>
79+
<span></span> Paste <kbd>Ctrl+V</kbd>
80+
</button>
81+
<div className="menu-rule" />
82+
<button
83+
type="button"
84+
role="menuitem"
85+
onClick={() => run(interactions.clearSelection)}
86+
>
87+
<span></span> Clear contents
88+
</button>
89+
<div className="menu-rule" />
90+
<button
91+
type="button"
92+
role="menuitem"
93+
onClick={() =>
94+
run(() => table.setSorting([{ id: column.id, desc: false }]))
95+
}
96+
>
97+
<span></span> Sort ascending
98+
</button>
99+
<button
100+
type="button"
101+
role="menuitem"
102+
onClick={() =>
103+
run(() => table.setSorting([{ id: column.id, desc: true }]))
104+
}
105+
>
106+
<span></span> Sort descending
107+
</button>
108+
</div>,
109+
document.body,
110+
)
111+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import React from 'react'
2+
import { createPortal } from 'react-dom'
3+
import type {
4+
SpreadsheetTable,
5+
SpreadsheetTableColumn,
6+
} from './spreadsheetTable'
7+
8+
interface ColumnMenuProps {
9+
anchorRect: DOMRect
10+
column: SpreadsheetTableColumn
11+
table: SpreadsheetTable
12+
onClose: () => void
13+
}
14+
15+
export function ColumnMenu({
16+
anchorRect,
17+
column,
18+
table,
19+
onClose,
20+
}: ColumnMenuProps) {
21+
const menuRef = React.useRef<HTMLDivElement>(null)
22+
const filterValue = String(column.getFilterValue() ?? '')
23+
const left = Math.min(anchorRect.left, window.innerWidth - 286)
24+
25+
React.useEffect(() => {
26+
const handlePointerDown = (event: PointerEvent) => {
27+
if (!menuRef.current?.contains(event.target as Node)) onClose()
28+
}
29+
const handleKeyDown = (event: KeyboardEvent) => {
30+
if (event.key === 'Escape') onClose()
31+
}
32+
33+
document.addEventListener('pointerdown', handlePointerDown)
34+
document.addEventListener('keydown', handleKeyDown)
35+
return () => {
36+
document.removeEventListener('pointerdown', handlePointerDown)
37+
document.removeEventListener('keydown', handleKeyDown)
38+
}
39+
}, [onClose])
40+
41+
return createPortal(
42+
<div
43+
ref={menuRef}
44+
className="column-menu"
45+
role="dialog"
46+
aria-label={`Column ${column.columnDef.meta?.letter ?? column.id} options`}
47+
style={{
48+
left: Math.max(8, left),
49+
top: Math.min(anchorRect.bottom + 4, window.innerHeight - 250),
50+
}}
51+
>
52+
<div className="column-menu-title">
53+
<span>{column.columnDef.meta?.letter}</span>
54+
<strong>{column.columnDef.meta?.label}</strong>
55+
</div>
56+
<button
57+
type="button"
58+
onClick={() => {
59+
table.setSorting([{ id: column.id, desc: false }])
60+
onClose()
61+
}}
62+
>
63+
Sort A → Z
64+
</button>
65+
<button
66+
type="button"
67+
onClick={() => {
68+
table.setSorting([{ id: column.id, desc: true }])
69+
onClose()
70+
}}
71+
>
72+
Sort Z → A
73+
</button>
74+
<button
75+
type="button"
76+
disabled={!column.getIsSorted()}
77+
onClick={() => {
78+
column.clearSorting()
79+
onClose()
80+
}}
81+
>
82+
Clear sort
83+
</button>
84+
<div className="column-menu-separator" />
85+
<label>
86+
Filter values containing
87+
<input
88+
autoFocus
89+
value={filterValue}
90+
onChange={(event) => column.setFilterValue(event.target.value)}
91+
placeholder="Type to filter…"
92+
/>
93+
</label>
94+
<button
95+
type="button"
96+
disabled={!filterValue}
97+
onClick={() => column.setFilterValue(undefined)}
98+
>
99+
Clear this filter
100+
</button>
101+
</div>,
102+
document.body,
103+
)
104+
}

0 commit comments

Comments
 (0)