diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js
index 60a34f6ea55fb0..f6fa73a053ac58 100644
--- a/packages/block-editor/src/hooks/index.js
+++ b/packages/block-editor/src/hooks/index.js
@@ -20,6 +20,7 @@ import './metadata';
import './metadata-name';
export { useCustomSides } from './dimensions';
+export { useLayoutClasses, useLayoutStyles } from './layout';
export { getBorderClassesAndStyles, useBorderProps } from './use-border-props';
export { getColorClassesAndStyles, useColorProps } from './use-color-props';
export { getSpacingClassesAndStyles } from './use-spacing-props';
diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js
index ac24fa6f4e232a..73d655c57c2acb 100644
--- a/packages/block-editor/src/hooks/layout.js
+++ b/packages/block-editor/src/hooks/layout.js
@@ -37,58 +37,101 @@ import { getLayoutType, getLayoutTypes } from '../layouts';
const layoutBlockSupportKey = '__experimentalLayout';
/**
- * Generates the utility classnames for the given blocks layout attributes.
- * This method was primarily added to reintroduce classnames that were removed
- * in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719), rather
- * than providing an extensive list of all possible layout classes. The plan is to
- * have the style engine generate a more extensive list of utility classnames which
- * will then replace this method.
+ * Generates the utility classnames for the given block's layout attributes.
*
- * @param { Object } layout Layout object.
- * @param { Object } layoutDefinitions An object containing layout definitions, stored in theme.json.
+ * @param { Object } block Block object.
*
* @return { Array } Array of CSS classname strings.
*/
-function useLayoutClasses( layout, layoutDefinitions ) {
+export function useLayoutClasses( block = {} ) {
const rootPaddingAlignment = useSelect( ( select ) => {
const { getSettings } = select( blockEditorStore );
return getSettings().__experimentalFeatures
?.useRootPaddingAwareAlignments;
}, [] );
+ const globalLayoutSettings = useSetting( 'layout' ) || {};
+
+ const { attributes = {}, name } = block;
+ const { layout } = attributes;
+
+ const { default: defaultBlockLayout } =
+ getBlockSupport( name, layoutBlockSupportKey ) || {};
+ const usedLayout =
+ layout?.inherit || layout?.contentSize || layout?.wideSize
+ ? { ...layout, type: 'constrained' }
+ : layout || defaultBlockLayout || {};
+
const layoutClassnames = [];
- if ( layoutDefinitions?.[ layout?.type || 'default' ]?.className ) {
+ if (
+ globalLayoutSettings?.definitions?.[ usedLayout?.type || 'default' ]
+ ?.className
+ ) {
layoutClassnames.push(
- layoutDefinitions?.[ layout?.type || 'default' ]?.className
+ globalLayoutSettings?.definitions?.[ usedLayout?.type || 'default' ]
+ ?.className
);
}
if (
- ( layout?.inherit ||
- layout?.contentSize ||
- layout?.type === 'constrained' ) &&
+ ( usedLayout?.inherit ||
+ usedLayout?.contentSize ||
+ usedLayout?.type === 'constrained' ) &&
rootPaddingAlignment
) {
layoutClassnames.push( 'has-global-padding' );
}
- if ( layout?.orientation ) {
- layoutClassnames.push( `is-${ kebabCase( layout.orientation ) }` );
+ if ( usedLayout?.orientation ) {
+ layoutClassnames.push( `is-${ kebabCase( usedLayout.orientation ) }` );
}
- if ( layout?.justifyContent ) {
+ if ( usedLayout?.justifyContent ) {
layoutClassnames.push(
- `is-content-justification-${ kebabCase( layout.justifyContent ) }`
+ `is-content-justification-${ kebabCase(
+ usedLayout.justifyContent
+ ) }`
);
}
- if ( layout?.flexWrap && layout.flexWrap === 'nowrap' ) {
+ if ( usedLayout?.flexWrap && usedLayout.flexWrap === 'nowrap' ) {
layoutClassnames.push( 'is-nowrap' );
}
return layoutClassnames;
}
+/**
+ * Generates a CSS rule with the given block's layout styles.
+ *
+ * @param { Object } block Block object.
+ * @param { string } selector A selector to use in generating the CSS rule.
+ *
+ * @return { string } CSS rule.
+ */
+export function useLayoutStyles( block = {}, selector ) {
+ const { attributes = {}, name } = block;
+ const { layout = {}, style = {} } = attributes;
+ // Update type for blocks using legacy layouts.
+ const usedLayout =
+ layout?.inherit || layout?.contentSize || layout?.wideSize
+ ? { ...layout, type: 'constrained' }
+ : layout || {};
+ const fullLayoutType = getLayoutType( usedLayout?.type || 'default' );
+ const globalLayoutSettings = useSetting( 'layout' ) || {};
+ const blockGapSupport = useSetting( 'spacing.blockGap' );
+ const hasBlockGapSupport = blockGapSupport !== null;
+ const css = fullLayoutType?.getLayoutStyle?.( {
+ blockName: name,
+ selector,
+ layout,
+ layoutDefinitions: globalLayoutSettings?.definitions,
+ style,
+ hasBlockGapSupport,
+ } );
+ return css;
+}
+
function LayoutPanel( { setAttributes, attributes, name: blockName } ) {
const { layout } = attributes;
const defaultThemeLayout = useSetting( 'layout' );
@@ -299,7 +342,7 @@ export const withInspectorControls = createHigherOrderComponent(
*/
export const withLayoutStyles = createHigherOrderComponent(
( BlockListBlock ) => ( props ) => {
- const { name, attributes } = props;
+ const { name, attributes, block } = props;
const hasLayoutBlockSupport = hasBlockSupport(
name,
layoutBlockSupportKey
@@ -321,7 +364,7 @@ export const withLayoutStyles = createHigherOrderComponent(
? { ...layout, type: 'constrained' }
: layout || defaultBlockLayout || {};
const layoutClasses = hasLayoutBlockSupport
- ? useLayoutClasses( usedLayout, defaultThemeLayout?.definitions )
+ ? useLayoutClasses( block )
: null;
const selector = `.${ getBlockDefaultClassName(
name
diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js
index ec0f20a8f9c892..1c81c910b21e12 100644
--- a/packages/block-editor/src/index.js
+++ b/packages/block-editor/src/index.js
@@ -12,6 +12,8 @@ export {
getSpacingClassesAndStyles as __experimentalGetSpacingClassesAndStyles,
getGapCSSValue as __experimentalGetGapCSSValue,
useCachedTruthy,
+ useLayoutClasses as __experimentaluseLayoutClasses,
+ useLayoutStyles as __experimentaluseLayoutStyles,
} from './hooks';
export * from './components';
export * from './elements';
diff --git a/packages/block-library/src/post-content/edit.js b/packages/block-library/src/post-content/edit.js
index 233399a63f8d55..1949553b2540f6 100644
--- a/packages/block-library/src/post-content/edit.js
+++ b/packages/block-library/src/post-content/edit.js
@@ -46,7 +46,9 @@ function EditableContent( { layout, context = {} } ) {
return getSettings()?.supportsLayout;
}, [] );
const defaultLayout = useSetting( 'layout' ) || {};
- const usedLayout = !! layout && layout.inherit ? defaultLayout : layout;
+ const usedLayout = ! layout?.type
+ ? { ...defaultLayout, ...layout, type: 'default' }
+ : { ...defaultLayout, ...layout };
const [ blocks, onInput, onChange ] = useEntityBlockEditor(
'postType',
postType,
diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js
index 8f6d54f064c38b..6ba8db74122d6c 100644
--- a/packages/edit-post/src/components/visual-editor/index.js
+++ b/packages/edit-post/src/components/visual-editor/index.js
@@ -28,6 +28,8 @@ import {
__unstableUseMouseMoveTypingReset as useMouseMoveTypingReset,
__unstableIframe as Iframe,
__experimentalRecursionProvider as RecursionProvider,
+ __experimentaluseLayoutClasses as useLayoutClasses,
+ __experimentaluseLayoutStyles as useLayoutStyles,
} from '@wordpress/block-editor';
import { useEffect, useRef, useMemo } from '@wordpress/element';
import { Button, __unstableMotion as motion } from '@wordpress/components';
@@ -35,6 +37,7 @@ import { useSelect, useDispatch } from '@wordpress/data';
import { useMergeRefs } from '@wordpress/compose';
import { arrowLeft } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
+import { parse } from '@wordpress/blocks';
/**
* Internal dependencies
@@ -82,11 +85,37 @@ function MaybeIframe( {
);
}
+/**
+ * Given an array of nested blocks, find the first Post Content
+ * block inside it, recursing through any nesting levels.
+ *
+ * @param {Array} blocks A list of blocks.
+ *
+ * @return {Object} The Post Content block.
+ */
+function findPostContent( blocks ) {
+ for ( let i = 0; i < blocks.length; i++ ) {
+ if ( blocks[ i ].name === 'core/post-content' ) {
+ return blocks[ i ];
+ }
+ if ( blocks[ i ].innerBlocks.length ) {
+ const nestedPostContent = findPostContent(
+ blocks[ i ].innerBlocks
+ );
+
+ if ( nestedPostContent ) {
+ return nestedPostContent;
+ }
+ }
+ }
+}
+
export default function VisualEditor( { styles } ) {
const {
deviceType,
isWelcomeGuideVisible,
isTemplateMode,
+ editedPostTemplate = {},
wrapperBlockName,
wrapperUniqueId,
} = useSelect( ( select ) => {
@@ -94,8 +123,10 @@ export default function VisualEditor( { styles } ) {
isFeatureActive,
isEditingTemplate,
__experimentalGetPreviewDeviceType,
+ getEditedPostTemplate,
} = select( editPostStore );
- const { getCurrentPostId, getCurrentPostType } = select( editorStore );
+ const { getCurrentPostId, getCurrentPostType, getEditorSettings } =
+ select( editorStore );
const _isTemplateMode = isEditingTemplate();
let _wrapperBlockName;
@@ -105,10 +136,17 @@ export default function VisualEditor( { styles } ) {
_wrapperBlockName = 'core/post-content';
}
+ const supportsTemplateMode = getEditorSettings().supportsTemplateMode;
+
return {
deviceType: __experimentalGetPreviewDeviceType(),
isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ),
isTemplateMode: _isTemplateMode,
+ // Post template fetch returns a 404 on classic themes, which
+ // messes with e2e tests, so we check it's a block theme first.
+ editedPostTemplate: supportsTemplateMode
+ ? getEditedPostTemplate()
+ : {},
wrapperBlockName: _wrapperBlockName,
wrapperUniqueId: getCurrentPostId(),
};
@@ -122,7 +160,6 @@ export default function VisualEditor( { styles } ) {
themeHasDisabledLayoutStyles,
themeSupportsLayout,
assets,
- useRootPaddingAwareAlignments,
isFocusMode,
} = useSelect( ( select ) => {
const _settings = select( blockEditorStore ).getSettings();
@@ -130,8 +167,6 @@ export default function VisualEditor( { styles } ) {
themeHasDisabledLayoutStyles: _settings.disableLayoutStyles,
themeSupportsLayout: _settings.supportsLayout,
assets: _settings.__unstableResolvedAssets,
- useRootPaddingAwareAlignments:
- _settings.__experimentalFeatures?.useRootPaddingAwareAlignments,
isFocusMode: _settings.focusMode,
};
}, [] );
@@ -154,7 +189,7 @@ export default function VisualEditor( { styles } ) {
borderBottom: 0,
};
const resizedCanvasStyles = useResizeCanvas( deviceType, isTemplateMode );
- const defaultLayout = useSetting( 'layout' );
+ const globalLayoutSettings = useSetting( 'layout' );
const previewMode = 'is-' + deviceType.toLowerCase() + '-preview';
let animatedStyles = isTemplateMode
@@ -183,7 +218,9 @@ export default function VisualEditor( { styles } ) {
const blockSelectionClearerRef = useBlockSelectionClearer();
- const layout = useMemo( () => {
+ // fallbackLayout is used if there is no Post Content,
+ // and for Post Title.
+ const fallbackLayout = useMemo( () => {
if ( isTemplateMode ) {
return { type: 'default' };
}
@@ -191,17 +228,58 @@ export default function VisualEditor( { styles } ) {
if ( themeSupportsLayout ) {
// We need to ensure support for wide and full alignments,
// so we add the constrained type.
- return { ...defaultLayout, type: 'constrained' };
+ return { ...globalLayoutSettings, type: 'constrained' };
}
// Set default layout for classic themes so all alignments are supported.
return { type: 'default' };
- }, [ isTemplateMode, themeSupportsLayout, defaultLayout ] );
+ }, [ isTemplateMode, themeSupportsLayout, globalLayoutSettings ] );
+
+ const postContentBlock = useMemo( () => {
+ // When in template editing mode, we can access the blocks directly.
+ if ( editedPostTemplate?.blocks ) {
+ return findPostContent( editedPostTemplate?.blocks );
+ }
+ // If there are no blocks, we have to parse the content string.
+ // Best double-check it's a string otherwise the parse function gets unhappy.
+ const parseableContent =
+ typeof editedPostTemplate?.content === 'string'
+ ? editedPostTemplate?.content
+ : '';
+
+ return findPostContent( parse( parseableContent ) ) || {};
+ }, [ editedPostTemplate?.content, editedPostTemplate?.blocks ] );
+
+ const postContentLayoutClasses = useLayoutClasses( postContentBlock );
+
+ const blockListLayoutClass = classnames(
+ {
+ 'is-layout-flow': ! themeSupportsLayout,
+ },
+ themeSupportsLayout && postContentLayoutClasses
+ );
- const blockListLayoutClass = classnames( {
- 'is-layout-constrained': themeSupportsLayout,
- 'is-layout-flow': ! themeSupportsLayout,
- 'has-global-padding': useRootPaddingAwareAlignments,
- } );
+ const postContentLayoutStyles = useLayoutStyles(
+ postContentBlock,
+ '.block-editor-block-list__layout.is-root-container'
+ );
+
+ const layout = postContentBlock?.attributes?.layout || {};
+
+ // Update type for blocks using legacy layouts.
+ const postContentLayout =
+ layout &&
+ ( layout?.type === 'constrained' ||
+ layout?.inherit ||
+ layout?.contentSize ||
+ layout?.wideSize )
+ ? { ...globalLayoutSettings, ...layout, type: 'constrained' }
+ : { ...globalLayoutSettings, ...layout, type: 'default' };
+
+ // If there is a Post Content block we use its layout for the block list;
+ // if not, this must be a classic theme, in which case we use the fallback layout.
+ const blockListLayout = postContentBlock
+ ? postContentLayout
+ : fallbackLayout;
const titleRef = useRef();
useEffect( () => {
@@ -257,13 +335,24 @@ export default function VisualEditor( { styles } ) {
{ themeSupportsLayout &&
! themeHasDisabledLayoutStyles &&
! isTemplateMode && (
-