Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,7 @@
{ "label": "Composable Tables (createTableHook)", "to": "framework/react/examples/composable-tables" },
{ "label": "Custom Plugin", "to": "framework/react/examples/custom-plugin" },
{ "label": "Experimental Web Workers Plugin", "to": "framework/react/examples/web-worker-row-models" },
{ "label": "Experimental Spreadsheet", "to": "framework/react/examples/spreadsheet" },
{ "label": "With TanStack Virtual - Columns", "to": "framework/react/examples/virtualized-columns" },
{ "label": "With TanStack Virtual - Columns (Exp)", "to": "framework/react/examples/virtualized-columns-experimental" },
{ "label": "With TanStack Virtual - Rows", "to": "framework/react/examples/virtualized-rows" },
Expand Down
12 changes: 12 additions & 0 deletions examples/react/spreadsheet/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TanStack Table Spreadsheet</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
31 changes: 31 additions & 0 deletions examples/react/spreadsheet/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "tanstack-react-table-example-spreadsheet",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"start": "vite",
"lint": "eslint ./src",
"test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts",
"test:types": "tsc"
},
"dependencies": {
"@faker-js/faker": "^10.5.0",
"@tanstack/react-store": "^0.11.0",
"@tanstack/react-table": "^9.0.0-beta.58",
"@tanstack/react-virtual": "^3.14.5",
"react": "^19.2.7",
"react-dom": "^19.2.7"
},
"devDependencies": {
"@rolldown/plugin-babel": "^0.2.3",
"@rollup/plugin-replace": "^6.0.3",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"babel-plugin-react-compiler": "^1.0.0",
"typescript": "6.0.3",
"vite": "^8.1.0"
}
}
111 changes: 111 additions & 0 deletions examples/react/spreadsheet/src/CellContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react'
import { createPortal } from 'react-dom'
import type { GridInteractions } from './useGridInteractions'
import type {
SpreadsheetTable,
SpreadsheetTableColumn,
} from './spreadsheetTable'

interface CellContextMenuProps {
x: number
y: number
column: SpreadsheetTableColumn
table: SpreadsheetTable
interactions: GridInteractions
onClose: () => void
}

export function CellContextMenu({
x,
y,
column,
table,
interactions,
onClose,
}: CellContextMenuProps) {
const menuRef = React.useRef<HTMLDivElement>(null)

React.useEffect(() => {
const handlePointerDown = (event: PointerEvent) => {
if (!menuRef.current?.contains(event.target as Node)) onClose()
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose()
}

document.addEventListener('pointerdown', handlePointerDown)
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('pointerdown', handlePointerDown)
document.removeEventListener('keydown', handleKeyDown)
}
}, [onClose])

const run = (action: () => void | Promise<void>) => {
onClose()
void action()
}

return createPortal(
<div
ref={menuRef}
className="cell-context-menu"
role="menu"
aria-label="Cell actions"
style={{
left: Math.min(x, window.innerWidth - 218),
top: Math.min(y, window.innerHeight - 286),
}}
>
<button
type="button"
role="menuitem"
onClick={() => run(interactions.cutToClipboard)}
>
<span>✂</span> Cut <kbd>Ctrl+X</kbd>
</button>
<button
type="button"
role="menuitem"
onClick={() => run(interactions.copyToClipboard)}
>
<span>▣</span> Copy <kbd>Ctrl+C</kbd>
</button>
<button
type="button"
role="menuitem"
onClick={() => run(interactions.pasteFromClipboard)}
>
<span>▤</span> Paste <kbd>Ctrl+V</kbd>
</button>
<div className="menu-rule" />
<button
type="button"
role="menuitem"
onClick={() => run(interactions.clearSelection)}
>
<span>⌫</span> Clear contents
</button>
<div className="menu-rule" />
<button
type="button"
role="menuitem"
onClick={() =>
run(() => table.setSorting([{ id: column.id, desc: false }]))
}
>
<span>↑</span> Sort ascending
</button>
<button
type="button"
role="menuitem"
onClick={() =>
run(() => table.setSorting([{ id: column.id, desc: true }]))
}
>
<span>↓</span> Sort descending
</button>
</div>,
document.body,
)
}
104 changes: 104 additions & 0 deletions examples/react/spreadsheet/src/ColumnMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from 'react'
import { createPortal } from 'react-dom'
import type {
SpreadsheetTable,
SpreadsheetTableColumn,
} from './spreadsheetTable'

interface ColumnMenuProps {
anchorRect: DOMRect
column: SpreadsheetTableColumn
table: SpreadsheetTable
onClose: () => void
}

export function ColumnMenu({
anchorRect,
column,
table,
onClose,
}: ColumnMenuProps) {
const menuRef = React.useRef<HTMLDivElement>(null)
const filterValue = String(column.getFilterValue() ?? '')
const left = Math.min(anchorRect.left, window.innerWidth - 286)

React.useEffect(() => {
const handlePointerDown = (event: PointerEvent) => {
if (!menuRef.current?.contains(event.target as Node)) onClose()
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose()
}

document.addEventListener('pointerdown', handlePointerDown)
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('pointerdown', handlePointerDown)
document.removeEventListener('keydown', handleKeyDown)
}
}, [onClose])

return createPortal(
<div
ref={menuRef}
className="column-menu"
role="dialog"
aria-label={`Column ${column.columnDef.meta?.letter ?? column.id} options`}
style={{
left: Math.max(8, left),
top: Math.min(anchorRect.bottom + 4, window.innerHeight - 250),
}}
>
<div className="column-menu-title">
<span>{column.columnDef.meta?.letter}</span>
<strong>{column.columnDef.meta?.label}</strong>
</div>
<button
type="button"
onClick={() => {
table.setSorting([{ id: column.id, desc: false }])
onClose()
}}
>
Sort A → Z
</button>
<button
type="button"
onClick={() => {
table.setSorting([{ id: column.id, desc: true }])
onClose()
}}
>
Sort Z → A
</button>
<button
type="button"
disabled={!column.getIsSorted()}
onClick={() => {
column.clearSorting()
onClose()
}}
>
Clear sort
</button>
<div className="column-menu-separator" />
<label>
Filter values containing
<input
autoFocus
value={filterValue}
onChange={(event) => column.setFilterValue(event.target.value)}
placeholder="Type to filter…"
/>
</label>
<button
type="button"
disabled={!filterValue}
onClick={() => column.setFilterValue(undefined)}
>
Clear this filter
</button>
</div>,
document.body,
)
}
Loading
Loading