[Blocks Editor] Fix empty list backspace issue (#18260)

* fixed backspace key for empty list

* fixed backspace key for empty list

* added a check for handleBackspaceKey

* test case updated for ordered-list, colocated code for list block
This commit is contained in:
Madhuri Sandbhor 2023-10-04 16:18:08 +05:30 committed by GitHub
parent 9b2cfe1ae1
commit 502bd29738
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 175 additions and 27 deletions

View File

@ -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) => { const handleKeyDown = (event) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
handleEnter(); handleEnter();
} }
if (event.key === 'Backspace') {
handleBackspaceEvent(event);
}
}; };
return ( return (

View File

@ -18,6 +18,12 @@ const initialValue = [
}, },
]; ];
const mockEvent = {
preventDefault: jest.fn(),
target: {
value: '',
},
};
const user = userEvent.setup(); const user = userEvent.setup();
const baseEditor = createEditor(); 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', () => { it('handles enter key on a quote', () => {
const { result } = renderHook(useBlocksStore); const { result } = renderHook(useBlocksStore);

View File

@ -151,35 +151,34 @@ List.propTypes = {
}).isRequired, }).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 (isListEmpty) {
if (!element.image) return null; event.preventDefault();
const { url, alternativeText, width, height } = element.image; // Delete the empty list
Transforms.removeNodes(editor, { at: currentListPath });
return ( if (currentListPath[0] === 0) {
<Box {...attributes}> // If the list was the only(or first) block element then insert empty paragraph as editor needs default value
{children} Transforms.insertNodes(
<Box contentEditable={false}> editor,
<Img src={url} alt={alternativeText} width={width} height={height} /> {
</Box> type: 'paragraph',
</Box> children: [{ type: 'text', text: '' }],
},
{ at: currentListPath }
); );
}; Transforms.select(editor, currentListPath);
}
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,
}; };
/** /**
@ -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 (
<Box {...attributes}>
{children}
<Box contentEditable={false}>
<Img src={url} alt={alternativeText} width={width} height={height} />
</Box>
</Box>
);
};
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 Link = React.forwardRef(({ element, children, ...attributes }, forwardedRef) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const editor = useSlate(); const editor = useSlate();
@ -381,6 +411,7 @@ Link.propTypes = {
* matchNode: (node: Object) => boolean, * matchNode: (node: Object) => boolean,
* isInBlocksSelector: true, * isInBlocksSelector: true,
* handleEnterKey: (editor: import('slate').Editor) => void, * 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. * }} 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 // TODO add icon and label and set isInBlocksEditor to true
isInBlocksSelector: false, isInBlocksSelector: false,
handleEnterKey: handleEnterKeyOnList, handleEnterKey: handleEnterKeyOnList,
handleBackspaceKey: handleBackspaceKeyOnList,
}, },
'list-unordered': { 'list-unordered': {
renderElement: (props) => <List {...props} />, renderElement: (props) => <List {...props} />,
@ -614,6 +646,7 @@ export function useBlocksStore() {
// TODO add icon and label and set isInBlocksEditor to true // TODO add icon and label and set isInBlocksEditor to true
isInBlocksSelector: false, isInBlocksSelector: false,
handleEnterKey: handleEnterKeyOnList, handleEnterKey: handleEnterKeyOnList,
handleBackspaceKey: handleBackspaceKeyOnList,
}, },
'list-item': { 'list-item': {
renderElement: (props) => ( renderElement: (props) => (