From 27dfba4594f9d9b2edcf7f7f153e07e7ff9c0ca5 Mon Sep 17 00:00:00 2001 From: markkaylor Date: Mon, 8 Dec 2025 17:36:56 +0100 Subject: [PATCH] fix(content-manager): blocks editor state always contains at least a paragraph block (#24990) --- .../FormInputs/BlocksInput/BlocksEditor.tsx | 35 ++++++++++++++-- .../tests/normalizeBlocksState.test.ts | 40 +++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/BlocksInput/tests/normalizeBlocksState.test.ts diff --git a/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.tsx b/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.tsx index 39fd640989..b7f2344dbc 100644 --- a/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.tsx +++ b/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.tsx @@ -159,6 +159,20 @@ const pipe = (value: Editor) => fns.reduce((prev, fn) => fn(prev), value); +/** + * Normalize the blocks state to null if the editor state is considered empty, + * otherwise return the state + */ +const normalizeBlocksState = ( + editor: Editor, + value: Schema.Attribute.BlocksValue | Descendant[] +): Schema.Attribute.BlocksValue | Descendant[] | null => { + const isEmpty = + value.length === 1 && Editor.isEmpty(editor, value[0] as Schema.Attribute.BlocksNode); + + return isEmpty ? null : value; +}; + interface BlocksEditorProps extends Pick, 'onChange' | 'value' | 'error'>, BlocksContentProps { @@ -213,12 +227,14 @@ const BlocksEditor = React.forwardRef<{ focus: () => void }, BlocksEditorProps>( // Set a new debounce timeout debounceTimeout.current = setTimeout(() => { incrementSlateUpdatesCount(); - onChange(name, state as Schema.Attribute.BlocksValue); + + // Normalize the state (empty editor becomes null) + onChange(name, normalizeBlocksState(editor, state) as Schema.Attribute.BlocksValue); debounceTimeout.current = null; }, 300); } }, - [editor.operations, incrementSlateUpdatesCount, name, onChange] + [editor, incrementSlateUpdatesCount, name, onChange] ); // Clean up the timeout on unmount @@ -232,8 +248,16 @@ const BlocksEditor = React.forwardRef<{ focus: () => void }, BlocksEditorProps>( // Ensure the editor is in sync after discard React.useEffect(() => { + // Normalize empty states for comparison to avoid losing focus on the editor when content is deleted + const normalizedValue = value?.length ? value : null; + const normalizedEditorState = normalizeBlocksState(editor, editor.children); + // Compare the field value with the editor state to check for a stale selection - if (value && JSON.stringify(editor.children) !== JSON.stringify(value)) { + if ( + normalizedValue && + normalizedEditorState && + JSON.stringify(normalizedEditorState) !== JSON.stringify(normalizedValue) + ) { // When there is a diff, unset selection to avoid an invalid state Transforms.deselect(editor); } @@ -263,7 +287,9 @@ const BlocksEditor = React.forwardRef<{ focus: () => void }, BlocksEditorProps>( {liveText} @@ -314,4 +340,5 @@ export { BlocksEditorProvider, useBlocksEditorContext, isSelectorBlockKey, + normalizeBlocksState, }; diff --git a/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/BlocksInput/tests/normalizeBlocksState.test.ts b/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/BlocksInput/tests/normalizeBlocksState.test.ts new file mode 100644 index 0000000000..09cf55a68c --- /dev/null +++ b/packages/core/content-manager/admin/src/pages/EditView/components/FormInputs/BlocksInput/tests/normalizeBlocksState.test.ts @@ -0,0 +1,40 @@ +import { createEditor, type Editor } from 'slate'; +import { withReact } from 'slate-react'; + +import { normalizeBlocksState } from '../BlocksEditor'; + +import type { Schema } from '@strapi/types'; + +describe('normalizeBlocksState', () => { + let editor: Editor; + + beforeEach(() => { + editor = withReact(createEditor()); + }); + + it('should return null when the editor contains a single empty paragraph block', () => { + const emptyState: Schema.Attribute.BlocksValue = [ + { + type: 'paragraph', + children: [{ type: 'text', text: '' }], + }, + ]; + + const result = normalizeBlocksState(editor, emptyState); + + expect(result).toBeNull(); + }); + + it('should return the state when the editor contains a paragraph with text', () => { + const stateWithText: Schema.Attribute.BlocksValue = [ + { + type: 'paragraph', + children: [{ type: 'text', text: 'Hello world' }], + }, + ]; + + const result = normalizeBlocksState(editor, stateWithText); + + expect(result).toEqual(stateWithText); + }); +});