fix: keep blocknote tables editable after focus changes

This commit is contained in:
lucaronin
2026-04-24 07:02:40 +02:00
parent 0ecac2ab79
commit 33d7404d6c
4 changed files with 172 additions and 18 deletions

View File

@@ -1,8 +1,6 @@
diff --git a/dist/defaultBlocks-DE5GNdJH.js b/dist/defaultBlocks-DE5GNdJH.js
index b2761001278486a8b2ac1d10c82420b4994e96d9..fbf4f2f450ed1351d64adeb16263ceacf9ee393c 100644
--- a/dist/defaultBlocks-DE5GNdJH.js
+++ b/dist/defaultBlocks-DE5GNdJH.js
@@ -2761,7 +2761,7 @@ const B = new Ze("SuggestionMenuPlugin"), _n = k(({ editor: e }) => {
@@ -2761,7 +2761,7 @@
if (a === s) {
const c = r.state.doc;
for (const l of t) {
@@ -11,10 +9,9 @@ index b2761001278486a8b2ac1d10c82420b4994e96d9..fbf4f2f450ed1351d64adeb16263ceac
if (l === u)
return r.dispatch(r.state.tr.insertText(i)), r.dispatch(
r.state.tr.setMeta(B, {
diff --git a/src/editor/managers/ExtensionManager/extensions.ts b/src/editor/managers/ExtensionManager/extensions.ts
--- a/src/editor/managers/ExtensionManager/extensions.ts
+++ b/src/editor/managers/ExtensionManager/extensions.ts
@@ -84,7 +84,8 @@ export function getDefaultTiptapExtensions(
@@ -84,7 +84,8 @@
}).configure({
defaultProtocol: DEFAULT_LINK_PROTOCOL,
// only call this once if we have multiple editors installed. Or fix https://github.com/ueberdosis/tiptap/issues/5450
@@ -24,10 +21,9 @@ diff --git a/src/editor/managers/ExtensionManager/extensions.ts b/src/editor/man
}),
...(Object.values(editor.schema.styleSpecs).map((styleSpec) => {
return styleSpec.implementation.mark.configure({
diff --git a/dist/blocknote.js b/dist/blocknote.js
--- a/dist/blocknote.js
+++ b/dist/blocknote.js
@@ -2037,7 +2037,9 @@ const de = () => {
@@ -2037,7 +2037,9 @@
]
})
);
@@ -38,8 +34,7 @@ diff --git a/dist/blocknote.js b/dist/blocknote.js
function mn(o, e) {
const t = [
I.ClipboardTextSerializer,
@@ -2063,8 +2065,9 @@ function mn(o, e) {
inclusive: !1
@@ -2062,7 +2064,8 @@
}).configure({
defaultProtocol: Ct,
// only call this once if we have multiple editors installed. Or fix https://github.com/ueberdosis/tiptap/issues/5450
@@ -49,7 +44,7 @@ diff --git a/dist/blocknote.js b/dist/blocknote.js
}),
...Object.values(o.schema.styleSpecs).map((n) => n.implementation.mark.configure({
editor: o
@@ -2112,7 +2115,7 @@ function mn(o, e) {
@@ -2112,7 +2115,7 @@
),
Do(o)
];
@@ -58,3 +53,40 @@ diff --git a/dist/blocknote.js b/dist/blocknote.js
}
function kn(o, e) {
const t = [
--- a/src/extensions/TableHandles/TableHandles.ts
+++ b/src/extensions/TableHandles/TableHandles.ts
@@ -572,9 +572,14 @@
const tableBody = this.tableElement!.querySelector("tbody");
if (!tableBody) {
- throw new Error(
- "Table block does not contain a 'tbody' HTML element. This should never happen.",
- );
+ this.state.show = false;
+ this.state.showAddOrRemoveRowsButton = false;
+ this.state.showAddOrRemoveColumnsButton = false;
+ this.state.rowIndex = undefined;
+ this.state.colIndex = undefined;
+ this.state.referencePosCell = undefined;
+ this.emitUpdate();
+ return;
}
if (
--- a/dist/TrailingNode-CxM966vN.js
+++ b/dist/TrailingNode-CxM966vN.js
@@ -1746,10 +1746,10 @@
);
this.state.rowIndex !== void 0 && this.state.colIndex !== void 0 && (this.state.rowIndex >= e && (this.state.rowIndex = e - 1), this.state.colIndex >= t && (this.state.colIndex = t - 1));
const o = this.tableElement.querySelector("tbody");
- if (!o)
- throw new Error(
- "Table block does not contain a 'tbody' HTML element. This should never happen."
- );
+ if (!o) {
+ this.state.show = !1, this.state.showAddOrRemoveRowsButton = !1, this.state.showAddOrRemoveColumnsButton = !1, this.state.rowIndex = void 0, this.state.colIndex = void 0, this.state.referencePosCell = void 0, this.emitUpdate();
+ return;
+ }
if (this.state.rowIndex !== void 0 && this.state.colIndex !== void 0) {
const i = o.children[this.state.rowIndex].children[this.state.colIndex];
i ? this.state.referencePosCell = i.getBoundingClientRect() : (this.state.rowIndex = void 0, this.state.colIndex = void 0);

16
pnpm-lock.yaml generated
View File

@@ -6,7 +6,7 @@ settings:
patchedDependencies:
'@blocknote/core@0.46.2':
hash: 04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323
hash: dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94
path: patches/@blocknote__core@0.46.2.patch
'@blocknote/react@0.46.2':
hash: e3726d75ed65e07adb1fd48262d6a15f8e52b1cc257177aaf632937830170c97
@@ -27,10 +27,10 @@ importers:
version: 0.78.0(zod@4.3.6)
'@blocknote/code-block':
specifier: ^0.46.2
version: 0.46.2(@blocknote/core@0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))
version: 0.46.2(@blocknote/core@0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))
'@blocknote/core':
specifier: ^0.46.2
version: 0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
version: 0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
'@blocknote/mantine':
specifier: ^0.46.2
version: 0.46.2(@floating-ui/dom@1.7.5)(@mantine/core@8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.14(react@19.2.4))(@mantine/utils@6.0.22(react@19.2.4))(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -4514,9 +4514,9 @@ snapshots:
'@bcoe/v8-coverage@1.0.2': {}
'@blocknote/code-block@0.46.2(@blocknote/core@0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))':
'@blocknote/code-block@0.46.2(@blocknote/core@0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))':
dependencies:
'@blocknote/core': 0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
'@blocknote/core': 0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
'@shikijs/core': 3.23.0
'@shikijs/engine-javascript': 3.23.0
'@shikijs/langs': 3.23.0
@@ -4524,7 +4524,7 @@ snapshots:
'@shikijs/themes': 3.23.0
'@shikijs/types': 3.22.0
'@blocknote/core@0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)':
'@blocknote/core@0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)':
dependencies:
'@emoji-mart/data': 1.2.1
'@handlewithcare/prosemirror-inputrules': 0.1.4(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)
@@ -4576,7 +4576,7 @@ snapshots:
'@blocknote/mantine@0.46.2(@floating-ui/dom@1.7.5)(@mantine/core@8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.14(react@19.2.4))(@mantine/utils@6.0.22(react@19.2.4))(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@blocknote/core': 0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
'@blocknote/core': 0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
'@blocknote/react': 0.46.2(patch_hash=e3726d75ed65e07adb1fd48262d6a15f8e52b1cc257177aaf632937830170c97)(@floating-ui/dom@1.7.5)(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/core': 8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/hooks': 8.3.14(react@19.2.4)
@@ -4598,7 +4598,7 @@ snapshots:
'@blocknote/react@0.46.2(patch_hash=e3726d75ed65e07adb1fd48262d6a15f8e52b1cc257177aaf632937830170c97)(@floating-ui/dom@1.7.5)(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@blocknote/core': 0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
'@blocknote/core': 0.46.2(patch_hash=dc394d11ab419b36b8d306cbbb6ce39f0722e042b63b2b7d254c7d7f90782e94)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
'@emoji-mart/data': 1.2.1
'@floating-ui/react': 0.27.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@floating-ui/utils': 0.2.10

View File

@@ -0,0 +1,63 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { TableHandlesView } from '../../node_modules/@blocknote/core/src/extensions/TableHandles/TableHandles'
function createTableBlock() {
return {
id: 'table-block',
type: 'table',
content: {
type: 'tableContent',
rows: [
{ cells: ['Head 1', 'Head 2'] },
{ cells: ['A', 'B'] },
],
},
}
}
describe('BlockNote table handles regression', () => {
afterEach(() => {
document.body.innerHTML = ''
})
it('hides stale table handles instead of throwing when tbody is missing during update', () => {
const block = createTableBlock()
const editorRoot = document.createElement('div')
document.body.appendChild(editorRoot)
const editor = {
getBlock: vi.fn(() => block),
}
const emitUpdate = vi.fn()
const view = new TableHandlesView(
editor as never,
{
dom: editorRoot,
root: document,
} as never,
emitUpdate,
)
view.state = {
block,
show: true,
showAddOrRemoveRowsButton: true,
showAddOrRemoveColumnsButton: true,
rowIndex: 0,
colIndex: 0,
} as never
const staleTableWrapper = document.createElement('div')
editorRoot.appendChild(staleTableWrapper)
view.tableElement = staleTableWrapper
expect(() => view.update()).not.toThrow()
expect(view.state?.show).toBe(false)
expect(view.state?.showAddOrRemoveRowsButton).toBe(false)
expect(view.state?.showAddOrRemoveColumnsButton).toBe(false)
expect(emitUpdate).toHaveBeenCalled()
view.destroy()
})
})

View File

@@ -0,0 +1,59 @@
import { test, expect, type Page } from '@playwright/test'
import { createFixtureVaultCopy, openFixtureVaultTauri, removeFixtureVaultCopy } from '../helpers/fixtureVault'
import { seedBlockNoteTable, triggerMenuCommand } from './testBridge'
let tempVaultDir: string
function trackUnexpectedErrors(page: Page): string[] {
const errors: string[] = []
page.on('pageerror', (error) => {
errors.push(error.message)
})
page.on('console', (message) => {
if (message.type() !== 'error') return
const text = message.text()
if (text.includes('ws://localhost:9711')) return
errors.push(text)
})
return errors
}
async function createUntitledNote(page: Page): Promise<void> {
await triggerMenuCommand(page, 'file-new-note')
await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
}
test.describe('table focus regression', () => {
test.beforeEach(({ page }, testInfo) => {
void page
testInfo.setTimeout(60_000)
tempVaultDir = createFixtureVaultCopy()
})
test.afterEach(() => {
removeFixtureVaultCopy(tempVaultDir)
})
test('table headers stay editable after moving focus back to paragraph content', async ({ page }) => {
const errors = trackUnexpectedErrors(page)
const editor = page.getByRole('textbox').last()
await openFixtureVaultTauri(page, tempVaultDir)
await createUntitledNote(page)
await seedBlockNoteTable(page, [180, 120, 120])
await expect(page.locator('table th')).toHaveCount(3, { timeout: 5_000 })
await page.locator('table th').first().click()
const trailingParagraph = page.locator('.bn-editor [data-content-type="paragraph"]').last()
await trailingParagraph.click()
await page.keyboard.type('typed after table focus')
await expect(editor).toContainText('typed after table focus')
await expect(page.locator('table')).toHaveCount(1)
expect(errors).toEqual([])
})
})