Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query Loop: Add useBlockPreview, fix Query Loop wide alignment in the editor #36431

Merged
merged 12 commits into from
Dec 15, 2021
Merged
60 changes: 60 additions & 0 deletions packages/block-editor/src/components/block-preview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
Expand Down Expand Up @@ -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 = (
<BlockEditorProvider value={ renderedBlocks } settings={ settings }>
<BlockListItems
renderAppender={ false }
__experimentalLayout={ __experimentalLayout }
/>
</BlockEditorProvider>
);

return {
...props,
ref,
className: classnames(
props.className,
'block-editor-block-preview__live-content',
'components-disabled'
),
children: blocks?.length ? children : null,
};
}
23 changes: 23 additions & 0 deletions packages/block-editor/src/components/block-preview/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
114 changes: 114 additions & 0 deletions packages/block-editor/src/components/block-preview/test/index.js
Original file line number Diff line number Diff line change
@@ -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: () => (
<div>
Test block save view
<button>Button</button>
</div>
),
edit: () => (
<div>
Test block edit view
<button>Button</button>
</div>
),
category: 'text',
title: 'test block',
} );
} );

afterAll( () => {
unregisterBlockType( 'core/test-block' );
} );

function BlockPreviewComponent( { blocks, className } ) {
const blockPreviewProps = useBlockPreview( {
blocks,
props: { className },
} );
return <div { ...blockPreviewProps } />;
}

it( 'will render a block preview with minimal nesting', async () => {
const blocks = [];
blocks.push( createBlock( 'core/test-block' ) );

const { container } = render(
<BlockPreviewComponent
className="test-container-classname"
blocks={ blocks }
/>
);

// 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 );
} );
} );
5 changes: 4 additions & 1 deletion packages/block-editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,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,
Expand Down
71 changes: 54 additions & 17 deletions packages/block-library/src/post-template/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -30,6 +30,39 @@ function PostTemplateInnerBlocks() {
return <li { ...innerBlocksProps } />;
}

function PostTemplateBlockPreview( {
blocks,
blockContextId,
isHidden,
setActiveBlockContextId,
} ) {
const blockPreviewProps = useBlockPreview( {
blocks,
} );

const handleOnClick = () => {
setActiveBlockContextId( blockContextId );
};

const style = {
display: isHidden ? 'none' : undefined,
};

return (
<li
{ ...blockPreviewProps }
tabIndex={ 0 }
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role="button"
onClick={ handleOnClick }
onKeyPress={ handleOnClick }
style={ style }
/>
);
}

const MemoizedPostTemplateBlockPreview = memo( PostTemplateBlockPreview );

export default function PostTemplateEdit( {
clientId,
context: {
Expand All @@ -53,7 +86,7 @@ export default function PostTemplateEdit( {
},
} ) {
const [ { page } ] = queryContext;
const [ activeBlockContext, setActiveBlockContext ] = useState();
const [ activeBlockContextId, setActiveBlockContextId ] = useState();

const { posts, blocks } = useSelect(
( select ) => {
Expand Down Expand Up @@ -115,7 +148,6 @@ export default function PostTemplateEdit( {
templateSlug,
]
);

const blockContexts = useMemo(
() =>
posts?.map( ( post ) => ( {
Expand Down Expand Up @@ -144,6 +176,10 @@ export default function PostTemplateEdit( {
return <p { ...blockProps }> { __( 'No results found.' ) }</p>;
}

// 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 (
<ul { ...blockProps }>
{ blockContexts &&
Expand All @@ -152,20 +188,21 @@ export default function PostTemplateEdit( {
key={ blockContext.postId }
value={ blockContext }
>
{ blockContext ===
( activeBlockContext || blockContexts[ 0 ] ) ? (
{ blockContext.postId ===
( activeBlockContextId ||
blockContexts[ 0 ]?.postId ) ? (
<PostTemplateInnerBlocks />
) : (
<li>
<BlockPreview
blocks={ blocks }
__experimentalLive
__experimentalOnClick={ () =>
setActiveBlockContext( blockContext )
}
/>
</li>
) }
) : null }
<MemoizedPostTemplateBlockPreview
blocks={ blocks }
blockContextId={ blockContext.postId }
setActiveBlockContextId={ setActiveBlockContextId }
isHidden={
blockContext.postId ===
( activeBlockContextId ||
blockContexts[ 0 ]?.postId )
}
/>
</BlockContextProvider>
) ) }
</ul>
Expand Down
Loading