diff --git a/packages/block-editor/src/components/block-preview/index.js b/packages/block-editor/src/components/block-preview/index.js
index cea2e8ee9b62c0..b43d8e9a74abd2 100644
--- a/packages/block-editor/src/components/block-preview/index.js
+++ b/packages/block-editor/src/components/block-preview/index.js
@@ -2,10 +2,15 @@
* External dependencies
*/
import { castArray } from 'lodash';
+import classnames from 'classnames';
/**
* WordPress dependencies
*/
+import {
+ __experimentalUseDisabled as useDisabled,
+ useMergeRefs,
+} from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
import { memo, useMemo } from '@wordpress/element';
@@ -16,6 +21,7 @@ import BlockEditorProvider from '../provider';
import LiveBlockPreview from './live';
import AutoHeightBlockPreview from './auto';
import { store as blockEditorStore } from '../../store';
+import { BlockListItems } from '../block-list';
export function BlockPreview( {
blocks,
@@ -63,3 +69,57 @@ export function BlockPreview( {
* @return {WPComponent} The component to be rendered.
*/
export default memo( BlockPreview );
+
+/**
+ * This hook is used to lightly mark an element as a block preview wrapper
+ * element. Call this hook and pass the returned props to the element to mark as
+ * a block preview wrapper, automatically rendering inner blocks as children. If
+ * you define a ref for the element, it is important to pass the ref to this
+ * hook, which the hook in turn will pass to the component through the props it
+ * returns. Optionally, you can also pass any other props through this hook, and
+ * they will be merged and returned.
+ *
+ * @param {Object} options Preview options.
+ * @param {WPBlock[]} options.blocks Block objects.
+ * @param {Object} options.props Optional. Props to pass to the element. Must contain
+ * the ref if one is defined.
+ * @param {Object} options.__experimentalLayout Layout settings to be used in the preview.
+ *
+ */
+export function useBlockPreview( {
+ blocks,
+ props = {},
+ __experimentalLayout,
+} ) {
+ const originalSettings = useSelect(
+ ( select ) => select( blockEditorStore ).getSettings(),
+ []
+ );
+ const disabledRef = useDisabled();
+ const ref = useMergeRefs( [ props.ref, disabledRef ] );
+ const settings = useMemo(
+ () => ( { ...originalSettings, __experimentalBlockPatterns: [] } ),
+ [ originalSettings ]
+ );
+ const renderedBlocks = useMemo( () => castArray( blocks ), [ blocks ] );
+
+ const children = (
+
+
+
+ );
+
+ return {
+ ...props,
+ ref,
+ className: classnames(
+ props.className,
+ 'block-editor-block-preview__live-content',
+ 'components-disabled'
+ ),
+ children: blocks?.length ? children : null,
+ };
+}
diff --git a/packages/block-editor/src/components/block-preview/style.scss b/packages/block-editor/src/components/block-preview/style.scss
index bbcccc58a7d8ad..7c522fec5c38c1 100644
--- a/packages/block-editor/src/components/block-preview/style.scss
+++ b/packages/block-editor/src/components/block-preview/style.scss
@@ -41,3 +41,26 @@
.block-editor-block-preview__content-iframe .block-list-appender {
display: none;
}
+
+.block-editor-block-preview__live-content {
+ * {
+ pointer-events: none;
+ }
+
+ // Hide the block appender, as the block is not editable in this context.
+ .block-list-appender {
+ display: none;
+ }
+
+ // Revert button disable styles to ensure that button styles render as they will on the
+ // front end of the site. For example, this ensures that Social Links icons display correctly.
+ .components-button:disabled {
+ opacity: initial;
+ }
+
+ // Hide placeholders.
+ .components-placeholder,
+ .block-editor-block-list__block[data-empty="true"] {
+ display: none;
+ }
+}
diff --git a/packages/block-editor/src/components/block-preview/test/index.js b/packages/block-editor/src/components/block-preview/test/index.js
new file mode 100644
index 00000000000000..9dcd070b83d69a
--- /dev/null
+++ b/packages/block-editor/src/components/block-preview/test/index.js
@@ -0,0 +1,114 @@
+/**
+ * External dependencies
+ */
+import { render, screen, waitFor } from '@testing-library/react';
+
+/**
+ * WordPress dependencies
+ */
+import {
+ registerBlockType,
+ unregisterBlockType,
+ createBlock,
+} from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { useBlockPreview } from '../';
+
+jest.mock( '@wordpress/dom', () => {
+ const focus = jest.requireActual( '../../../../../dom/src' ).focus;
+
+ return {
+ focus: {
+ ...focus,
+ focusable: {
+ ...focus.focusable,
+ find( context ) {
+ // In JSDOM, all elements have zero'd widths and height.
+ // This is a metric for focusable's `isVisible`, so find
+ // and apply an arbitrary non-zero width.
+ Array.from( context.querySelectorAll( '*' ) ).forEach(
+ ( element ) => {
+ Object.defineProperties( element, {
+ offsetWidth: {
+ get: () => 1,
+ configurable: true,
+ },
+ } );
+ }
+ );
+
+ return focus.focusable.find( ...arguments );
+ },
+ },
+ },
+ };
+} );
+
+describe( 'useBlockPreview', () => {
+ beforeAll( () => {
+ registerBlockType( 'core/test-block', {
+ save: () => (
+
+ Test block save view
+
+
+ ),
+ edit: () => (
+
+ Test block edit view
+
+
+ ),
+ category: 'text',
+ title: 'test block',
+ } );
+ } );
+
+ afterAll( () => {
+ unregisterBlockType( 'core/test-block' );
+ } );
+
+ function BlockPreviewComponent( { blocks, className } ) {
+ const blockPreviewProps = useBlockPreview( {
+ blocks,
+ props: { className },
+ } );
+ return ;
+ }
+
+ it( 'will render a block preview with minimal nesting', async () => {
+ const blocks = [];
+ blocks.push( createBlock( 'core/test-block' ) );
+
+ const { container } = render(
+
+ );
+
+ // Test block and block contents are rendered.
+ const previewedBlock = screen.getByLabelText( 'Block: test block' );
+ const previewedBlockContents = screen.getByText(
+ 'Test block edit view'
+ );
+ expect( previewedBlockContents ).toBeInTheDocument();
+
+ // Test elements within block contents are disabled.
+ await waitFor( () => {
+ const button = screen.getByText( 'Button' );
+ expect( button.hasAttribute( 'disabled' ) ).toBe( true );
+ } );
+
+ // Ensure the block preview class names are merged with the component's class name.
+ expect( container.firstChild.className ).toBe(
+ 'test-container-classname block-editor-block-preview__live-content components-disabled'
+ );
+
+ // Ensure there is no nesting between the parent component and rendered blocks.
+ expect( container.firstChild.firstChild ).toBe( previewedBlock );
+ } );
+} );
diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js
index dcccb216217e30..d725e185ca4055 100644
--- a/packages/block-editor/src/components/index.js
+++ b/packages/block-editor/src/components/index.js
@@ -103,7 +103,10 @@ export { default as BlockList } from './block-list';
export { useBlockProps } from './block-list/use-block-props';
export { LayoutStyle as __experimentalLayoutStyle } from './block-list/layout';
export { default as BlockMover } from './block-mover';
-export { default as BlockPreview } from './block-preview';
+export {
+ default as BlockPreview,
+ useBlockPreview as __experimentalUseBlockPreview,
+} from './block-preview';
export {
default as BlockSelectionClearer,
useBlockSelectionClearer as __unstableUseBlockSelectionClearer,
diff --git a/packages/block-library/src/post-template/edit.js b/packages/block-library/src/post-template/edit.js
index 75c7046eba142c..59a3e306dd247d 100644
--- a/packages/block-library/src/post-template/edit.js
+++ b/packages/block-library/src/post-template/edit.js
@@ -6,12 +6,12 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { useState, useMemo } from '@wordpress/element';
+import { memo, useMemo, useState } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import {
BlockContextProvider,
- BlockPreview,
+ __experimentalUseBlockPreview as useBlockPreview,
useBlockProps,
useInnerBlocksProps,
store as blockEditorStore,
@@ -30,6 +30,39 @@ function PostTemplateInnerBlocks() {
return ;
}
+function PostTemplateBlockPreview( {
+ blocks,
+ blockContextId,
+ isHidden,
+ setActiveBlockContextId,
+} ) {
+ const blockPreviewProps = useBlockPreview( {
+ blocks,
+ } );
+
+ const handleOnClick = () => {
+ setActiveBlockContextId( blockContextId );
+ };
+
+ const style = {
+ display: isHidden ? 'none' : undefined,
+ };
+
+ return (
+
+ );
+}
+
+const MemoizedPostTemplateBlockPreview = memo( PostTemplateBlockPreview );
+
export default function PostTemplateEdit( {
clientId,
context: {
@@ -53,7 +86,7 @@ export default function PostTemplateEdit( {
},
} ) {
const [ { page } ] = queryContext;
- const [ activeBlockContext, setActiveBlockContext ] = useState();
+ const [ activeBlockContextId, setActiveBlockContextId ] = useState();
const { posts, blocks } = useSelect(
( select ) => {
@@ -115,7 +148,6 @@ export default function PostTemplateEdit( {
templateSlug,
]
);
-
const blockContexts = useMemo(
() =>
posts?.map( ( post ) => ( {
@@ -144,6 +176,10 @@ export default function PostTemplateEdit( {
return
{ __( 'No results found.' ) }
;
}
+ // To avoid flicker when switching active block contexts, a preview is rendered
+ // for each block context, but the preview for the active block context is hidden.
+ // This ensures that when it is displayed again, the cached rendering of the
+ // block preview is used, instead of having to re-render the preview from scratch.
return (