From babb62f63506866daf93805a1024cc346230ca1b Mon Sep 17 00:00:00 2001 From: Kevin Vandy Date: Fri, 16 Feb 2024 17:14:05 -0600 Subject: [PATCH] docs: replace react-dnd with dnd-kit in row ordering example --- examples/react/row-dnd/package.json | 6 +- examples/react/row-dnd/src/index.css | 1 + examples/react/row-dnd/src/main.tsx | 284 +++++++++++++------------ examples/react/row-dnd/src/makeData.ts | 4 +- pnpm-lock.yaml | 73 ++++++- 5 files changed, 227 insertions(+), 141 deletions(-) diff --git a/examples/react/row-dnd/package.json b/examples/react/row-dnd/package.json index 9533b58ca0..a8b68bedb1 100644 --- a/examples/react/row-dnd/package.json +++ b/examples/react/row-dnd/package.json @@ -9,11 +9,13 @@ "start": "vite" }, "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/modifiers": "^7.0.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@faker-js/faker": "^8.3.1", "@tanstack/react-table": "^8.12.0", "react": "^18.2.0", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0" }, "devDependencies": { diff --git a/examples/react/row-dnd/src/index.css b/examples/react/row-dnd/src/index.css index 13cddcd1a5..538251302f 100644 --- a/examples/react/row-dnd/src/index.css +++ b/examples/react/row-dnd/src/index.css @@ -20,6 +20,7 @@ th { td { border-right: 1px solid lightgray; padding: 2px 4px; + background-color: white; } td button { diff --git a/examples/react/row-dnd/src/main.tsx b/examples/react/row-dnd/src/main.tsx index 9a7be35740..dd9d0ebddb 100644 --- a/examples/react/row-dnd/src/main.tsx +++ b/examples/react/row-dnd/src/main.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react' +import React, { CSSProperties } from 'react' import ReactDOM from 'react-dom/client' import './index.css' @@ -12,103 +12,113 @@ import { } from '@tanstack/react-table' import { makeData, Person } from './makeData' -import { DndProvider, useDrag, useDrop } from 'react-dnd' -import { HTML5Backend } from 'react-dnd-html5-backend' +// needed for table body level scope DnD setup +import { + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + closestCenter, + type DragEndEvent, + type UniqueIdentifier, + useSensor, + useSensors, +} from '@dnd-kit/core' +import { restrictToVerticalAxis } from '@dnd-kit/modifiers' +import { + arrayMove, + SortableContext, + verticalListSortingStrategy, +} from '@dnd-kit/sortable' + +// needed for row & cell level scope DnD setup +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' + +// Cell Component +const RowDragHandleCell = ({ rowId }: { rowId: string }) => { + const { attributes, listeners } = useSortable({ + id: rowId, + }) + return ( + // Alternatively, you could set these attributes on the rows themselves + + ) +} + +// Row Component +const DraggableRow = ({ row }: { row: Row }) => { + const { transform, transition, setNodeRef, isDragging } = useSortable({ + id: row.original.userId, + }) + + const style: CSSProperties = { + transform: CSS.Transform.toString(transform), + transition: transition, //let dnd-kit do its thing + opacity: isDragging ? 0.8 : 1, + zIndex: isDragging ? 1 : 0, + position: 'relative', + } + return ( + // connect row ref to dnd-kit, apply important styles + + {row.getVisibleCells().map(cell => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ) +} -const defaultColumns: ColumnDef[] = [ - { - header: 'Name', - footer: props => props.column.id, - columns: [ +// Table Component +function App() { + const columns = React.useMemo[]>( + () => [ + // Create a dedicated drag handle column. Alternatively, you could just set up dnd events on the rows themselves. + { + id: 'drag-handle', + header: 'Move', + cell: ({ row }) => , + size: 60, + }, { accessorKey: 'firstName', cell: info => info.getValue(), - footer: props => props.column.id, }, { accessorFn: row => row.lastName, id: 'lastName', cell: info => info.getValue(), header: () => Last Name, - footer: props => props.column.id, }, - ], - }, - { - header: 'Info', - footer: props => props.column.id, - columns: [ { accessorKey: 'age', header: () => 'Age', - footer: props => props.column.id, }, { - header: 'More Info', - columns: [ - { - accessorKey: 'visits', - header: () => Visits, - footer: props => props.column.id, - }, - { - accessorKey: 'status', - header: 'Status', - footer: props => props.column.id, - }, - { - accessorKey: 'progress', - header: 'Profile Progress', - footer: props => props.column.id, - }, - ], + accessorKey: 'visits', + header: () => Visits, + }, + { + accessorKey: 'status', + header: 'Status', + }, + { + accessorKey: 'progress', + header: 'Profile Progress', }, ], - }, -] - -const DraggableRow: FC<{ - row: Row - reorderRow: (draggedRowIndex: number, targetRowIndex: number) => void -}> = ({ row, reorderRow }) => { - const [, dropRef] = useDrop({ - accept: 'row', - drop: (draggedRow: Row) => reorderRow(draggedRow.index, row.index), - }) - - const [{ isDragging }, dragRef, previewRef] = useDrag({ - collect: monitor => ({ - isDragging: monitor.isDragging(), - }), - item: () => row, - type: 'row', - }) - - return ( - - - - - {row.getVisibleCells().map(cell => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - + [] ) -} - -function App() { - const [columns] = React.useState(() => [...defaultColumns]) const [data, setData] = React.useState(() => makeData(20)) - const reorderRow = (draggedRowIndex: number, targetRowIndex: number) => { - data.splice(targetRowIndex, 0, data.splice(draggedRowIndex, 1)[0] as Person) - setData([...data]) - } + const dataIds = React.useMemo( + () => data?.map(({ userId }) => userId), + [data] + ) const rerender = () => setData(() => makeData(20)) @@ -116,63 +126,77 @@ function App() { data, columns, getCoreRowModel: getCoreRowModel(), - getRowId: row => row.userId, //good to have guaranteed unique row ids/keys for rendering + getRowId: row => row.userId, //required because row indexes will change debugTable: true, debugHeaders: true, debugColumns: true, }) + // reorder columns after drag & drop + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event + if (active && over && active.id !== over.id) { + setData(data => { + const oldIndex = dataIds.indexOf(active.id) + const newIndex = dataIds.indexOf(over.id) + return arrayMove(data, oldIndex, newIndex) //this is just a splice util + }) + } + } + + const sensors = useSensors( + useSensor(MouseSensor, {}), + useSensor(TouchSensor, {}), + useSensor(KeyboardSensor, {}) + ) + return ( -
-
-
- -
-
- - - {table.getHeaderGroups().map(headerGroup => ( - - - ))} - - ))} - - - {table.getRowModel().rows.map(row => ( - - ))} - - - {table.getFooterGroups().map(footerGroup => ( - - {footerGroup.headers.map(header => ( - + // NOTE: This provider creates div elements, so don't nest inside of
- {headerGroup.headers.map(header => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.footer, - header.getContext() - )} -
elements + +
+
+
+ +
+
+
+ + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + + {table.getRowModel().rows.map(row => ( + ))} - - ))} - -
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
-
{JSON.stringify(data, null, 2)}
-
+ + + +
{JSON.stringify(data, null, 2)}
+
+ ) } @@ -180,9 +204,7 @@ const rootElement = document.getElementById('root') if (!rootElement) throw new Error('Failed to find the root element') ReactDOM.createRoot(rootElement).render( - // //disabled for react-dnd preview bug for now - + - - // + ) diff --git a/examples/react/row-dnd/src/makeData.ts b/examples/react/row-dnd/src/makeData.ts index 6d811a80f5..26b666d2ae 100644 --- a/examples/react/row-dnd/src/makeData.ts +++ b/examples/react/row-dnd/src/makeData.ts @@ -21,7 +21,7 @@ const range = (len: number) => { const newPerson = (): Person => { return { - userId: faker.datatype.uuid(), + userId: faker.string.uuid(), firstName: faker.person.firstName(), lastName: faker.person.lastName(), age: faker.number.int(40), @@ -38,7 +38,7 @@ const newPerson = (): Person => { export function makeData(...lens: number[]) { const makeDataLevel = (depth = 0): Person[] => { const len = lens[depth]! - return range(len).map((d): Person => { + return range(len).map((_d): Person => { return { ...newPerson(), subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ae6f82dfb..115e21048d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -809,6 +809,18 @@ importers: examples/react/row-dnd: dependencies: + '@dnd-kit/core': + specifier: ^6.1.0 + version: 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/modifiers': + specifier: ^7.0.0 + version: 7.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) + '@dnd-kit/sortable': + specifier: ^8.0.0 + version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.2.0) '@faker-js/faker': specifier: ^8.3.1 version: 8.3.1 @@ -818,12 +830,6 @@ importers: react: specifier: ^18.2.0 version: 18.2.0 - react-dnd: - specifier: ^16.0.1 - version: 16.0.1(@types/node@18.19.7)(@types/react@18.2.48)(react@18.2.0) - react-dnd-html5-backend: - specifier: ^16.0.1 - version: 16.0.1 react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -3016,6 +3022,61 @@ packages: chalk: 4.1.2 dev: true + /@dnd-kit/accessibility@3.1.0(react@18.2.0): + resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@dnd-kit/accessibility': 3.1.0(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + dev: false + + /@dnd-kit/modifiers@7.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): + resolution: {integrity: sha512-BG/ETy3eBjFap7+zIti53f0PCLGDzNXyTmn6fSdrudORf+OH04MxrW4p5+mPu4mgMk9kM41iYONjc3DOUWTcfg==} + peerDependencies: + '@dnd-kit/core': ^6.1.0 + react: '>=16.8.0' + dependencies: + '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): + resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} + peerDependencies: + '@dnd-kit/core': ^6.1.0 + react: '>=16.8.0' + dependencies: + '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@dnd-kit/utilities@3.2.2(react@18.2.0): + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + tslib: 2.6.2 + dev: false + /@emotion/babel-plugin-jsx-pragmatic@0.2.1(@babel/core@7.23.7): resolution: {integrity: sha512-xy1SlgEJygAAIvIuC2idkGKJYa6v5iwoyILkvNKgk347bV+IImXrUat5Z86EmLGyWhEoTplVT9EHqTnHZG4HFw==} peerDependencies: