Commit e585992258e for woocommerce

commit e585992258e584714c06eb964b70b775b22ab6a3
Author: Yuliyan Slavchev <yuliyan.slavchev@gmail.com>
Date:   Mon Jul 13 14:13:20 2026 +0300

    Email Editor: Make full-width blocks render properly as full width in the editor (#66235)

    * Email Editor: Make full-width blocks render properly as full width in the editor

    * Avoid remounting full-width blocks on alignment change

diff --git a/packages/js/email-editor/changelog/fix-email-editor-full-width-blocks b/packages/js/email-editor/changelog/fix-email-editor-full-width-blocks
new file mode 100644
index 00000000000..aa888ac0437
--- /dev/null
+++ b/packages/js/email-editor/changelog/fix-email-editor-full-width-blocks
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show full-width blocks as full width in the email editor canvas to match the rendered email.
diff --git a/packages/js/email-editor/src/blocks/core/full-width.tsx b/packages/js/email-editor/src/blocks/core/full-width.tsx
new file mode 100644
index 00000000000..14a7f68731d
--- /dev/null
+++ b/packages/js/email-editor/src/blocks/core/full-width.tsx
@@ -0,0 +1,183 @@
+/**
+ * External dependencies
+ */
+import clsx from 'clsx';
+import { createHigherOrderComponent } from '@wordpress/compose';
+import { useSelect } from '@wordpress/data';
+import { store as blockEditorStore } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import { addFilterForEmail } from '../../config-tools/filters';
+import { unwrapCompressedPresetStyleVariable } from '../../style-variables';
+
+type SpacingPadding =
+	| string
+	| { top?: string; right?: string; bottom?: string; left?: string };
+
+// Columns handle their own width, so don't let full-width blocks inside a
+// column break out (the renderer doesn't either).
+const COLUMN_BLOCKS = [ 'core/column', 'core/columns' ];
+
+// User blocks live inside these wrappers, which add no inset of their own. We
+// skip them so a full-width block breaks out of the padded template group above.
+const PASSTHROUGH_BLOCKS = [ 'core/post-content', 'woocommerce/email-content' ];
+
+/**
+ * Checks whether the value is zero (0, 0px, 0em, 0%, ...)
+ */
+function isZeroValue( value?: string ): boolean {
+	if ( value === undefined || value === null ) {
+		return false;
+	}
+	return parseFloat( String( value ) ) === 0;
+}
+
+/**
+ * Reads a block's left/right padding as CSS values (preset vars resolved).
+ * Returns null for a side when it's missing or zero.
+ */
+function getHorizontalPadding( padding?: SpacingPadding ): {
+	left: string | null;
+	right: string | null;
+} {
+	const toCss = ( value?: string ): string | null => {
+		if ( value === undefined || value === null || isZeroValue( value ) ) {
+			return null;
+		}
+		return unwrapCompressedPresetStyleVariable( String( value ) );
+	};
+
+	if ( ! padding ) {
+		return { left: null, right: null };
+	}
+	if ( typeof padding === 'string' ) {
+		const value = toCss( padding );
+		return { left: value, right: value };
+	}
+	return { left: toCss( padding.left ), right: toCss( padding.right ) };
+}
+
+type BreakoutInfo = {
+	left: string | null;
+	right: string | null;
+};
+
+/**
+ * Finds the padding a full-width block should break out of.
+ *
+ * Inside the post content (user blocks), a block breaks out of its direct
+ * parent group's padding only, matching the core editor - if the direct parent
+ * has no padding there's nothing to break out of.
+ *
+ * Returns false when the block sits in a column, since columns keep their width.
+ *
+ * Skips the ancestor lookup when `enabled` is false (block isn't full width) so
+ * we don't traverse the tree for every block in the editor.
+ */
+function useBreakoutPadding(
+	clientId: string,
+	enabled: boolean
+): BreakoutInfo | false {
+	return useSelect(
+		( select ) => {
+			if ( ! enabled ) {
+				return { left: null, right: null };
+			}
+			const { getBlockParents, getBlockName, getBlockAttributes } =
+				select( blockEditorStore );
+			// getBlockParents lists ancestors root-first, so the last entry is
+			// the direct parent. Walk from there up to the root.
+			const parents = getBlockParents( clientId ) as string[];
+			let inTemplate = false;
+
+			for ( let i = parents.length - 1; i >= 0; i-- ) {
+				const parentId = parents[ i ];
+				const name = getBlockName( parentId ) as string;
+
+				if ( COLUMN_BLOCKS.includes( name ) ) {
+					return false;
+				}
+				// Post content wrappers separate user blocks from the template.
+				// Once we pass one, we're climbing the template layout.
+				if ( PASSTHROUGH_BLOCKS.includes( name ) ) {
+					inTemplate = true;
+					continue;
+				}
+
+				const padding = getHorizontalPadding(
+					getBlockAttributes( parentId )?.style?.spacing
+						?.padding as SpacingPadding
+				);
+				if ( padding.left || padding.right ) {
+					return padding;
+				}
+				// Parent has no padding. In user content, stop here - there's
+				// nothing to break out of. In the template, keep going up to
+				// find the padded group.
+				if ( ! inTemplate ) {
+					return { left: null, right: null };
+				}
+			}
+
+			return { left: null, right: null };
+		},
+		[ clientId, enabled ]
+	);
+}
+
+function BlockWithFullWidth( { block: BlockListBlock, props } ) {
+	const isFullWidth = props.attributes?.align === 'full';
+	const breakout = useBreakoutPadding( props.clientId, isFullWidth );
+
+	// Not full width, or inside a column: render the block untouched.
+	if ( ! isFullWidth || breakout === false ) {
+		return <BlockListBlock { ...props } />;
+	}
+
+	const style: Record< string, string > = {
+		...( props.wrapperProps?.style || {} ),
+	};
+	if ( breakout.left ) {
+		style.marginLeft = `calc(-1 * ${ breakout.left })`;
+	}
+	if ( breakout.right ) {
+		style.marginRight = `calc(-1 * ${ breakout.right })`;
+	}
+
+	return (
+		<BlockListBlock
+			{ ...props }
+			className={ clsx( props.className, 'is-email-full-width' ) }
+			wrapperProps={ { ...props.wrapperProps, style } }
+		/>
+	);
+}
+
+/**
+ * Marks full-width aligned blocks so they go edge-to-edge in the editor canvas,
+ * the same way they show up in the sent email. Always renders the same component
+ * so toggling alignment doesn't remount the block.
+ */
+const withFullWidthClassName = createHigherOrderComponent(
+	( BlockListBlock ) =>
+		function maybeAddFullWidthClassName( props ) {
+			return (
+				<BlockWithFullWidth block={ BlockListBlock } props={ props } />
+			);
+		},
+	'withFullWidthClassName'
+);
+
+/**
+ * Makes full-width blocks span the full width in the editor, the same way they
+ * show up in the sent email.
+ */
+export function enableFullWidthBlocks(): void {
+	addFilterForEmail(
+		'editor.BlockListBlock',
+		'woocommerce-email-editor/full-width-blocks',
+		withFullWidthClassName
+	);
+}
diff --git a/packages/js/email-editor/src/blocks/index.ts b/packages/js/email-editor/src/blocks/index.ts
index 7a1318b3939..3f5fb00f3ee 100644
--- a/packages/js/email-editor/src/blocks/index.ts
+++ b/packages/js/email-editor/src/blocks/index.ts
@@ -28,6 +28,7 @@ import { enhanceQuoteBlock } from './core/quote';
 import { filterSetUrlAttribute } from './core/block-edit';
 import { enhanceSocialLinksBlock } from './core/social-links';
 import { enhanceSiteLogoBlock } from './core/site-logo';
+import { enableFullWidthBlocks } from './core/full-width';
 import { enableProductImageAlignment } from './woocommerce/product-image';

 export { getAllowedBlockNames } from './utils';
@@ -53,6 +54,7 @@ export function initBlocks() {
 	alterSupportConfiguration();
 	enhanceSocialLinksBlock();
 	enhanceSiteLogoBlock();
+	enableFullWidthBlocks();
 	enableProductImageAlignment();
 	removeBlockStylesFromAllBlocks();
 }