-
-
Notifications
You must be signed in to change notification settings - Fork 761
feat(core): built-in table drag visualization #2963
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
782c8db
fca3f9c
318144b
4c03ac4
fde31a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "playground": true, | ||
| "docs": false, | ||
| "author": "must", | ||
| "tags": [ | ||
| "Intermediate", | ||
| "UI Components", | ||
| "Tables", | ||
| "Drag & Drop", | ||
| "Appearance & Styling" | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Table Reordering Visualization | ||
|
|
||
| BlockNote gives table row/column dragging visual feedback out of the box: a | ||
| snapshot of the row/column follows the cursor, the row/column being dragged is | ||
| tinted and outlined, and a drop indicator marks where it would land. | ||
|
|
||
| This example shows how to restyle a table - and those built-in drag | ||
| affordances - to match your own product, using a Microsoft Loop-inspired look: | ||
|
|
||
| - **Restyled tables**: rounded card look, muted header row, hairline | ||
| borders, and a row-hover highlight instead of a harsh black grid. | ||
| - **Retuned drag affordances**: the built-in drag source highlight, drop | ||
| indicator and drag snapshot recolored to the same palette. | ||
| - **Header row by default**: the `/table` command starts new tables with | ||
| a header row already enabled, so the header styling is visible right away. | ||
|
|
||
| ## How It Works | ||
|
|
||
| Everything here is CSS plus one slash-menu tweak - no extensions, no event | ||
| handling. BlockNote's `TableHandlesExtension` owns the whole drag lifecycle and | ||
| exposes it through classes you can target: | ||
|
|
||
| | Class | What it's on | | ||
| | -------------------------- | ---------------------------------------------- | | ||
| | `bn-table-drag-source-row` | every cell of the row being dragged | | ||
| | `bn-table-drag-source-col` | every cell of the column being dragged | | ||
| | `bn-table-drop-cursor` | a bar on the edge the row/column would drop at | | ||
| | `bn-table-drag-preview` | the snapshot shown next to the cursor | | ||
|
|
||
| The first three are ProseMirror decorations inside the editor, so they're | ||
| scoped under `.bn-editor [data-content-type="table"]` like any other table | ||
| style. `bn-table-drag-preview` is different: it's appended outside the editor | ||
| (the browser can only use an attached element as a drag image), so it has to be | ||
| styled through its own class rather than through the table selectors. | ||
|
|
||
| `tableStyles.css` does the restyling; `App.tsx` overrides the default `/table` | ||
| slash-menu item so new tables start with `headerRows: 1`. | ||
|
|
||
| ## Known Limitations | ||
|
|
||
| - **Keyboard and touch**: BlockNote's table drag handles are `draggable` + | ||
| `onDragStart` only today (see `TableHandle.tsx`) - there's no | ||
| keyboard-operable reorder path, and native HTML5 drag-and-drop isn't | ||
| supported on touch browsers at all. Both are gaps in BlockNote's table-drag | ||
| feature as a whole, not something this example introduces or fixes. | ||
| - **Accessibility**: for the same reason, there's no keyboard focus | ||
| restoration to verify after a reorder - the interaction can't be reached by | ||
| keyboard in the first place yet. | ||
|
|
||
| **Relevant Docs:** | ||
|
|
||
| - [Tables](/docs/features/blocks/tables) | ||
| - [Overriding CSS](/docs/react/styling-theming/overriding-css) | ||
| - [Editor Setup](/docs/getting-started/editor-setup) | ||
| - [Slash Menu](/docs/react/components/suggestion-menus) | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,14 @@ | ||||||||
| <html lang="en"> | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Add the HTML doctype. The missing doctype can enable quirks mode. Add Proposed fix+<!doctype html>
<html lang="en">📝 Committable suggestion
Suggested change
🧰 Tools🪛 HTMLHint (1.9.2)[error] 1-1: Doctype must be declared before any non-comment content. (doctype-first) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||
| <head> | ||||||||
| <meta charset="UTF-8" /> | ||||||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||||
| <title>Table Reordering Visualization</title> | ||||||||
| <script> | ||||||||
| <!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY --> | ||||||||
| </script> | ||||||||
| </head> | ||||||||
| <body> | ||||||||
| <div id="root"></div> | ||||||||
| <script type="module" src="./main.tsx"></script> | ||||||||
| </body> | ||||||||
| </html> | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY | ||
| import React from "react"; | ||
| import { createRoot } from "react-dom/client"; | ||
| import App from "./src/App.jsx"; | ||
|
|
||
| const root = createRoot(document.getElementById("root")!); | ||
| root.render( | ||
| <React.StrictMode> | ||
| <App /> | ||
| </React.StrictMode>, | ||
| ); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,30 @@ | ||||||
| { | ||||||
| "name": "@blocknote/example-ui-components-table-reordering-visualization", | ||||||
| "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", | ||||||
| "type": "module", | ||||||
| "private": true, | ||||||
| "version": "0.12.4", | ||||||
| "scripts": { | ||||||
| "start": "vp dev", | ||||||
| "dev": "vp dev", | ||||||
| "build:prod": "tsc && vp build", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Replace Line 10 invokes Proposed fix- "build:prod": "tsc && vp build",
+ "build:prod": "vp run lint && vp build",As per coding guidelines, use only 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||
| "preview": "vp preview" | ||||||
| }, | ||||||
| "dependencies": { | ||||||
| "@blocknote/ariakit": "latest", | ||||||
| "@blocknote/core": "latest", | ||||||
| "@blocknote/mantine": "latest", | ||||||
| "@blocknote/react": "latest", | ||||||
| "@blocknote/shadcn": "latest", | ||||||
| "@mantine/core": "^9.0.2", | ||||||
| "@mantine/hooks": "^9.0.2", | ||||||
| "react": "^19.2.3", | ||||||
| "react-dom": "^19.2.3" | ||||||
| }, | ||||||
| "devDependencies": { | ||||||
| "@types/react": "^19.2.3", | ||||||
| "@types/react-dom": "^19.2.3", | ||||||
| "@vitejs/plugin-react": "^6.0.1", | ||||||
| "vite-plus": "catalog:" | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { BlockNoteEditor } from "@blocknote/core"; | ||
| import { | ||
| filterSuggestionItems, | ||
| insertOrUpdateBlockForSlashMenu, | ||
| } from "@blocknote/core/extensions"; | ||
| import "@blocknote/core/fonts/inter.css"; | ||
| import { BlockNoteView } from "@blocknote/mantine"; | ||
| import "@blocknote/mantine/style.css"; | ||
| import { | ||
| DefaultReactSuggestionItem, | ||
| getDefaultReactSlashMenuItems, | ||
| SuggestionMenuController, | ||
| useCreateBlockNote, | ||
| } from "@blocknote/react"; | ||
|
|
||
| import "./tableStyles.css"; | ||
|
|
||
| // BlockNote's stock "/table" item inserts a table with no header row, so it | ||
| // never picks up the header styling until someone manually toggles it on. | ||
| // This swaps in a version that starts with `headerRows: 1` instead. | ||
| const getCustomSlashMenuItems = ( | ||
| editor: BlockNoteEditor<any, any, any>, | ||
| ): DefaultReactSuggestionItem[] => | ||
| getDefaultReactSlashMenuItems(editor).map((item) => { | ||
| // `key` is typed away on the React item (it's reserved for JSX), but the | ||
| // underlying object - built from the same items core uses - still has it. | ||
| const key = (item as unknown as { key?: string }).key; | ||
| if (key !== "table") { | ||
| return item; | ||
| } | ||
| return { | ||
| ...item, | ||
| onItemClick: () => | ||
| insertOrUpdateBlockForSlashMenu(editor, { | ||
| type: "table", | ||
| content: { | ||
| type: "tableContent", | ||
| headerRows: 1, | ||
| rows: [{ cells: ["", "", ""] }, { cells: ["", "", ""] }], | ||
| } as any, | ||
| }), | ||
| }; | ||
| }); | ||
|
|
||
| export default function App() { | ||
| const editor = useCreateBlockNote({ | ||
| tables: { | ||
| splitCells: true, | ||
| cellBackgroundColor: true, | ||
| cellTextColor: true, | ||
| headers: true, | ||
| }, | ||
| initialContent: [ | ||
| { | ||
| type: "heading", | ||
| props: { level: 2 }, | ||
| content: "Restyling BlockNote.js Table Reordering", | ||
| }, | ||
| { | ||
| type: "table", | ||
| content: { | ||
| type: "tableContent", | ||
| columnWidths: [180, 140, 140, 220], | ||
| headerRows: 1, | ||
| rows: [ | ||
| { cells: ["Column A", "Column B", "Column C", "Column D"] }, | ||
| { cells: ["1a", "1b", "1c", "1d"] }, | ||
| { cells: ["2a", "2b", "2c", "2d"] }, | ||
| { cells: ["3a", "3b", "3c", "3d"] }, | ||
| ], | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| return ( | ||
| <BlockNoteView editor={editor} slashMenu={false}> | ||
| <SuggestionMenuController | ||
| triggerCharacter="/" | ||
| getItems={async (query) => | ||
| filterSuggestionItems(getCustomSlashMenuItems(editor), query) | ||
| } | ||
| /> | ||
| </BlockNoteView> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| /** | ||
| * Palette | ||
| * | ||
| * Declared on `.bn-root` rather than on the table, for two reasons: the drag | ||
| * handles render in a portal outside the editor, and so does the drag | ||
| * snapshot - BlockNote puts `bn-root` and the active color scheme on both, so | ||
| * anything defined here reaches them. | ||
| */ | ||
| .bn-root { | ||
| --table-border: #e2e2ea; | ||
| --table-shadow: rgb(0 0 0 / 6%); | ||
| --table-header-bg: #f0f0f3; | ||
| --table-header-text: #5d5d70; | ||
| --table-row-hover: #d3d4e0; | ||
| --table-accent: #4a48b8; | ||
| --table-handle-hover-bg: #eef1fa; | ||
| --table-handle-hover-text: #5e5cd0; | ||
| /* Kept translucent so they read as a wash over the cell's own background | ||
| rather than replacing it - a flat fill light enough for this palette would | ||
| blot out the text in dark mode. */ | ||
| --table-drag-tint: rgb(94 92 208 / 14%); | ||
| --table-drag-outline: rgb(94 92 208 / 45%); | ||
| --table-selected: rgb(94 92 208 / 18%); | ||
| } | ||
|
|
||
| .bn-root[data-color-scheme="dark"] { | ||
| --table-border: #3b3b46; | ||
| --table-shadow: rgb(0 0 0 / 40%); | ||
| --table-header-bg: #2c2c35; | ||
| --table-header-text: #a6a6bd; | ||
| --table-row-hover: #35353f; | ||
| /* The light-mode accent is a dark purple, which all but disappears against | ||
| the dark editor background. */ | ||
| --table-accent: #9391ff; | ||
| --table-handle-hover-bg: #35354a; | ||
| --table-handle-hover-text: #b3b1ff; | ||
| --table-drag-tint: rgb(147 145 255 / 22%); | ||
| --table-drag-outline: rgb(147 145 255 / 55%); | ||
| --table-selected: rgb(147 145 255 / 25%); | ||
| } | ||
|
|
||
| /** | ||
| * Tables | ||
| * Loop/Notion-style card look: rounded outer border, muted header row, | ||
| * hairline internal grid and a hover highlight instead of the default | ||
| * harsh black grid lines. | ||
| */ | ||
| .bn-editor [data-content-type="table"] table { | ||
| border-collapse: separate; | ||
| border-spacing: 0; | ||
| border: 1px solid var(--table-border); | ||
| border-radius: 8px; | ||
| box-shadow: 0 1px 3px var(--table-shadow); | ||
| overflow: hidden; | ||
| } | ||
| .bn-editor [data-content-type="table"] th, | ||
| .bn-editor [data-content-type="table"] td { | ||
| border: none; | ||
| border-right: 1px solid var(--table-border); | ||
| border-bottom: 1px solid var(--table-border); | ||
| padding: 10px 16px; | ||
| transition: background-color 0.15s ease; | ||
| } | ||
| .bn-editor [data-content-type="table"] th:last-child, | ||
| .bn-editor [data-content-type="table"] td:last-child { | ||
| border-right: none; | ||
| } | ||
| .bn-editor [data-content-type="table"] tr:last-child th, | ||
| .bn-editor [data-content-type="table"] tr:last-child td { | ||
| border-bottom: none; | ||
| } | ||
| .bn-editor [data-content-type="table"] th { | ||
| background-color: var(--table-header-bg); | ||
| color: var(--table-header-text); | ||
| font-weight: 600; | ||
| font-size: 0.8125em; | ||
| letter-spacing: 0.01em; | ||
| } | ||
| .bn-editor [data-content-type="table"] tr:hover > td { | ||
| background-color: var(--table-row-hover); | ||
| } | ||
| .bn-editor [data-content-type="table"] .selectedCell:after { | ||
| background: var(--table-selected); | ||
| opacity: 1; | ||
| } | ||
|
|
||
| /** | ||
| * Row/column reordering: BlockNote already highlights the row/column being | ||
| * dragged and marks the drop position; these just retune the built-in | ||
| * affordances to the palette above, so the drag state stays distinguishable | ||
| * from this table's own hover and selection colors. | ||
| */ | ||
| .bn-editor [data-content-type="table"] td.bn-table-drag-source-row, | ||
| .bn-editor [data-content-type="table"] th.bn-table-drag-source-row, | ||
| .bn-editor [data-content-type="table"] td.bn-table-drag-source-col, | ||
| .bn-editor [data-content-type="table"] th.bn-table-drag-source-col { | ||
| background-color: var(--table-drag-tint); | ||
| outline-color: var(--table-drag-outline); | ||
| } | ||
| .bn-editor [data-content-type="table"] .bn-table-drop-cursor { | ||
| background-color: var(--table-accent); | ||
| } | ||
|
|
||
| /* The drag snapshot is rendered outside `.bn-editor`, so it's styled through | ||
| its own class rather than the table selectors above. */ | ||
| .bn-table-drag-preview th, | ||
| .bn-table-drag-preview td { | ||
| border-color: var(--table-accent); | ||
| padding: 10px 16px; | ||
| } | ||
|
|
||
| /* Row/column drag handles and add-row/add-column buttons. */ | ||
| .bn-mantine .bn-table-handle, | ||
| .bn-mantine .bn-table-cell-handle { | ||
| border-radius: 2px; | ||
| } | ||
| .bn-mantine .bn-table-handle:hover, | ||
| .bn-mantine .bn-table-handle-dragging, | ||
| .bn-mantine .bn-table-cell-handle:hover, | ||
| .bn-mantine .bn-extend-button:hover, | ||
| .bn-mantine .bn-extend-button-editing { | ||
| background-color: var(--table-handle-hover-bg); | ||
| color: var(--table-handle-hover-text); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /// <reference types="vite-plus/client" /> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", | ||
| "compilerOptions": { | ||
| "target": "ESNext", | ||
| "useDefineForClassFields": true, | ||
| "lib": ["DOM", "DOM.Iterable", "ESNext"], | ||
| "allowJs": false, | ||
| "skipLibCheck": true, | ||
| "allowSyntheticDefaultImports": true, | ||
| "strict": true, | ||
| "forceConsistentCasingInFileNames": true, | ||
| "module": "ESNext", | ||
| "moduleResolution": "bundler", | ||
| "resolveJsonModule": true, | ||
| "isolatedModules": true, | ||
| "noEmit": true, | ||
| "jsx": "react-jsx", | ||
| "composite": true | ||
| }, | ||
| "include": ["."], | ||
| "__ADD_FOR_LOCAL_DEV_references": [ | ||
| { | ||
| "path": "../../../packages/core/" | ||
| }, | ||
| { | ||
| "path": "../../../packages/react/" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /// <reference types="vite-plus/client" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the extension wording.
TableHandlesExtensionprovides the built-in behavior. “No extensions” is incorrect. State that the example requires no additional extensions or event handling.examples/03-ui-components/21-table-reordering-visualization/README.md#L19-L21: replace “no extensions” with “no additional extensions”.playground/src/examples.gen.tsx#L918-L918: regenerate this generated file after correcting the source README.📍 Affects 2 files
examples/03-ui-components/21-table-reordering-visualization/README.md#L19-L21(this comment)playground/src/examples.gen.tsx#L918-L918🤖 Prompt for AI Agents