[Blocks editor] Manage mixed blocks selection (#18181)

* first draft multiblocks selection fix

* refactor multi blocks selection logic

* remove useless console log

* refactor getAchorBlockKey based on comments

* change test order

* refactor the search using matchNode

* add some comments to explain the useEffect

* fix comments syntax and typos

* fix comments syntax
This commit is contained in:
Simone 2023-10-03 14:25:33 +02:00 committed by GitHub
parent 08a62df085
commit 493eb0a58d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 79 additions and 41 deletions

View File

@ -131,21 +131,6 @@ ModifierButton.propTypes = {
disabled: PropTypes.bool.isRequired,
};
const isBlockActive = (editor, matchNode) => {
const { selection } = editor;
if (!selection) return false;
const match = Array.from(
Editor.nodes(editor, {
at: Editor.unhangRange(editor, selection),
match: (n) => !Editor.isEditor(n) && SlateElement.isElement(n) && matchNode(n),
})
);
return match.length > 0;
};
const toggleBlock = (editor, value) => {
const { type, level } = value;
@ -202,7 +187,7 @@ const ImageDialog = ({ handleClose }) => {
const handleSelectAssets = (images) => {
const formattedImages = images.map((image) => {
// create an object with imageSchema defined and exclude unnecessary props coming from media library config
// Create an object with imageSchema defined and exclude unnecessary props coming from media library config
const expectedImage = pick(image, IMAGE_SCHEMA_FIELDS);
return {
@ -215,7 +200,7 @@ const ImageDialog = ({ handleClose }) => {
insertImages(formattedImages);
if (isLastBlockType(editor, 'image')) {
// insert blank line to add new blocks below image block
// Insert blank line to add new blocks below image block
insertEmptyBlockAtLast(editor);
}
@ -295,7 +280,7 @@ export const BlocksDropdown = ({ disabled }) => {
setBlockSelected(optionKey);
if (optionKey === 'code' && isLastBlockType(editor, 'code')) {
// insert blank line to add new blocks below code block
// Insert blank line to add new blocks below code block
insertEmptyBlockAtLast(editor);
}
@ -304,6 +289,23 @@ export const BlocksDropdown = ({ disabled }) => {
}
};
// Listen to the selection change and update the selected block in the dropdown
React.useEffect(() => {
if (editor.selection) {
// Get the parent node of the anchor
const [anchorNode] = Editor.parent(editor, editor.selection.anchor);
// Find the block key that matches the anchor node
const anchorBlockKey = Object.keys(blocks).find((blockKey) =>
blocks[blockKey].matchNode(anchorNode)
);
// Change the value selected in the dropdown if it doesn't match the anchor block key
if (anchorBlockKey && anchorBlockKey !== blockSelected) {
setBlockSelected(anchorBlockKey);
}
}
}, [editor.selection, editor, blocks, blockSelected]);
return (
<>
<Select
@ -323,8 +325,6 @@ export const BlocksDropdown = ({ disabled }) => {
value={key}
label={blocks[key].label}
icon={blocks[key].icon}
matchNode={blocks[key].matchNode}
handleSelection={setBlockSelected}
blockSelected={blockSelected}
/>
))}
@ -338,19 +338,11 @@ BlocksDropdown.propTypes = {
disabled: PropTypes.bool.isRequired,
};
const BlockOption = ({ value, icon, label, handleSelection, blockSelected, matchNode }) => {
const BlockOption = ({ value, icon, label, blockSelected }) => {
const { formatMessage } = useIntl();
const editor = useSlate();
const isActive = isBlockActive(editor, matchNode);
const isSelected = value === blockSelected;
React.useEffect(() => {
if (isActive && !isSelected) {
handleSelection(value);
}
}, [handleSelection, isActive, isSelected, value]);
return (
<Option
startIcon={<Icon as={icon} color={isSelected ? 'primary600' : 'neutral600'} />}
@ -368,8 +360,6 @@ BlockOption.propTypes = {
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
}).isRequired,
matchNode: PropTypes.func.isRequired,
handleSelection: PropTypes.func.isRequired,
blockSelected: PropTypes.string.isRequired,
};

View File

@ -1,11 +1,11 @@
import * as React from 'react';
import { lightTheme, ThemeProvider } from '@strapi/design-system';
import { render, screen } from '@testing-library/react';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import PropTypes from 'prop-types';
import { IntlProvider } from 'react-intl';
import { createEditor, Transforms } from 'slate';
import { createEditor, Transforms, Editor } from 'slate';
import { Slate, withReact } from 'slate-react';
import { BlocksToolbar, BlocksDropdown } from '..';
@ -28,17 +28,34 @@ const initialValue = [
},
];
const mixedInitialValue = [
{
type: 'heading',
level: 1,
children: [{ type: 'text', text: 'A heading one' }],
},
{
type: 'paragraph',
children: [{ type: 'text', text: 'A line of text in a paragraph.' }],
},
{
type: 'heading',
level: 2,
children: [{ type: 'text', text: 'A heading two' }],
},
];
const user = userEvent.setup();
const baseEditor = createEditor();
const Wrapper = ({ children }) => {
const Wrapper = ({ children, initialData }) => {
const [editor] = React.useState(() => withReact(baseEditor));
return (
<ThemeProvider theme={lightTheme}>
<IntlProvider messages={{}} locale="en">
<Slate initialValue={initialValue} editor={editor}>
<Slate initialValue={initialData} editor={editor}>
{children}
</Slate>
</IntlProvider>
@ -48,24 +65,40 @@ const Wrapper = ({ children }) => {
Wrapper.propTypes = {
children: PropTypes.node.isRequired,
initialData: PropTypes.array,
};
const setup = () =>
Wrapper.defaultProps = {
initialData: initialValue,
};
const setup = (data) => {
render(<BlocksToolbar disabled={false} />, {
wrapper: Wrapper,
wrapper: ({ children }) => <Wrapper initialData={data}>{children}</Wrapper>,
});
};
describe('BlocksEditor toolbar', () => {
beforeEach(() => {
baseEditor.children = initialValue;
});
it('should render the toolbar', () => {
setup();
expect(screen.getByRole('toolbar')).toBeInTheDocument();
});
it('checks if a mixed selected content shows only one option selected in the dropdown when you select only part of the content', async () => {
setup(mixedInitialValue);
const headingsDropdown = screen.getByRole('combobox', { name: /Select a block/i });
// Set the selection to cover the second and third row
Transforms.setSelection(baseEditor, {
anchor: { path: [1, 0], offset: 0 },
});
// The dropdown should show only one option selected which is the block content in the second row
expect(within(headingsDropdown).getByText(/text/i)).toBeInTheDocument();
});
it('toggles the modifier on a selection', async () => {
setup();
@ -299,4 +332,19 @@ describe('BlocksEditor toolbar', () => {
},
]);
});
it('checks if a mixed selected content shows only one option selected in the dropdown', async () => {
setup(mixedInitialValue);
const headingsDropdown = screen.getByRole('combobox', { name: /Select a block/i });
// Set the selection to cover the entire content
Transforms.setSelection(baseEditor, {
anchor: Editor.start(baseEditor, []),
focus: Editor.end(baseEditor, []),
});
// The dropdown should show only one option selected which is the block content in the first row
expect(within(headingsDropdown).getByText(/heading 1/i)).toBeInTheDocument();
});
});