diff --git a/packages/core/src/extensions/SourceBlockWithPreview/SourceBlockWithPreview.ts b/packages/core/src/extensions/SourceBlockWithPreview/SourceBlockWithPreview.ts index c9e8648861..b3de102057 100644 --- a/packages/core/src/extensions/SourceBlockWithPreview/SourceBlockWithPreview.ts +++ b/packages/core/src/extensions/SourceBlockWithPreview/SourceBlockWithPreview.ts @@ -183,8 +183,19 @@ export const SourceBlockWithPreviewExtension = createExtension( signal, }); - const handleBlur = () => + function handleBlur(event: FocusEvent) { + // Error text has its own focus target so the browser can select and + // copy it. Keep the popup open while interacting with that text. + if ( + event.relatedTarget === dom || + (event.relatedTarget instanceof Element && + dom.contains(event.relatedTarget) && + event.relatedTarget.closest(".bn-code-block-source-error")) + ) { + return; + } store.setState((state) => ({ ...state, popupOpen: undefined })); + } dom.addEventListener("blur", handleBlur, { capture: true, signal }); }, }; diff --git a/packages/core/src/extensions/SourceInlineContentWithPreview/SourceInlineContentWithPreview.ts b/packages/core/src/extensions/SourceInlineContentWithPreview/SourceInlineContentWithPreview.ts index 9ab67e93a7..e298bd38af 100644 --- a/packages/core/src/extensions/SourceInlineContentWithPreview/SourceInlineContentWithPreview.ts +++ b/packages/core/src/extensions/SourceInlineContentWithPreview/SourceInlineContentWithPreview.ts @@ -134,7 +134,18 @@ export const SourceInlineContentWithPreviewExtension = createExtension( signal, }); - const handleBlur = () => store.setState({ selected: undefined }); + function handleBlur(event: FocusEvent) { + // Keep the popup open while selecting error text or returning to the source. + if ( + event.relatedTarget === dom || + (event.relatedTarget instanceof Element && + dom.contains(event.relatedTarget) && + event.relatedTarget.closest(".bn-code-block-source-error")) + ) { + return; + } + store.setState({ selected: undefined }); + } dom.addEventListener("blur", handleBlur, { capture: true, signal }); }, }; diff --git a/packages/react/src/blocks/SourceWithPreview/SourceWithPreview.tsx b/packages/react/src/blocks/SourceWithPreview/SourceWithPreview.tsx index 65be1cfa13..02084d814f 100644 --- a/packages/react/src/blocks/SourceWithPreview/SourceWithPreview.tsx +++ b/packages/react/src/blocks/SourceWithPreview/SourceWithPreview.tsx @@ -238,6 +238,9 @@ export const SourceWithPreview = ( className="bn-code-block-source-error" contentEditable={false} style={{ display: error ? "block" : "none" }} + // Let the browser focus the non-editable error so ProseMirror doesn't + // replace the browser's text selection with a source selection. + tabIndex={error ? -1 : undefined} // Announced while editing (the popup is open); silenced & removed // from the tree once the popup closes, where the compact error // preview takes over. diff --git a/tests/src/end-to-end/sourcepreview/errorSelection.test.tsx b/tests/src/end-to-end/sourcepreview/errorSelection.test.tsx new file mode 100644 index 0000000000..1a9ec069ec --- /dev/null +++ b/tests/src/end-to-end/sourcepreview/errorSelection.test.tsx @@ -0,0 +1,138 @@ +import { BlockNoteSchema } from "@blocknote/core"; +import "@blocknote/core/fonts/inter.css"; +import { createReactDiagramBlockSpec } from "@blocknote/diagram-block"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { + createReactInlineMathSpec, + createReactMathBlockSpec, +} from "@blocknote/math-block"; +import { useCreateBlockNote } from "@blocknote/react"; +import { expect, test, vi } from "vite-plus/test"; +import { render } from "vitest-browser-react"; + +import { browserName, MOD, userEvent } from "../../utils/context.js"; +import { mouseSequence } from "../../utils/mouse.js"; + +const schema = BlockNoteSchema.create().extend({ + blockSpecs: { + mathBlock: createReactMathBlockSpec(), + diagram: createReactDiagramBlockSpec(), + }, + inlineContentSpecs: { math: createReactInlineMathSpec() }, +}); + +type Kind = "mathBlock" | "diagram" | "math"; + +function ErrorSelectionApp({ kind }: { kind: Kind }) { + const editor = useCreateBlockNote({ + schema, + initialContent: + kind === "math" + ? [ + { + type: "paragraph", + content: [{ type: "math", content: "\\badcommand" }], + }, + ] + : [ + { + type: kind, + content: kind === "diagram" ? "graph TD\nA -->" : "\\badcommand", + }, + ], + }); + return ( + <> + +