feat(blocks): make it easier to exit code blocks (#18894)

* handle double enter on code blocks

* rename code translations

* allow forced line breaks

* mark feedback

* don't add paragraph when converting to code block
This commit is contained in:
Rémi de Juvigny 2023-11-29 11:19:25 +01:00 committed by GitHub
parent 76a7b8a5b5
commit adbd820884
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 104 additions and 119 deletions

View File

@ -1,11 +1,11 @@
import * as React from 'react';
import { Code } from '@strapi/icons';
import { Transforms } from 'slate';
import styled from 'styled-components';
import { type BlocksStore } from '../BlocksEditor';
import { insertEmptyBlockAtLast, isLastBlockType, baseHandleConvert } from '../utils/conversions';
import { baseHandleConvert } from '../utils/conversions';
import { pressEnterTwiceToExit } from '../utils/enterKey';
import { type Block } from '../utils/types';
const CodeBlock = styled.pre.attrs({ role: 'code' })`
@ -34,20 +34,15 @@ const codeBlocks: Pick<BlocksStore, 'code'> = {
icon: Code,
label: {
id: 'components.Blocks.blocks.code',
defaultMessage: 'Code',
defaultMessage: 'Code block',
},
matchNode: (node) => node.type === 'code',
isInBlocksSelector: true,
handleConvert(editor) {
baseHandleConvert<Block<'code'>>(editor, { type: 'code' });
if (isLastBlockType(editor, 'code')) {
insertEmptyBlockAtLast(editor);
}
},
handleEnterKey(editor) {
// Insert a new line within the block
Transforms.insertText(editor, '\n');
pressEnterTwiceToExit(editor);
},
},
};

View File

@ -1,11 +1,11 @@
import * as React from 'react';
import { Quote } from '@strapi/icons';
import { type Text, Editor, Node, Transforms } from 'slate';
import styled from 'styled-components';
import { type BlocksStore } from '../BlocksEditor';
import { baseHandleConvert } from '../utils/conversions';
import { pressEnterTwiceToExit } from '../utils/enterKey';
import { type Block } from '../utils/types';
const Blockquote = styled.blockquote.attrs({ role: 'blockquote' })`
@ -16,10 +16,6 @@ const Blockquote = styled.blockquote.attrs({ role: 'blockquote' })`
color: ${({ theme }) => theme.colors.neutral600};
`;
const isText = (node: unknown): node is Text => {
return Node.isNode(node) && !Editor.isEditor(node) && node.type === 'text';
};
const quoteBlocks: Pick<BlocksStore, 'quote'> = {
quote: {
renderElement: (props) => <Blockquote {...props.attributes}>{props.children}</Blockquote>,
@ -34,40 +30,7 @@ const quoteBlocks: Pick<BlocksStore, 'quote'> = {
baseHandleConvert<Block<'quote'>>(editor, { type: 'quote' });
},
handleEnterKey(editor) {
/**
* To determine if we should break out of the quote node, check 2 things:
* 1. If the cursor is at the end of the quote node
* 2. If the last line of the quote node is empty
*/
const quoteNodeEntry = Editor.above(editor, {
match: (node) => !Editor.isEditor(node) && node.type === 'quote',
});
if (!quoteNodeEntry || !editor.selection) {
return;
}
const [quoteNode, quoteNodePath] = quoteNodeEntry;
const isNodeEnd = Editor.isEnd(editor, editor.selection.anchor, quoteNodePath);
const lastTextNode = quoteNode.children.at(-1);
const isEmptyLine = isText(lastTextNode) && lastTextNode.text.endsWith('\n');
if (isNodeEnd && isEmptyLine) {
// Remove the last line break
Transforms.delete(editor, { distance: 1, unit: 'character', reverse: true });
// Break out of the quote node new paragraph
Transforms.insertNodes(editor, {
type: 'paragraph',
children: [{ type: 'text', text: '' }],
});
} else {
// Otherwise insert a new line within the quote node
Transforms.insertText(editor, '\n');
// If there's nothing after the cursor, disable modifiers
if (isNodeEnd) {
Editor.removeMark(editor, 'bold');
Editor.removeMark(editor, 'italic');
}
}
pressEnterTwiceToExit(editor);
},
},
};

View File

@ -32,37 +32,65 @@ describe('Code', () => {
expect(code).toBeInTheDocument();
});
it('handles enter key on code block', () => {
it('handles enter key on a code block', () => {
const baseEditor = createEditor();
baseEditor.children = [
{
type: 'code',
children: [
{
type: 'text',
text: 'Line of code',
text: 'Some code',
},
],
},
];
// Set the cursor at the end of the fast list item
// Simulate enter key press at the end of the code
Transforms.select(baseEditor, {
anchor: Editor.end(baseEditor, []),
focus: Editor.end(baseEditor, []),
});
// Simulate the enter key
codeBlocks.code.handleEnterKey!(baseEditor);
// Should insert a newline within the code block (shoudn't exit the code block)
// Should enter a line break within the code
expect(baseEditor.children).toEqual([
{
type: 'code',
children: [
{
type: 'text',
text: 'Line of code\n',
text: 'Some code\n',
},
],
},
]);
// Simulate enter key press at the end of the code again
Transforms.select(baseEditor, {
anchor: Editor.end(baseEditor, []),
focus: Editor.end(baseEditor, []),
});
codeBlocks.code.handleEnterKey!(baseEditor);
// Should delete the line break and create a paragraph after the code
expect(baseEditor.children).toEqual([
{
type: 'code',
children: [
{
type: 'text',
text: 'Some code',
},
],
},
{
type: 'paragraph',
children: [
{
type: 'text',
text: '',
},
],
},
@ -100,64 +128,6 @@ describe('Code', () => {
},
],
},
// Should insert a new paragraph as it was the last block
{
type: 'paragraph',
children: [{ type: 'text', text: '' }],
},
]);
});
it('should not insert an empty block below if the converted block is not the last one', () => {
const baseEditor = createEditor();
baseEditor.children = [
{
type: 'quote',
children: [
{
type: 'text',
text: 'Some quote',
},
],
},
{
type: 'paragraph',
children: [
{
type: 'text',
text: 'Some paragraph',
},
],
},
];
Transforms.select(baseEditor, {
anchor: { path: [0, 0], offset: 0 },
focus: { path: [0, 0], offset: 0 },
});
codeBlocks.code.handleConvert!(baseEditor);
expect(baseEditor.children).toEqual([
{
type: 'code',
children: [
{
type: 'text',
text: 'Some quote',
},
],
},
{
type: 'paragraph',
children: [
{
type: 'text',
text: 'Some paragraph',
},
],
},
// Nothing should be inserted here
]);
});
});

