From 7c9f2b0ddac8f279d940e976e30999908e665386 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Fri, 24 Apr 2026 03:25:50 +0200 Subject: [PATCH] fix: guard editor selection churn around inline actions --- .../blockNoteFormattingToolbarHoverGuard.ts | 19 ++++++- .../tolariaEditorFormatting.behavior.test.tsx | 22 ++++++++ src/components/tolariaEditorFormatting.tsx | 53 ++++++++++++++----- 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/src/components/blockNoteFormattingToolbarHoverGuard.ts b/src/components/blockNoteFormattingToolbarHoverGuard.ts index 014315e9..9e98bebf 100644 --- a/src/components/blockNoteFormattingToolbarHoverGuard.ts +++ b/src/components/blockNoteFormattingToolbarHoverGuard.ts @@ -105,8 +105,23 @@ export function shouldSuppressFormattingToolbarHoverUpdate({ function getActiveFormattingToolbarFileBlockId( editor: BlockNoteEditor, ) { - const selectedBlock = - editor.getSelection()?.blocks[0] ?? editor.getTextCursorPosition().block + let selectedBlock: ReturnType['block'] | null = null + + try { + selectedBlock = editor.getSelection()?.blocks[0] ?? null + } catch { + selectedBlock = null + } + + if (!selectedBlock) { + try { + selectedBlock = editor.getTextCursorPosition().block + } catch { + selectedBlock = null + } + } + + if (!selectedBlock) return null return FORMATTING_TOOLBAR_FILE_BLOCK_TYPES.has(selectedBlock.type) ? selectedBlock.id diff --git a/src/components/tolariaEditorFormatting.behavior.test.tsx b/src/components/tolariaEditorFormatting.behavior.test.tsx index 3152b900..bd3d6f3d 100644 --- a/src/components/tolariaEditorFormatting.behavior.test.tsx +++ b/src/components/tolariaEditorFormatting.behavior.test.tsx @@ -319,4 +319,26 @@ describe('tolariaEditorFormatting behavior', () => { }), })) }) + + it('stays stable when BlockNote selection reads throw during inline action churn', () => { + const editor = createMockEditor('paragraph') + const selectionError = new RangeError('Index 0 out of range for <>') + + editor.getSelection = vi.fn(() => { + throw selectionError + }) + editor.getTextCursorPosition = vi.fn(() => { + throw selectionError + }) + useBlockNoteEditorMock.mockReturnValue(editor) + + expect(() => { + render( + <> + + + , + ) + }).not.toThrow() + }) }) diff --git a/src/components/tolariaEditorFormatting.tsx b/src/components/tolariaEditorFormatting.tsx index e9e49666..27bb5674 100644 --- a/src/components/tolariaEditorFormatting.tsx +++ b/src/components/tolariaEditorFormatting.tsx @@ -207,12 +207,39 @@ function editorSupportsTextStyle( ) } +function getSelectedBlocksSafely( + editor: BlockNoteEditor, +): TolariaSelectedBlock[] { + try { + const selectionBlocks = editor.getSelection()?.blocks + if (selectionBlocks?.length) { + return selectionBlocks as TolariaSelectedBlock[] + } + } catch { + // BlockNote can briefly expose an invalid selection while inline actions remount blocks. + } + + try { + return [editor.getTextCursorPosition().block as TolariaSelectedBlock] + } catch { + return [] + } +} + +function getCursorBlockSafely( + editor: BlockNoteEditor, +): TolariaSelectedBlock | null { + try { + return editor.getTextCursorPosition().block as TolariaSelectedBlock + } catch { + return null + } +} + function selectionSupportsInlineFormatting( editor: BlockNoteEditor, ) { - return ( - editor.getSelection()?.blocks || [editor.getTextCursorPosition().block] - ).some((block) => block.content !== undefined) + return getSelectedBlocksSafely(editor).some((block) => block.content !== undefined) } function getBasicTextStyleButtonState( @@ -274,8 +301,8 @@ function getTolariaBlockTypeSelectOptions( function getFormattingToolbarBridgeBlockId( editor: BlockNoteEditor, ) { - const selectedBlock = - editor.getSelection()?.blocks[0] ?? editor.getTextCursorPosition().block + const selectedBlock = getSelectedBlocksSafely(editor)[0] + if (!selectedBlock) return null return FORMATTING_TOOLBAR_FILE_BLOCK_TYPES.has(selectedBlock.type) ? selectedBlock.id @@ -353,14 +380,15 @@ function TolariaBlockTypeSelect() { >() const selectedBlocks = useEditorState({ editor, - selector: ({ editor }): TolariaSelectedBlock[] => - (editor.getSelection()?.blocks || [ - editor.getTextCursorPosition().block, - ]) as TolariaSelectedBlock[], + selector: ({ editor }): TolariaSelectedBlock[] => getSelectedBlocksSafely(editor), }) - const firstSelectedBlock = selectedBlocks[0] + const firstSelectedBlock = selectedBlocks[0] ?? null const selectItems = useMemo( - () => getTolariaBlockTypeSelectOptions(editor, firstSelectedBlock), + () => ( + firstSelectedBlock + ? getTolariaBlockTypeSelectOptions(editor, firstSelectedBlock) + : [] + ), [editor, firstSelectedBlock], ) const selectedItem = selectItems.find( @@ -509,7 +537,8 @@ export function TolariaFormattingToolbarController(props: { const placement = useEditorState({ editor, selector: ({ editor }) => { - const block = editor.getTextCursorPosition().block + const block = getCursorBlockSafely(editor) + if (!block) return 'top-start' if (!blockHasType(block, editor, block.type, { textAlignment: defaultProps.textAlignment,