Skip to content

Commit

Permalink
Refactor block preview behaviour to a useBlockPreview hook for the po…
Browse files Browse the repository at this point in the history
…st content block
  • Loading branch information
andrewserong committed Nov 3, 2021
1 parent 04caf9b commit b06b374
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 66 deletions.
17 changes: 17 additions & 0 deletions packages/block-editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,23 @@ _Returns_

Undocumented declaration.

### useBlockPreview

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.

_Parameters_

- _props_ `Object`: Optional. Props to pass to the element. Must contain the ref if one is defined.
- _options_ `Object`: Preview options.
- _options.blocks_ `WPBlock[]`: Block objects.
- _options.\_\_experimentalLayout_ `Object`: Layout settings to be used in the preview.

### useBlockProps

This hook is used to lightly mark an element as a block element. The element
Expand Down
67 changes: 61 additions & 6 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,12 +21,12 @@ 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,
__experimentalPadding = 0,
viewportWidth = 1200,
__experimentalAsButton = true,
__experimentalLive = false,
__experimentalOnClick,
} ) {
Expand All @@ -41,11 +46,7 @@ export function BlockPreview( {
return (
<BlockEditorProvider value={ renderedBlocks } settings={ settings }>
{ __experimentalLive ? (
<LiveBlockPreview
__experimentalAsButton={ __experimentalAsButton }
onClick={ __experimentalOnClick }
themeSupportsLayout={ !! settings?.supportsLayout }
/>
<LiveBlockPreview onClick={ __experimentalOnClick } />
) : (
<AutoHeightBlockPreview
viewportWidth={ viewportWidth }
Expand All @@ -68,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} props Optional. Props to pass to the element. Must contain
* the ref if one is defined.
* @param {Object} options Preview options.
* @param {WPBlock[]} options.blocks Block objects.
* @param {Object} options.__experimentalLayout Layout settings to be used in the preview.
*
*/
export function useBlockPreview(
props = {},
{ blocks, __experimentalLayout }
) {
const originalSettings = useSelect(
( select ) => select( blockEditorStore ).getSettings(),
[]
);
const disabledRef = useDisabled();
const ref = useMergeRefs( [ props.ref, disabledRef ] );
const settings = useMemo( () => {
const _settings = { ...originalSettings };
_settings.__experimentalBlockPatterns = [];
return _settings;
}, [ 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,
};
}
40 changes: 6 additions & 34 deletions packages/block-editor/src/components/block-preview/live.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,19 @@ import { Disabled } from '@wordpress/components';
/**
* Internal dependencies
*/
import { BlockListItems } from '../block-list';
import BlockList from '../block-list';

const DEFAULT_CONTROLS = [ 'none', 'left', 'center', 'right', 'wide', 'full' ];
const WIDE_CONTROLS = [ 'wide', 'full' ];

const layout = {
type: 'default',
alignments: [ ...DEFAULT_CONTROLS, ...WIDE_CONTROLS ],
};

export default function LiveBlockPreview( {
onClick,
__experimentalAsButton = true,
themeSupportsLayout = false,
} ) {
let blockList;

if ( __experimentalAsButton ) {
blockList = (
<Disabled className="block-editor-block-preview__live-content">
<BlockListItems />
</Disabled>
);
} else {
const props = {};
if ( themeSupportsLayout ) {
props.__experimentalLayout = layout;
}
blockList = <BlockListItems { ...props } />;
}

return __experimentalAsButton ? (
export default function LiveBlockPreview( { onClick } ) {
return (
<div
tabIndex={ 0 }
role="button"
onClick={ onClick }
onKeyPress={ onClick }
>
{ blockList }
<Disabled>
<BlockList />
</Disabled>
</div>
) : (
<>{ blockList }</>
);
}
2 changes: 1 addition & 1 deletion packages/block-editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ 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 } from './block-preview';
export {
default as BlockSelectionClearer,
useBlockSelectionClearer as __unstableUseBlockSelectionClearer,
Expand Down
42 changes: 17 additions & 25 deletions packages/block-library/src/post-content/edit.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { parse } from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';
import {
BlockPreview,
useBlockPreview,
useBlockProps,
useInnerBlocksProps,
useSetting,
Expand All @@ -19,49 +14,45 @@ import {
Warning,
} from '@wordpress/block-editor';
import { useEntityProp, useEntityBlockEditor } from '@wordpress/core-data';
import { useMergeRefs } from '@wordpress/compose';
import { useMemo } from '@wordpress/element';

/**
* Internal dependencies
*/
import { useCanEditEntity } from '../utils/hooks';
import { useDisabled } from './use-disabled';

function ReadOnlyContent( { userCanEdit, postType, postId } ) {
function ReadOnlyContent( { layout, userCanEdit, postType, postId } ) {
const [ , , content ] = useEntityProp(
'postType',
postType,
'content',
postId
);
const { ref, ...blockProps } = useBlockProps( {
className: classnames(
'components-disabled',
'block-editor-block-preview__live-content'
),
} );
const node = useDisabled();
const blockProps = useBlockProps();

const mergedRefs = useMergeRefs( [ ref, node ] );
const themeSupportsLayout = useSelect( ( select ) => {
const { getSettings } = select( blockEditorStore );
return getSettings()?.supportsLayout;
}, [] );
const defaultLayout = useSetting( 'layout' ) || {};
const usedLayout = !! layout && layout.inherit ? defaultLayout : layout;

const rawContent = content?.raw;
const blocks = useMemo( () => {
return rawContent ? parse( rawContent ) : [];
}, [ rawContent ] );

const blockPreviewProps = useBlockPreview( blockProps, {
blocks,
__experimentalLayout: themeSupportsLayout ? usedLayout : undefined,
} );

return content?.protected && ! userCanEdit ? (
<div { ...blockProps }>
<Warning>{ __( 'This content is password protected.' ) }</Warning>
</div>
) : (
<div { ...blockProps } ref={ mergedRefs }>
<BlockPreview
blocks={ blocks }
__experimentalAsButton={ false }
__experimentalLive={ true }
/>
</div>
<div { ...blockPreviewProps }></div>
);
}

Expand Down Expand Up @@ -92,7 +83,7 @@ function EditableContent( { layout, context = {} } ) {
}

function Content( props ) {
const { context: { queryId, postType, postId } = {} } = props;
const { context: { queryId, postType, postId } = {}, layout } = props;
const isDescendentOfQueryLoop = !! queryId;
const userCanEdit = useCanEditEntity( 'postType', postType, postId );
const isEditable = userCanEdit && ! isDescendentOfQueryLoop;
Expand All @@ -101,6 +92,7 @@ function Content( props ) {
<EditableContent { ...props } />
) : (
<ReadOnlyContent
layout={ layout }
userCanEdit={ userCanEdit }
postType={ postType }
postId={ postId }
Expand Down
92 changes: 92 additions & 0 deletions packages/compose/src/hooks/use-disabled/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* External dependencies
*/
import { includes, debounce } from 'lodash';

/**
* WordPress dependencies
*/
import { useCallback, useLayoutEffect, useRef } from '@wordpress/element';
import { focus } from '@wordpress/dom';

/**
* Names of control nodes which qualify for disabled behavior.
*
* See WHATWG HTML Standard: 4.10.18.5: "Enabling and disabling form controls: the disabled attribute".
*
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#enabling-and-disabling-form-controls:-the-disabled-attribute
*
* @type {string[]}
*/
const DISABLED_ELIGIBLE_NODE_NAMES = [
'BUTTON',
'FIELDSET',
'INPUT',
'OPTGROUP',
'OPTION',
'SELECT',
'TEXTAREA',
];

export default function useDisabled() {
/** @type {import('react').RefObject<HTMLDivElement>} */
const node = useRef( null );

const disable = () => {
if ( ! node.current ) {
return;
}

focus.focusable.find( node.current ).forEach( ( focusable ) => {
if (
includes( DISABLED_ELIGIBLE_NODE_NAMES, focusable.nodeName )
) {
focusable.setAttribute( 'disabled', '' );
}

if ( focusable.nodeName === 'A' ) {
focusable.setAttribute( 'tabindex', '-1' );
}

const tabIndex = focusable.getAttribute( 'tabindex' );
if ( tabIndex !== null && tabIndex !== '-1' ) {
focusable.removeAttribute( 'tabindex' );
}

if ( focusable.hasAttribute( 'contenteditable' ) ) {
focusable.setAttribute( 'contenteditable', 'false' );
}
} );
};

// Debounce re-disable since disabling process itself will incur
// additional mutations which should be ignored.
const debouncedDisable = useCallback(
debounce( disable, undefined, { leading: true } ),
[]
);

useLayoutEffect( () => {
disable();

/** @type {MutationObserver | undefined} */
let observer;
if ( node.current ) {
observer = new window.MutationObserver( debouncedDisable );
observer.observe( node.current, {
childList: true,
attributes: true,
subtree: true,
} );
}

return () => {
if ( observer ) {
observer.disconnect();
}
debouncedDisable.cancel();
};
}, [] );

return node;
}
1 change: 1 addition & 0 deletions packages/compose/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { default as useConstrainedTabbing } from './hooks/use-constrained-tabbin
export { default as useCopyOnClick } from './hooks/use-copy-on-click';
export { default as useCopyToClipboard } from './hooks/use-copy-to-clipboard';
export { default as __experimentalUseDialog } from './hooks/use-dialog';
export { default as __experimentalUseDisabled } from './hooks/use-disabled';
export { default as __experimentalUseDragging } from './hooks/use-dragging';
export { default as useFocusOnMount } from './hooks/use-focus-on-mount';
export { default as __experimentalUseFocusOutside } from './hooks/use-focus-outside';
Expand Down

0 comments on commit b06b374

Please sign in to comment.