Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/volto/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -832,8 +832,8 @@ function shouldVerifyContent(type) {
return !type.includes('{');
}

Cypress.Commands.add('getSlateEditorAndType', (type) => {
cy.getSlate().click().trigger('focus').type(type);
Cypress.Commands.add('getSlateEditorAndType', (type, options = {}) => {
cy.getSlate().click(options).trigger('focus', options).type(type, options);

if (shouldVerifyContent(type)) {
return cy.getSlate().should('contain', type);
Expand Down
15 changes: 11 additions & 4 deletions packages/volto/cypress/tests/core/blocks/blocks-copypaste.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@ describe('Blocks copy/paste', () => {
cy.get('a[aria-label="Edit"]').click();

// WHEN: I copy paste them
cy.getSlateTitle().focus().click().type('{shift}', { release: false });
// NOTE: focus the title without clicking, like the Delete test below.
// A click leaves a text-selection anchor in the title; when Cypress
// then scrolls the image block into view, the browser can re-anchor
// the scroll on that caret, moving the sticky page header over the
// image click point (Volto 17 layout), so the click lands on the
// header instead of the block. Focus alone already selects the title.
cy.getSlateTitle().focus().type('{shift}', { release: false });
cy.get('.block-editor-maps').click();
cy.get('#toolbar-copy-blocks').click();

cy.getSlateEditorAndType('{shift}').click();
cy.getSlateEditorAndType('{shift}', { force: true }).click({ force: true });
cy.get('#toolbar-paste-blocks').should('be.visible');
cy.get('#toolbar-paste-blocks').click();

Expand Down Expand Up @@ -77,11 +83,12 @@ describe('Blocks copy/paste', () => {
cy.get('a[aria-label="Edit"]').click();

// WHEN: I cut paste them
cy.getSlateTitle().focus().click().type('{shift}', { release: false });
// Same focus-without-click gesture as the Copy test (see note there).
cy.getSlateTitle().focus().type('{shift}', { release: false });
cy.get('.block-editor-maps').click();
cy.get('#toolbar-cut-blocks').click();

cy.getSlateEditorAndType('{shift}').click();
cy.getSlateEditorAndType('{shift}', { force: true }).click({ force: true });

cy.get('#toolbar-paste-blocks').should('be.visible');
cy.get('#toolbar-paste-blocks').click();
Expand Down
68 changes: 68 additions & 0 deletions packages/volto/cypress/tests/core/blocks/blocks-grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,73 @@ context('Blocks Acceptance Tests', () => {
.should('have.attr', 'href')
.and('include', 'https://google.com');
});

it('As editor I can copy a block into a Grid', function () {
cy.intercept('PATCH', '/**/document').as('edit');

// GIVEN: a text block outside the grid
cy.getSlate().click().type('Copy me into the grid');

// WHEN: shift+click the text block to multi-select it, then copy
cy.get('.block-editor-slate').first().click();
cy.get('.block-editor-slate').first().click({ shiftKey: true });
cy.get('#toolbar-copy-blocks').should('be.visible');
cy.get('#toolbar-copy-blocks').click();

// AND: add a grid block below and an empty text block inside it
cy.getSlate().click().type('{moveToEnd}{enter}');
cy.addNewBlock('grid', true);
cy.findByText('2 columns').click();

cy.get('button[aria-label="Add block in position 0"]').click();
cy.get('.blocks-chooser [aria-label="Unfold Text blocks"]').click();
cy.wait(200);
cy.get('.blocks-chooser .text .button.slate').click();

// THEN: the paste button appears and pastes inside the grid
cy.get('#toolbar-paste-blocks').should('be.visible');
cy.get('#toolbar-paste-blocks').click();

cy.get('#toolbar-save').click();
cy.wait('@edit');
cy.wait('@content');

cy.findAllByText('Copy me into the grid').should('have.length', 2);
});

it('As editor I can cut a block into a Grid', function () {
cy.intercept('PATCH', '/**/document').as('edit');

// GIVEN: a text block outside the grid
cy.getSlate().click().type('Copy me into the grid');

// WHEN: shift+click the text block to multi-select it, then copy
cy.get('.block-editor-slate').first().click();
cy.get('.block-editor-slate').first().click({ shiftKey: true });
cy.get('#toolbar-cut-blocks').should('be.visible');
cy.get('#toolbar-cut-blocks').click();

cy.getSlateTitle().focus().click().type('{enter}');
cy.getSlate().click();
cy.addNewBlock('grid', true);
// AND: add a grid block below and an empty text block inside it

cy.findByText('2 columns').click();

cy.get('button[aria-label="Add block in position 0"]').click();
cy.get('.blocks-chooser [aria-label="Unfold Text blocks"]').click();
cy.wait(200);
cy.get('.blocks-chooser .text .button.slate').click();

// THEN: the paste button appears and pastes inside the grid
cy.get('#toolbar-paste-blocks').should('be.visible');
cy.get('#toolbar-paste-blocks').click();

cy.get('#toolbar-save').click();
cy.wait('@edit');
cy.wait('@content');

cy.findAllByText('Copy me into the grid').should('have.length', 1);
});
});
});
1 change: 1 addition & 0 deletions packages/volto/news/8410.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed multi-selection of blocks within container blocks (grid, group, columns) and ensured the copy/paste toolbar works reliably across nested forms. @nileshgulia1
57 changes: 40 additions & 17 deletions packages/volto/src/components/manage/Blocks/Block/Edit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import cx from 'classnames';
import { setSidebarTab } from '@plone/volto/actions/sidebar/sidebar';
import { setUIState } from '@plone/volto/actions/form/form';

import config from '@plone/volto/registry';
import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
import ViewDefaultBlock from '@plone/volto/components/manage/Blocks/Block/DefaultView';
Expand Down Expand Up @@ -58,6 +59,8 @@ export class Edit extends Component {
* @property {Object} defaultProps Default properties.
* @static
*/
blockNode = React.createRef();

static defaultProps = {
manage: false,
editable: true,
Expand Down Expand Up @@ -113,7 +116,22 @@ export class Edit extends Component {
}
}

blockNode = React.createRef();
/**
* True when the event originates inside a nested block form rendered by
* this block (e.g. a child block of a Group, Columns, Grid, Tabs or
* Accordion container). Child modifier semantics belong to the nested
* form; the container may only activate itself as a single selection.
* @param {Event} event the synthetic event
* @returns {boolean}
*/
isNestedFormEvent = (event) => {
const node = this.blockNode.current;
if (!node || !event?.target?.closest) {
return false;
}
const nestedForm = event.target.closest('.blocks-form');
return !!nestedForm && node.contains(nestedForm);
};

/**
* Render method.
Expand Down Expand Up @@ -154,25 +172,31 @@ export class Edit extends Component {
e.stopPropagation();
this.props.setUIState({ hovered: null });
}}
onMouseDown={(e) => {
if (this.isNestedFormEvent(e)) {
return;
}
const isMultipleSelection = e.shiftKey || e.ctrlKey || e.metaKey;
if (!this.props.selected && isMultipleSelection) {
e.preventDefault();
}
}}
onClick={(e) => {
if (this.isNestedFormEvent(e)) {
!this.props.selected &&
this.props.onSelectBlock(this.props.id, false);
return;
}
const isMultipleSelection = e.shiftKey || e.ctrlKey || e.metaKey;
!this.props.selected &&
this.props.onSelectBlock(
this.props.id,
this.props.selected ? false : isMultipleSelection,
e,
);
(!this.props.selected || isMultipleSelection) &&
this.props.onSelectBlock(this.props.id, isMultipleSelection, e);
}}
// onFocus={(e) => {
// // TODO: This `onFocus` steals somehow the focus from the slate block
// // we have to investigate why this is happening
// // Apparently, I can't see any difference in the behavior
// // If any, we can fix it in successive iterations
// // if (this.props.hovered !== this.props.id) {
// // this.props.setUIState({ hovered: this.props.id });
// // }
// }}
onFocus={(e) => {
if (this.isNestedFormEvent(e)) {
!this.props.selected &&
this.props.onSelectBlock(this.props.id, false);
return;
}
const isMultipleSelection = e.shiftKey || e.ctrlKey || e.metaKey;
!this.props.selected &&
this.props.onSelectBlock(
Expand Down Expand Up @@ -227,7 +251,6 @@ export class Edit extends Component {
e.stopPropagation();
this.props.setUIState({ hovered: this.props.id });
}}
// Mantenha apenas este onFocus ou remova se não for necessário
onFocus={(e) => {
e.preventDefault();
e.stopPropagation();
Expand Down
Loading
Loading