🐛(frontend) clear callout background on Backspace

Reset the background when Backspace converts a callout to a paragraph.
Preserve content, alignment, regular text deletion, and undo behavior.

Fixes #2052

Signed-off-by: Manixhor <manigururam@hotmail.com>
This commit is contained in:
MANI K
2026-09-22 10:36:39 +02:00
committed by Anthony LC
parent 3fa9186289
commit a75dbaaeb9
3 changed files with 177 additions and 0 deletions
+2
View File
@@ -17,6 +17,8 @@ and this project adheres to
### Fixed
- 🐛(frontend) clear callout background on Backspace #2052
- 🐛(export) keep image aspect ratio in PDF columns #2670
- 🐛(frontend) fix redirect after deleting a document #2706
@@ -4,6 +4,7 @@ import {
BlockNoteEditor,
InlineContentSchema,
StyleSchema,
createExtension,
defaultProps,
} from '@blocknote/core';
import { insertOrUpdateBlockForSlashMenu } from '@blocknote/core/extensions';
@@ -157,6 +158,30 @@ const CalloutComponent = ({
);
};
export const CalloutBlockShortcuts = createExtension({
key: 'callout-shortcuts',
keyboardShortcuts: {
Backspace: ({ editor }) => {
const { selection } = editor.prosemirrorState;
if (
!selection.empty ||
selection.$from.parentOffset !== 0 ||
selection.$from.parent.type.name !== 'callout'
) {
return false;
}
// BlockNote's default conversion retains shared props, including the
// callout background. Reset it when removing the callout formatting.
editor.updateBlock(editor.getTextCursorPosition().block, {
type: 'paragraph',
props: { backgroundColor: defaultProps.backgroundColor.default },
});
return true;
},
},
});
export const CalloutBlock = createReactBlockSpec(
{
type: 'callout',
@@ -172,6 +197,7 @@ export const CalloutBlock = createReactBlockSpec(
<CalloutComponent block={block} editor={editor} contentRef={contentRef} />
),
},
[CalloutBlockShortcuts],
);
export const getCalloutReactSlashMenuItems = (
@@ -0,0 +1,149 @@
import {
BlockNoteEditor,
BlockNoteSchema,
createBlockSpec,
defaultBlockSpecs,
defaultProps,
} from '@blocknote/core';
import { TextSelection } from '@tiptap/pm/state';
import { afterEach, describe, expect, it } from 'vitest';
import { CalloutBlockShortcuts } from '../CalloutBlock';
// Use a DOM renderer so the real editor keymaps can run without React UI.
const schema = BlockNoteSchema.create({
blockSpecs: {
...defaultBlockSpecs,
callout: createBlockSpec(
{
type: 'callout',
propSchema: {
textAlignment: defaultProps.textAlignment,
backgroundColor: defaultProps.backgroundColor,
emoji: { default: '💡' },
},
content: 'inline',
},
{
render: () => {
const dom = document.createElement('div');
return { dom, contentDOM: dom };
},
},
[CalloutBlockShortcuts],
)(),
},
});
function createEditor(content = '', backgroundColor = 'yellow') {
const editor = BlockNoteEditor.create({
schema,
initialContent: [
{
id: 'callout',
type: 'callout',
props: { backgroundColor, textAlignment: 'center' },
content,
},
],
});
editor.mount(document.createElement('div'));
editor.setTextCursorPosition('callout', 'start');
return editor;
}
type Editor = ReturnType<typeof createEditor>;
let editor: Editor | undefined;
function backspace(editor: Editor) {
const view = editor.prosemirrorView;
return view.someProp('handleKeyDown', (handler) =>
handler(view, new KeyboardEvent('keydown', { key: 'Backspace' })),
);
}
afterEach(() => {
editor?._tiptapEditor.destroy();
editor = undefined;
});
describe('callout Backspace', () => {
it.each(['yellow', 'blue', 'default'])(
'clears the %s background when an empty callout becomes a paragraph',
(color) => {
editor = createEditor('', color);
backspace(editor);
expect(editor.document[0]).toMatchObject({
id: 'callout',
type: 'paragraph',
props: { backgroundColor: 'default', textAlignment: 'center' },
content: [],
});
},
);
it('preserves text when converting at the start of a nonempty callout', () => {
editor = createEditor('Keep this text');
backspace(editor);
expect(editor.document[0]).toMatchObject({
type: 'paragraph',
props: { backgroundColor: 'default' },
content: [{ type: 'text', text: 'Keep this text' }],
});
});
it('leaves character deletion inside a callout to the editor', () => {
editor = createEditor('Text');
editor.setTextCursorPosition('callout', 'end');
expect(backspace(editor)).toBeFalsy();
expect(editor.document[0]).toMatchObject({
type: 'callout',
props: { backgroundColor: 'yellow' },
});
});
it('deletes selected text without converting the callout', () => {
editor = createEditor('Text');
const view = editor.prosemirrorView;
const start = view.state.selection.from;
view.dispatch(
view.state.tr.setSelection(
TextSelection.create(view.state.doc, start, start + 4),
),
);
backspace(editor);
expect(editor.document[0]).toMatchObject({
type: 'callout',
props: { backgroundColor: 'yellow' },
content: [],
});
});
it('does not clear the background of an ordinary paragraph', () => {
editor = createEditor();
editor.updateBlock('callout', { type: 'paragraph' });
editor.setTextCursorPosition('callout', 'start');
backspace(editor);
expect(editor.document[0]).toMatchObject({
type: 'paragraph',
props: { backgroundColor: 'yellow' },
});
});
it('restores the callout and its background in one undo', () => {
editor = createEditor('Text', 'blue');
backspace(editor);
editor.undo();
expect(editor.document[0]).toMatchObject({
type: 'callout',
props: { backgroundColor: 'blue' },
content: [{ type: 'text', text: 'Text' }],
});
});
});