Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ export default function useInput() {

// Ensure template is not locked.
if (
canInsertBlockType(
getDefaultBlockName(),
getBlockRootClientId( clientId )
) ||
canInsertBlockType(
blockName,
getBlockRootClientId( clientId )
Expand Down
77 changes: 63 additions & 14 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getBlockType,
getBlockTypes,
getBlockVariations,
getDefaultBlockName,
hasBlockSupport,
getPossibleBlockTransformations,
switchToBlockType,
Expand Down Expand Up @@ -1704,7 +1705,10 @@ const canInsertBlockTypeUnmemoized = (
}

const blockEditingMode = getBlockEditingMode( state, rootClientId ?? '' );
if ( blockEditingMode === 'disabled' ) {
if (
blockEditingMode === 'disabled' &&
blockName !== getDefaultBlockName()
) {
return false;
}

Expand All @@ -1720,13 +1724,18 @@ const canInsertBlockTypeUnmemoized = (
// some cases when the block is a content block.
const isContentRoleBlock = isContentBlock( blockName );
const isParentSectionBlock = !! isSectionBlock( state, rootClientId );
const isBlockWithinSection = !! getParentSectionBlock(
state,
rootClientId
);
const sectionClientId = isParentSectionBlock
? rootClientId
: getParentSectionBlock( state, rootClientId );
const isWithinSection = !! sectionClientId;
if ( isWithinSection && ! isContentRoleBlock ) {
return false;
}

// Don't allow insertion into synced patterns.
if (
( isParentSectionBlock || isBlockWithinSection ) &&
! isContentRoleBlock
isWithinSection &&
getBlockName( state, sectionClientId ) === 'core/block'
) {
return false;
}
Expand All @@ -1740,7 +1749,20 @@ const canInsertBlockTypeUnmemoized = (
rootClientId
)
) {
return false;
// Allow inserting the default block anywhere that another default block already exists
// when in contentOnly mode.
if ( blockName === getDefaultBlockName() ) {
const existingBlocks = getBlockOrder( state, rootClientId );
const hasDefaultBlock = existingBlocks.some(
( clientId ) =>
getBlockName( state, clientId ) === getDefaultBlockName()
);
if ( ! hasDefaultBlock ) {
return false;
}
} else {
return false;
}
}

const parentName = getBlockName( state, rootClientId );
Expand Down Expand Up @@ -1894,26 +1916,53 @@ export function canRemoveBlock( state, clientId ) {

// It shouldn't be possible to move in a section block unless in
// some cases when the block is a content block.
const isBlockWithinSection = !! getParentSectionBlock( state, clientId );
const isParentSectionBlock = !! isSectionBlock( state, rootClientId );
const sectionClientId = isParentSectionBlock
? rootClientId
: getParentSectionBlock( state, rootClientId );
const isWithinSection = !! sectionClientId;
const isContentRoleBlock = isContentBlock(
getBlockName( state, clientId )
);
if ( isBlockWithinSection && ! isContentRoleBlock ) {
if ( isWithinSection && ! isContentRoleBlock ) {
return false;
}

// Disallow removal from synced patterns.
if (
isWithinSection &&
getBlockName( state, sectionClientId ) === 'core/block'
) {
return false;
}

const isParentSectionBlock = !! isSectionBlock( state, rootClientId );
const rootBlockEditingMode = getBlockEditingMode( state, rootClientId );
// Check if the parent container allows insertion/removal in contentOnly mode
const blockName = getBlockName( state, clientId );
// Check if the parent container allows insertion/removal in contentOnly mode.
if (
( isParentSectionBlock || rootBlockEditingMode === 'contentOnly' ) &&
( isParentSectionBlock ||
rootBlockEditingMode === 'contentOnly' ||
blockName === getDefaultBlockName() ) &&
! isContainerInsertableToInContentOnlyMode(
state,
getBlockName( state, clientId ),
rootClientId
)
) {
return false;
// Allow removing the default block when other default blocks exist
// in contentOnly mode.
if ( blockName === getDefaultBlockName() ) {
const existingBlocks = getBlockOrder( state, rootClientId );
const defaultBlocks = existingBlocks.filter(
( id ) => getBlockName( state, id ) === getDefaultBlockName()
);
// Allow removal if there are other default blocks besides this one
if ( defaultBlocks.length > 1 ) {
return true;
}
} else {
return false;
}
Comment on lines +1954 to +1965
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker IMO but this doesn't check that the other paragraph blocks are adjacent to the paragraph in question. It simply ensures there's at least 2 default blocks anywhere in the existingBlocks.

I reckon let's see how this change feels in practice, and whether this could use tweaking in follow-ups.

}

return rootBlockEditingMode !== 'disabled';
Expand Down
5 changes: 5 additions & 0 deletions packages/block-editor/src/store/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2676,6 +2676,8 @@ describe( 'selectors', () => {
blocks: {
byClientId: new Map(),
attributes: new Map(),
order: new Map(),
parents: new Map(),
},
blockListSettings: {},
settings: {},
Expand All @@ -2689,6 +2691,8 @@ describe( 'selectors', () => {
blocks: {
byClientId: new Map(),
attributes: new Map(),
order: new Map(),
parents: new Map(),
},
blockListSettings: {},
settings: {
Expand Down Expand Up @@ -2726,6 +2730,7 @@ describe( 'selectors', () => {
byClientId: new Map(),
attributes: new Map(),
order: new Map(),
parents: new Map(),
},
blockListSettings: {},
settings: {
Expand Down
8 changes: 7 additions & 1 deletion packages/block-editor/src/store/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { parse as grammarParse } from '@wordpress/block-serialization-default-pa
import { selectBlockPatternsKey } from './private-keys';
import { unlock } from '../lock-unlock';
import { STORE_NAME } from './constants';
import { getSectionRootClientId, isSectionBlock } from './private-selectors';
import {
getSectionRootClientId,
isSectionBlock,
getParentSectionBlock,
} from './private-selectors';
import { getBlockEditingMode } from './selectors';
import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils';

Expand Down Expand Up @@ -136,10 +140,12 @@ export const getInsertBlockTypeDependants = () => ( state, rootClientId ) => {
return [
state.blockListSettings[ rootClientId ],
state.blocks.byClientId.get( rootClientId ),
state.blocks.order.get( rootClientId || '' ),
state.settings.allowedBlockTypes,
state.settings.templateLock,
getBlockEditingMode( state, rootClientId ),
getSectionRootClientId( state ),
isSectionBlock( state, rootClientId ),
getParentSectionBlock( state, rootClientId ),
];
};
79 changes: 70 additions & 9 deletions test/e2e/specs/editor/various/content-only-lock.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ test.describe( 'Content-only lock', () => {
).not.toBeAttached();
} );

test( 'content role blocks not within a `content` role container cannot be duplicated, inserted before/after, or moved', async ( {
test( 'non-paragraph content role blocks not within a `content` role container cannot be duplicated, inserted before/after, or moved', async ( {
editor,
page,
pageUtils,
Expand All @@ -217,9 +217,9 @@ test.describe( 'Content-only lock', () => {

await page.getByPlaceholder( 'Start writing with text or HTML' )
.fill( `<!-- wp:group {"templateLock":"contentOnly","layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:paragraph -->
<p>First paragraph</p>
<!-- /wp:paragraph -->
<div class="wp-block-group"><!-- wp:heading -->
<h2 class="wp-block-heading">Heading</h2>
<!-- /wp:heading -->

<!-- wp:list -->
<ul class="wp-block-list"><!-- wp:list-item -->
Expand All @@ -237,18 +237,18 @@ test.describe( 'Content-only lock', () => {
const groupBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Group',
} );
const paragraph = editor.canvas
const heading = editor.canvas
.getByRole( 'document', {
name: 'Block: Paragraph',
name: 'Block: Heading',
includeHidden: true,
} )
.filter( { hasText: 'First paragraph' } );
.filter( { hasText: 'Heading' } );

// Select the content-locked group block.
await editor.selectBlocks( groupBlock );
await test.step( 'Blocks cannot be inserted before/after or duplicated', async () => {
// Test paragraph.
await editor.selectBlocks( paragraph );
await editor.selectBlocks( heading );
await editor.showBlockToolbar();

await expect(
Expand All @@ -260,7 +260,7 @@ test.describe( 'Content-only lock', () => {

await test.step( 'Blocks cannot be moved', async () => {
// Test paragraph.
await editor.selectBlocks( paragraph );
await editor.selectBlocks( heading );
await editor.showBlockToolbar();

await expect(
Expand All @@ -277,6 +277,67 @@ test.describe( 'Content-only lock', () => {
} );
} );

test( 'paragraph blocks that are within a `content` role container can be duplicated, inserted before/after, or moved', async ( {
editor,
page,
pageUtils,
} ) => {
// Add content only locked block with paragraph and list
await pageUtils.pressKeys( 'secondary+M' );

await page.getByPlaceholder( 'Start writing with text or HTML' )
.fill( `<!-- wp:group {"templateLock":"contentOnly","layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:paragraph -->
<p>First paragraph</p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:group -->` );

await pageUtils.pressKeys( 'secondary+M' );

const paragraph = editor.canvas.getByRole( 'document', {
name: 'Block: Paragraph',
includeHidden: true,
} );

await test.step( 'Blocks can be inserted before/after or duplicated', async () => {
// Test first list item.
await editor.selectBlocks( paragraph );
await editor.showBlockToolbar();

const firstOptionsButton = page
.getByRole( 'toolbar', { name: 'Block tools' } )
.getByRole( 'button', { name: 'Options' } );

await expect( firstOptionsButton ).toBeVisible();

// Open the options menu.
await firstOptionsButton.click();

// Verify Insert Before, Insert After, and Duplicate menu items are present.
await expect(
page
.getByRole( 'menu', { name: 'Options' } )
.getByRole( 'menuitem', { name: 'Add before' } )
).toBeVisible();

await expect(
page
.getByRole( 'menu', { name: 'Options' } )
.getByRole( 'menuitem', { name: 'Add after' } )
).toBeVisible();

await expect(
page
.getByRole( 'menu', { name: 'Options' } )
.getByRole( 'menuitem', { name: 'Duplicate' } )
).toBeVisible();

// Close the menu.
await page.keyboard.press( 'Escape' );
} );
} );

test( 'content role blocks that are within a `content` role container can be duplicated, inserted before/after, or moved', async ( {
editor,
page,
Expand Down
Loading