diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js
index 3d6d16be11eacf..99b78b0ce3caf9 100644
--- a/packages/block-editor/src/components/inserter/menu.js
+++ b/packages/block-editor/src/components/inserter/menu.js
@@ -33,6 +33,7 @@ import useInsertionPoint from './hooks/use-insertion-point';
import { store as blockEditorStore } from '../../store';
import TabbedSidebar from '../tabbed-sidebar';
import { useZoomOut } from '../../hooks/use-zoom-out';
+import { unlock } from '../../lock-unlock';
const NOOP = () => {};
function InserterMenu(
@@ -58,6 +59,11 @@ function InserterMenu(
select( blockEditorStore ).__unstableGetEditorMode() === 'zoom-out',
[]
);
+ const hasSectionRootClientId = useSelect(
+ ( select ) =>
+ !! unlock( select( blockEditorStore ) ).getSectionRootClientId(),
+ []
+ );
const [ filterValue, setFilterValue, delayedFilterValue ] =
useDebouncedInput( __experimentalFilterValue );
const [ hoveredItem, setHoveredItem ] = useState( null );
@@ -82,7 +88,9 @@ function InserterMenu(
const [ selectedTab, setSelectedTab ] = useState( getInitialTab() );
const shouldUseZoomOut =
- selectedTab === 'patterns' || selectedTab === 'media';
+ hasSectionRootClientId &&
+ ( selectedTab === 'patterns' || selectedTab === 'media' );
+
useZoomOut( shouldUseZoomOut && isLargeViewport );
const [ destinationRootClientId, onInsertBlocks, onToggleInsertionPoint ] =
diff --git a/packages/editor/src/components/header/index.js b/packages/editor/src/components/header/index.js
index eb0dd68647f56f..2a336eebe72798 100644
--- a/packages/editor/src/components/header/index.js
+++ b/packages/editor/src/components/header/index.js
@@ -24,6 +24,7 @@ import PostViewLink from '../post-view-link';
import PreviewDropdown from '../preview-dropdown';
import ZoomOutToggle from '../zoom-out-toggle';
import { store as editorStore } from '../../store';
+import { unlock } from '../../lock-unlock';
const toolbarVariations = {
distractionFreeDisabled: { y: '-50px' },
@@ -91,6 +92,16 @@ function Header( {
// The edit-post-header classname is only kept for backward compatibilty
// as some plugins might be relying on its presence.
+ const hasSectionRootClientId = useSelect(
+ ( select ) =>
+ !! unlock( select( blockEditorStore ) ).getSectionRootClientId(),
+ []
+ );
+
+ /*
+ * The edit-post-header classname is only kept for backward compatability
+ * as some plugins might be relying on its presence.
+ */
return (
{ hasBackButton && (
@@ -150,9 +161,11 @@ function Header( {
/>
- { isEditorIframed && isWideViewport && (
-
- ) }
+ { isEditorIframed &&
+ isWideViewport &&
+ hasSectionRootClientId && (
+
+ ) }
{ ( isWideViewport || ! showIconLabels ) && (
diff --git a/test/e2e/specs/editor/various/parsing-patterns.spec.js b/test/e2e/specs/editor/various/parsing-patterns.spec.js
index 3b801591aed5de..4170d3fcaed9b2 100644
--- a/test/e2e/specs/editor/various/parsing-patterns.spec.js
+++ b/test/e2e/specs/editor/various/parsing-patterns.spec.js
@@ -37,9 +37,8 @@ test.describe( 'Parsing patterns', () => {
} );
} );
- // Exit zoom out mode and select the inner buttons block to ensure
+ // Select the inner buttons block to ensure
// the correct insertion point is selected.
- await page.getByRole( 'button', { name: 'Zoom Out' } ).click();
await editor.selectBlocks(
editor.canvas.locator( 'role=document[name="Block: Button"i]' )
);
diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js
index 5fbd0e66b5fd02..55d1557886f211 100644
--- a/test/e2e/specs/editor/various/pattern-overrides.spec.js
+++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js
@@ -226,6 +226,373 @@ test.describe( 'Pattern Overrides', () => {
} );
} );
+ test.describe( 'block editing modes', () => {
+ test.beforeEach( async ( { page } ) => {
+ await page.addInitScript( () => {
+ window.__experimentalEditorWriteMode = true;
+ } );
+ } );
+
+ test( 'blocks with bindings in a synced pattern are editable, and all other blocks are disabled', async ( {
+ admin,
+ editor,
+ page,
+ requestUtils,
+ } ) => {
+ const content = `
+
+
Pattern Overrides
+
+
+
Post Meta Binding
+
+
+
No Overrides or Binding
+
+ `;
+
+ const { id } = await requestUtils.createBlock( {
+ title: 'Pattern',
+ content,
+ status: 'publish',
+ } );
+
+ await admin.visitSiteEditor( {
+ postId: 'emptytheme//index',
+ postType: 'wp_template',
+ canvas: 'edit',
+ } );
+
+ await editor.setContent( '' );
+ await editor.switchEditorTool( 'Design' );
+
+ // Insert a `
` group block.
+ // In zoomed out and write mode it acts as the section root.
+ // Inside is a pattern that acts as a section.
+ await editor.insertBlock( {
+ name: 'core/group',
+ attributes: { tagName: 'main' },
+ innerBlocks: [
+ {
+ name: 'core/block',
+ attributes: { ref: id },
+ },
+ ],
+ } );
+
+ const groupBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Group',
+ } );
+ const patternBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Pattern',
+ } );
+ const paragraphs = editor.canvas.getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } );
+ const blockWithOverrides = paragraphs.filter( {
+ hasText: 'Pattern Overrides',
+ } );
+ const blockWithBindings = paragraphs.filter( {
+ hasText: 'Post Meta Binding',
+ } );
+ const blockWithoutOverridesOrBindings = paragraphs.filter( {
+ hasText: 'No Overrides or Binding',
+ } );
+
+ await test.step( 'Click-through behavior', async () => {
+ // With the group block selected, all the inner blocks of the pattern
+ // are inert due to the 'click-through' behavior, that requires the
+ // pattern block be selected first before its inner blocks are selectable.
+ await editor.selectBlocks( groupBlock );
+ await expect( patternBlock ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithOverrides ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ await test.step( 'Zoomed in / Design mode', async () => {
+ await editor.selectBlocks( patternBlock );
+
+ // Once selected and in zoomed in/design mode the child blocks
+ // of the pattern with bindings are editable, but unbound
+ // blocks are inert.
+ await expect( blockWithOverrides ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ await test.step( 'Zoomed in / Write mode - pattern as a section', async () => {
+ await editor.switchEditorTool( 'Write' );
+
+ // The pattern block is still editable as a section.
+ await expect( patternBlock ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+
+ // Ensure the pattern block is selected.
+ await editor.selectBlocks( patternBlock );
+
+ // Child blocks of the pattern with bindings are editable.
+ await expect( blockWithOverrides ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ await test.step( 'Zoomed out / Write mode - pattern as a section', async () => {
+ await page.getByLabel( 'Zoom Out' ).click();
+ // In zoomed out only the pattern block is editable,
+ // as in this scenario it's a section.
+ await expect( patternBlock ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+
+ // Ensure the pattern block is selected before checking the child blocks
+ // to ensure the click-through behavior isn't interfering.
+ await editor.selectBlocks( patternBlock );
+
+ // None of the child blocks are editable in zoomed out mode.
+ await expect( blockWithOverrides ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ await test.step( 'Zoomed out / Design mode - pattern as a section', async () => {
+ await editor.switchEditorTool( 'Design' );
+ // In zoomed out only the pattern block is editable,
+ // as in this scenario it's a section.
+ await expect( patternBlock ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+
+ // Ensure the pattern block is selected before checking the child blocks
+ // to ensure the click-through behavior isn't interfering.
+ await editor.selectBlocks( patternBlock );
+
+ await expect( blockWithOverrides ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ // Zoom out and group the pattern so that it's no longer a section.
+ await page.getByLabel( 'Zoom Out' ).click();
+ await editor.selectBlocks( patternBlock );
+ await editor.clickBlockOptionsMenuItem( 'Group' );
+
+ await test.step( 'Zoomed in / Write mode - pattern nested in a section', async () => {
+ await editor.switchEditorTool( 'Write' );
+ // The pattern block is not inert as it has editable content, but it shouldn't be selectable.
+ // TODO: find a way to test that the block is not selectable.
+ await expect( patternBlock ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ // Child blocks of the pattern are editable as normal.
+ await expect( blockWithOverrides ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ await test.step( 'Zoomed out / Write mode - pattern nested in a section', async () => {
+ await page.getByLabel( 'Zoom Out' ).click();
+ // None of the pattern is editable in zoomed out when nested in a section.
+ await expect( patternBlock ).toHaveAttribute( 'inert', 'true' );
+ await expect( blockWithOverrides ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ await test.step( 'Zoomed out / Design mode - pattern nested in a section', async () => {
+ await editor.switchEditorTool( 'Design' );
+ // None of the pattern is editable in zoomed out when nested in a section.
+ await expect( patternBlock ).toHaveAttribute( 'inert', 'true' );
+ await expect( blockWithOverrides ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+ } );
+
+ test( 'disables editing of nested patterns', async ( {
+ page,
+ admin,
+ requestUtils,
+ editor,
+ } ) => {
+ const paragraphName = 'Editable paragraph';
+ const headingName = 'Editable heading';
+ const innerPattern = await requestUtils.createBlock( {
+ title: 'Inner Pattern',
+ content: `
+ Inner paragraph
+ `,
+ status: 'publish',
+ } );
+ const outerPattern = await requestUtils.createBlock( {
+ title: 'Outer Pattern',
+ content: `
+ Outer heading
+
+ `,
+ status: 'publish',
+ } );
+
+ await admin.createNewPost();
+
+ await editor.insertBlock( {
+ name: 'core/block',
+ attributes: { ref: outerPattern.id },
+ } );
+
+ // Make an edit to the outer pattern heading.
+ await editor.canvas
+ .getByRole( 'document', { name: 'Block: Heading' } )
+ .fill( 'Outer heading (edited)' );
+
+ const postId = await editor.publishPost();
+
+ // Check the pattern has the correct attributes.
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/block',
+ attributes: {
+ ref: outerPattern.id,
+ content: {
+ [ headingName ]: {
+ content: 'Outer heading (edited)',
+ },
+ },
+ },
+ innerBlocks: [],
+ },
+ ] );
+ // Check it renders correctly.
+ const headingBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Heading',
+ } );
+ const paragraphBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } );
+ await expect( headingBlock ).toHaveText( 'Outer heading (edited)' );
+ await expect( headingBlock ).not.toHaveAttribute( 'inert', 'true' );
+ await expect( paragraphBlock ).toHaveText(
+ 'Inner paragraph (edited)'
+ );
+ await expect( paragraphBlock ).toHaveAttribute( 'inert', 'true' );
+
+ // Edit the outer pattern.
+ await editor.selectBlocks(
+ editor.canvas
+ .getByRole( 'document', { name: 'Block: Pattern' } )
+ .first()
+ );
+ await editor.showBlockToolbar();
+ await page
+ .getByRole( 'toolbar', { name: 'Block tools' } )
+ .getByRole( 'button', { name: 'Edit original' } )
+ .click();
+
+ // The inner paragraph should be editable in the pattern focus mode.
+ await editor.selectBlocks(
+ editor.canvas
+ .getByRole( 'document', { name: 'Block: Pattern' } )
+ .first()
+ );
+ await expect(
+ editor.canvas.getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } ),
+ 'The inner paragraph should be editable'
+ ).not.toHaveAttribute( 'inert', 'true' );
+
+ // Visit the post on the frontend.
+ await page.goto( `/?p=${ postId }` );
+
+ await expect(
+ page.getByRole( 'heading', { level: 2 } )
+ ).toHaveText( 'Outer heading (edited)' );
+ await expect(
+ page.getByText( 'Inner paragraph (edited)' )
+ ).toBeVisible();
+ } );
+ } );
+
test( 'retains override values when converting a pattern block to regular blocks', async ( {
page,
admin,
diff --git a/test/e2e/specs/site-editor/zoom-out.spec.js b/test/e2e/specs/site-editor/zoom-out.spec.js
index 2a299fd474c737..531d3566efa449 100644
--- a/test/e2e/specs/site-editor/zoom-out.spec.js
+++ b/test/e2e/specs/site-editor/zoom-out.spec.js
@@ -4,7 +4,8 @@
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
const EDITOR_ZOOM_OUT_CONTENT = `
-
+
+
First Section Start
@@ -58,6 +59,21 @@ const EDITOR_ZOOM_OUT_CONTENT = `
Fourth Section End
+
+`;
+
+const EDITOR_ZOOM_OUT_CONTENT_NO_SECTION_ROOT = `
+
+
First Section Start
+
+
+
+
First Section Center
+
+
+
+
First Section End
+
`;
// The test is flaky and fails almost consistently.
@@ -77,6 +93,8 @@ test.describe( 'Zoom Out', () => {
test.afterAll( async ( { requestUtils } ) => {
await requestUtils.activateTheme( 'twentytwentyone' );
+ await requestUtils.deleteAllTemplates( 'wp_template' );
+ await requestUtils.deleteAllTemplates( 'wp_template_part' );
} );
test.skip( 'Clicking on inserter while on zoom-out should open the patterns tab on the inserter', async ( {
@@ -212,4 +230,29 @@ test.describe( 'Zoom Out', () => {
await expect( thirdSectionEnd ).toBeInViewport();
await expect( fourthSectionStart ).not.toBeInViewport();
} );
+
+ test( 'Zoom Out cannot be activated when the section root is missing', async ( {
+ page,
+ editor,
+ } ) => {
+ await editor.setContent( EDITOR_ZOOM_OUT_CONTENT_NO_SECTION_ROOT );
+
+ // Check that the Zoom Out toggle button is not visible.
+ await expect(
+ page.getByRole( 'button', { name: 'Zoom Out' } )
+ ).toBeHidden();
+
+ // Check that activating the Patterns tab in the Inserter does not activate
+ // Zoom Out.
+ await page
+ .getByRole( 'button', {
+ name: 'Block Inserter',
+ exact: true,
+ } )
+ .click();
+
+ await page.getByRole( 'tab', { name: 'Patterns' } ).click();
+
+ await expect( page.locator( '.is-zoomed-out' ) ).toBeHidden();
+ } );
} );