diff --git a/packages/core/admin/admin/src/content-manager/components/BlocksEditor/BlocksInput/index.js b/packages/core/admin/admin/src/content-manager/components/BlocksEditor/BlocksInput/index.js index 40afcc10c3..04b3c0e307 100644 --- a/packages/core/admin/admin/src/content-manager/components/BlocksEditor/BlocksInput/index.js +++ b/packages/core/admin/admin/src/content-manager/components/BlocksEditor/BlocksInput/index.js @@ -62,11 +62,23 @@ const BlocksInput = ({ disabled }) => { } }; + const handleBackspaceEvent = (event) => { + const selectedNode = editor.children[editor.selection.anchor.path[0]]; + const selectedBlock = Object.values(blocks).find((block) => block.matchNode(selectedNode)); + + if (selectedBlock.handleBackspaceKey) { + selectedBlock.handleBackspaceKey(editor, event); + } + }; + const handleKeyDown = (event) => { if (event.key === 'Enter') { event.preventDefault(); handleEnter(); } + if (event.key === 'Backspace') { + handleBackspaceEvent(event); + } }; return ( diff --git a/packages/core/admin/admin/src/content-manager/components/BlocksEditor/hooks/tests/useBlocksStore.test.js b/packages/core/admin/admin/src/content-manager/components/BlocksEditor/hooks/tests/useBlocksStore.test.js index 226ff335f3..e2810790b3 100644 --- a/packages/core/admin/admin/src/content-manager/components/BlocksEditor/hooks/tests/useBlocksStore.test.js +++ b/packages/core/admin/admin/src/content-manager/components/BlocksEditor/hooks/tests/useBlocksStore.test.js @@ -18,6 +18,12 @@ const initialValue = [ }, ]; +const mockEvent = { + preventDefault: jest.fn(), + target: { + value: '', + }, +}; const user = userEvent.setup(); const baseEditor = createEditor(); @@ -608,6 +614,103 @@ describe('useBlocksStore', () => { ]); }); + it('handles the backspace key on a very first list with single empty list item', () => { + const { result } = renderHook(useBlocksStore); + + baseEditor.children = [ + { + type: 'list', + format: 'unordered', + children: [ + { + type: 'list-item', + children: [ + { + type: 'text', + text: '', + }, + ], + }, + ], + }, + ]; + + // Set the cursor on the first list item + Transforms.select(baseEditor, { + anchor: { path: [0, 0, 0], offset: 0 }, + focus: { path: [0, 0, 0], offset: 0 }, + }); + + // Simulate the backspace key + result.current['list-unordered'].handleBackspaceKey(baseEditor, mockEvent); + + // Should remove the empty list item and replace with empty paragraph + expect(baseEditor.children).toEqual([ + { + type: 'paragraph', + children: [ + { + type: 'text', + text: '', + }, + ], + }, + ]); + }); + + it('handles the backspace key on a list with single empty list item', () => { + const { result } = renderHook(useBlocksStore); + + baseEditor.children = [ + { + type: 'paragraph', + children: [ + { + type: 'text', + text: 'some text', + }, + ], + }, + { + type: 'list', + format: 'ordered', + children: [ + { + type: 'list-item', + children: [ + { + type: 'text', + text: '', + }, + ], + }, + ], + }, + ]; + + // Set the cursor on the first list item + Transforms.select(baseEditor, { + anchor: { path: [1, 0, 0], offset: 0 }, + focus: { path: [1, 0, 0], offset: 0 }, + }); + + // Simulate the backspace key + result.current['list-ordered'].handleBackspaceKey(baseEditor, mockEvent); + + // Should remove the empty list item + expect(baseEditor.children).toEqual([ + { + type: 'paragraph', + children: [ + { + type: 'text', + text: 'some text', + }, + ], + }, + ]); + }); + it('handles enter key on a quote', () => { const { result } = renderHook(useBlocksStore); diff --git a/packages/core/admin/admin/src/content-manager/components/BlocksEditor/hooks/useBlocksStore.js b/packages/core/admin/admin/src/content-manager/components/BlocksEditor/hooks/useBlocksStore.js index 5e6a7264fc..019b7428f3 100644 --- a/packages/core/admin/admin/src/content-manager/components/BlocksEditor/hooks/useBlocksStore.js +++ b/packages/core/admin/admin/src/content-manager/components/BlocksEditor/hooks/useBlocksStore.js @@ -151,35 +151,34 @@ List.propTypes = { }).isRequired, }; -const Img = styled.img` - max-width: 100%; -`; +/** + * Common handler for the backspace event on ordered and unordered lists + * @param {import('slate').Editor} editor + * @param {Event} event + */ +const handleBackspaceKeyOnList = (editor, event) => { + const [currentListItem, currentListItemPath] = Editor.parent(editor, editor.selection.anchor); + const [currentList, currentListPath] = Editor.parent(editor, currentListItemPath); + const isListEmpty = currentList.children.length === 1 && currentListItem.children[0].text === ''; -const Image = ({ attributes, children, element }) => { - if (!element.image) return null; - const { url, alternativeText, width, height } = element.image; + if (isListEmpty) { + event.preventDefault(); + // Delete the empty list + Transforms.removeNodes(editor, { at: currentListPath }); - return ( - - {children} - - {alternativeText} - - - ); -}; - -Image.propTypes = { - attributes: PropTypes.object.isRequired, - children: PropTypes.node.isRequired, - element: PropTypes.shape({ - image: PropTypes.shape({ - url: PropTypes.string.isRequired, - alternativeText: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - }), - }).isRequired, + if (currentListPath[0] === 0) { + // If the list was the only(or first) block element then insert empty paragraph as editor needs default value + Transforms.insertNodes( + editor, + { + type: 'paragraph', + children: [{ type: 'text', text: '' }], + }, + { at: currentListPath } + ); + Transforms.select(editor, currentListPath); + } + } }; /** @@ -218,6 +217,37 @@ const handleEnterKeyOnList = (editor) => { } }; +const Img = styled.img` + max-width: 100%; +`; + +const Image = ({ attributes, children, element }) => { + if (!element.image) return null; + const { url, alternativeText, width, height } = element.image; + + return ( + + {children} + + {alternativeText} + + + ); +}; + +Image.propTypes = { + attributes: PropTypes.object.isRequired, + children: PropTypes.node.isRequired, + element: PropTypes.shape({ + image: PropTypes.shape({ + url: PropTypes.string.isRequired, + alternativeText: PropTypes.string, + width: PropTypes.number, + height: PropTypes.number, + }), + }).isRequired, +}; + const Link = React.forwardRef(({ element, children, ...attributes }, forwardedRef) => { const { formatMessage } = useIntl(); const editor = useSlate(); @@ -381,6 +411,7 @@ Link.propTypes = { * matchNode: (node: Object) => boolean, * isInBlocksSelector: true, * handleEnterKey: (editor: import('slate').Editor) => void, + * handleBackspaceKey?:(editor: import('slate').Editor, event: Event) => void, * } * }} an object containing rendering functions and metadata for different blocks, indexed by name. */ @@ -603,6 +634,7 @@ export function useBlocksStore() { // TODO add icon and label and set isInBlocksEditor to true isInBlocksSelector: false, handleEnterKey: handleEnterKeyOnList, + handleBackspaceKey: handleBackspaceKeyOnList, }, 'list-unordered': { renderElement: (props) => , @@ -614,6 +646,7 @@ export function useBlocksStore() { // TODO add icon and label and set isInBlocksEditor to true isInBlocksSelector: false, handleEnterKey: handleEnterKeyOnList, + handleBackspaceKey: handleBackspaceKeyOnList, }, 'list-item': { renderElement: (props) => (