diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 5c8f30a5802ce..1736d857763df 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -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_ + +- _options_ `Object`: Preview options. +- _options.blocks_ `WPBlock[]`: Block objects. +- _options.props_ `Object`: Optional. Props to pass to the element. Must contain the ref if one is defined. +- _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 diff --git a/packages/block-editor/src/components/block-preview/index.js b/packages/block-editor/src/components/block-preview/index.js index cea2e8ee9b62c..e8ea15edf9271 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,58 @@ 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( () => { + const _settings = { ...originalSettings }; + _settings.__experimentalBlockPatterns = []; + return _settings; + }, [ 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 71ca31e9bc221..23661a1232f67 100644 --- a/packages/block-editor/src/components/block-preview/style.scss +++ b/packages/block-editor/src/components/block-preview/style.scss @@ -36,3 +36,26 @@ } } } + +.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/index.js b/packages/block-editor/src/components/index.js index dcccb216217e3..a2e4b5c3a02db 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -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, diff --git a/packages/block-library/src/post-content/edit.js b/packages/block-library/src/post-content/edit.js index 4a0c277a1f5cf..fa4eb89973979 100644 --- a/packages/block-library/src/post-content/edit.js +++ b/packages/block-library/src/post-content/edit.js @@ -2,9 +2,10 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { parse } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; -import { RawHTML } from '@wordpress/element'; import { + useBlockPreview, useBlockProps, useInnerBlocksProps, useSetting, @@ -13,13 +14,19 @@ import { Warning, } from '@wordpress/block-editor'; import { useEntityProp, useEntityBlockEditor } from '@wordpress/core-data'; +import { useMemo } from '@wordpress/element'; /** * Internal dependencies */ import { useCanEditEntity } from '../utils/hooks'; -function ReadOnlyContent( { userCanEdit, postType, postId } ) { +function ReadOnlyContent( { + __experimentalLayout, + postId, + postType, + userCanEdit, +} ) { const [ , , content ] = useEntityProp( 'postType', postType, @@ -27,25 +34,29 @@ function ReadOnlyContent( { userCanEdit, postType, postId } ) { postId ); const blockProps = useBlockProps(); + + const rawContent = content?.raw; + const blocks = useMemo( () => { + return rawContent ? parse( rawContent ) : []; + }, [ rawContent ] ); + + const blockPreviewProps = useBlockPreview( { + blocks, + props: blockProps, + __experimentalLayout, + } ); + return content?.protected && ! userCanEdit ? (
{ __( 'This content is password protected.' ) }
) : ( -
- { content?.rendered } -
+
); } -function EditableContent( { layout, context = {} } ) { +function EditableContent( { __experimentalLayout, context = {} } ) { const { postType, postId } = context; - const themeSupportsLayout = useSelect( ( select ) => { - const { getSettings } = select( blockEditorStore ); - return getSettings()?.supportsLayout; - }, [] ); - const defaultLayout = useSetting( 'layout' ) || {}; - const usedLayout = !! layout && layout.inherit ? defaultLayout : layout; const [ blocks, onInput, onChange ] = useEntityBlockEditor( 'postType', postType, @@ -58,22 +69,34 @@ function EditableContent( { layout, context = {} } ) { value: blocks, onInput, onChange, - __experimentalLayout: themeSupportsLayout ? usedLayout : undefined, + __experimentalLayout, } ); return
; } 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; + const themeSupportsLayout = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + return getSettings()?.supportsLayout; + }, [] ); + const defaultLayout = useSetting( 'layout' ) || {}; + const usedLayout = !! layout && layout.inherit ? defaultLayout : layout; + const __experimentalLayout = themeSupportsLayout ? usedLayout : undefined; + return isEditable ? ( - + ) : ( } */ + 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; +} diff --git a/packages/compose/src/index.js b/packages/compose/src/index.js index 2ce3a2ab33f6f..2bfc7c7f5f848 100644 --- a/packages/compose/src/index.js +++ b/packages/compose/src/index.js @@ -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';