mirror of
https://github.com/strapi/strapi.git
synced 2025-12-12 15:32:42 +00:00
[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:
parent
08a62df085
commit
493eb0a58d
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user