View File

@ -95,7 +95,7 @@ describe('Quote', () => {
]);
});
it('has no modofiers applied when pressing enter key at the end of a quote', () => {
it('has no modifiers applied when pressing enter key at the end of a quote', () => {
const baseEditor = createEditor();
baseEditor.children = [
{

View File

@ -1,6 +1,7 @@
import * as React from 'react';
import { Box } from '@strapi/design-system';
import { Transforms } from 'slate';
import {
type ReactEditor,
type RenderElementProps,
@ -68,7 +69,7 @@ const BlocksContent = ({ placeholder }: BlocksInputProps) => {
[blocks]
);
const handleEnter = () => {
const handleEnter = (event: React.KeyboardEvent<HTMLElement>) => {
if (!editor.selection) {
return;
}
@ -82,6 +83,12 @@ const BlocksContent = ({ placeholder }: BlocksInputProps) => {
return;
}
// Allow forced line breaks when shift is pressed
if (event.shiftKey && selectedNode.type !== 'image') {
Transforms.insertText(editor, '\n');
return;
}
// Check if there's an enter handler for the selected block
if (selectedBlock.handleEnterKey) {
selectedBlock.handleEnterKey(editor);
@ -126,7 +133,7 @@ const BlocksContent = ({ placeholder }: BlocksInputProps) => {
const handleKeyDown: React.KeyboardEventHandler<HTMLElement> = (event) => {
if (event.key === 'Enter') {
event.preventDefault();
handleEnter();
handleEnter(event);
}
if (event.key === 'Backspace') {
handleBackspaceEvent(event);

View File

@ -117,7 +117,7 @@ const modifiers: ModifiersStore = {
code: {
icon: Code,
isValidEventKey: (event) => event.key === 'e',
label: { id: 'components.Blocks.modifiers.code', defaultMessage: 'Code' },
label: { id: 'components.Blocks.modifiers.code', defaultMessage: 'Inline code' },
checkIsActive: (editor) => baseCheckIsActive(editor, 'code'),
handleToggle: (editor) => baseHandleToggle(editor, 'code'),
renderLeaf: (children) => <InlineCode>{children}</InlineCode>,

View File

@ -465,7 +465,7 @@ describe('BlocksToolbar', () => {
// Convert it to a code block
const selectDropdown = screen.getByRole('combobox', { name: /Select a block/i });
await user.click(selectDropdown);
await user.click(screen.getByRole('option', { name: 'Code' }));
await user.click(screen.getByRole('option', { name: 'Code block' }));
// The list should have been split in two
expect(baseEditor.children).toEqual([

View File

@ -0,0 +1,49 @@
import { type Text, Editor, Node, Transforms } from 'slate';
const isText = (node: unknown): node is Text => {
return Node.isNode(node) && !Editor.isEditor(node) && node.type === 'text';
};
/**
* Inserts a line break the first time the user presses enter, and exits the node the second time.
*/
const pressEnterTwiceToExit = (editor: Editor) => {
/**
* To determine if we should break out of the node, check 2 things:
* 1. If the cursor is at the end of the node
* 2. If the last line of the node is empty
*/
const nodeEntry = Editor.above(editor, {
match: (node) => !Editor.isEditor(node) && !['link', 'text'].includes(node.type),
});
if (!nodeEntry || !editor.selection) {
return;
}
const [node, nodePath] = nodeEntry;
const isNodeEnd = Editor.isEnd(editor, editor.selection.anchor, nodePath);
const lastTextNode = node.children.at(-1);
const isEmptyLine = isText(lastTextNode) && lastTextNode.text.endsWith('\n');
if (isNodeEnd && isEmptyLine) {
// Remove the last line break
Transforms.delete(editor, { distance: 1, unit: 'character', reverse: true });
// Break out of the node by creating a new paragraph
Transforms.insertNodes(editor, {
type: 'paragraph',
children: [{ type: 'text', text: '' }],
});
return;
}
// Otherwise insert a new line within the node
Transforms.insertText(editor, '\n');
// If there's nothing after the cursor, disable modifiers
if (isNodeEnd) {
['bold', 'italic', 'underline', 'strikethrough', 'code'].forEach((modifier) => {
Editor.removeMark(editor, modifier);
});
}
};
export { pressEnterTwiceToExit };

View File

@ -629,7 +629,7 @@
"components.Blocks.modifiers.italic": "Italic",
"components.Blocks.modifiers.underline": "Underline",
"components.Blocks.modifiers.strikethrough": "Strikethrough",
"components.Blocks.modifiers.code": "Code",
"components.Blocks.modifiers.code": "Inline code",
"components.Blocks.link": "Link",
"components.Blocks.popover.text": "Text",
"components.Blocks.popover.text.placeholder": "Enter link text",
@ -646,6 +646,7 @@
"components.Blocks.blocks.heading4": "Heading 4",
"components.Blocks.blocks.heading5": "Heading 5",
"components.Blocks.blocks.heading6": "Heading 6",
"components.Blocks.blocks.code": "Code block",
"components.Blocks.blocks.quote": "Quote",
"components.Blocks.blocks.image": "Image",
"components.Blocks.blocks.unorderedList": "Bulleted list",