fix: guard editor selection churn around inline actions

This commit is contained in:
lucaronin
2026-04-24 03:25:50 +02:00
parent 24e317cfa6
commit 7c9f2b0dda
3 changed files with 80 additions and 14 deletions

View File

@@ -105,8 +105,23 @@ export function shouldSuppressFormattingToolbarHoverUpdate({
function getActiveFormattingToolbarFileBlockId(
editor: BlockNoteEditor<BlockSchema, InlineContentSchema, StyleSchema>,
) {
const selectedBlock =
editor.getSelection()?.blocks[0] ?? editor.getTextCursorPosition().block
let selectedBlock: ReturnType<typeof editor.getTextCursorPosition>['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

View File

@@ -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(
<>
<TolariaFormattingToolbar />
<TolariaFormattingToolbarController />
</>,
)
}).not.toThrow()
})
})

View File

@@ -207,12 +207,39 @@ function editorSupportsTextStyle(
)
}
function getSelectedBlocksSafely(
editor: BlockNoteEditor<BlockSchema, InlineContentSchema, StyleSchema>,
): 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<BlockSchema, InlineContentSchema, StyleSchema>,
): TolariaSelectedBlock | null {
try {
return editor.getTextCursorPosition().block as TolariaSelectedBlock
} catch {
return null
}
}
function selectionSupportsInlineFormatting(
editor: BlockNoteEditor<BlockSchema, InlineContentSchema, StyleSchema>,
) {
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<BlockSchema, InlineContentSchema, StyleSchema>,
) {
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,