Commit ea1631610ad for woocommerce
commit ea1631610ad5cbf695f95caad88621ecdc54da76
Author: Allison Levine <1689238+allilevine@users.noreply.github.com>
Date: Fri Mar 20 13:01:02 2026 -0400
Email Editor: Add rendering support for full-width blocks (#63752)
* Email Editor: Add rendering support for full-width blocks breaking out of template group padding
Extends the per-block root padding distribution pattern (from #63682) to
also handle template group padding. When a template group wrapping
post-content has its own horizontal padding, that padding is distributed
per-block as container-padding alongside root-padding. Alignfull blocks
skip both padding types and span the full contentSize.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix PHPStan error: loosen type annotation on remove_horizontal_padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add changelog entry for email editor full-width block rendering support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address code review: resolve preset variables in container padding, add defensive guard, improve tests
- Fix: resolve preset variable references (var:preset|spacing|20) in
container padding values before they reach sum_padding_values() and
parse_number_from_string_with_pixels(), which would otherwise parse
them as 0.
- Add defensive guard in remove_horizontal_padding() for malformed input.
- Use Styles_Helper::extend_block_styles() for CSS rebuild (PHPStan
level 9 safe).
- Add asymmetric padding test (left != right).
- Assert width in testItDistributesBothPaddingsInSecondPass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix double container padding: root-level group subsumes root padding, accumulate nested containers
Root-level groups with own padding (e.g. WooCommerce template's 20px group)
represent the same visual inset as the theme's root padding. Previously both
were applied additively (root 20px + container 20px = 40px). Now the root
group's padding subsumes root padding, and nested container paddings are
accumulated (root group 20px + content group 10px = 30px total).
Changes:
- Spacing_Preprocessor: pass empty root_padding to children when root-level
container absorbs it
- Content_Renderer: accumulate padding from all suppressed containers in
ancestor chain instead of returning only the first
- Content_Renderer: detect root-level suppress to remove root padding in
second pass
- Content_Renderer: resolve preset references in sum_padding_values
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert "Fix double container padding: root-level group subsumes root padding, accumulate nested containers"
This reverts commit 2680cd337f5cd974d1f83abf61b528a6a061864d.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/packages/php/email-editor/changelog/add-email-editor-fullwidth-block-rendering-support b/packages/php/email-editor/changelog/add-email-editor-fullwidth-block-rendering-support
new file mode 100644
index 00000000000..15e36292706
--- /dev/null
+++ b/packages/php/email-editor/changelog/add-email-editor-fullwidth-block-rendering-support
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add rendering support for full-width blocks to break out of template group padding in emails, distributing container padding per-block so alignfull blocks can span the full contentSize.
diff --git a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-blocks-width-preprocessor.php b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-blocks-width-preprocessor.php
index 6db7325d72e..f0ba2130574 100644
--- a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-blocks-width-preprocessor.php
+++ b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-blocks-width-preprocessor.php
@@ -57,17 +57,24 @@ class Blocks_Width_Preprocessor implements Preprocessor {
$layout_width -= $this->parse_number_from_string_with_pixels( $styles['spacing']['padding']['right'] ?? '0px' );
}
- // Subtract root padding for blocks that will receive it as CSS padding
- // from Content_Renderer. This ensures block widths fit inside the
- // root padding wrapper without overflow.
+ // Subtract root padding and container padding for blocks that will
+ // receive them as CSS padding from Content_Renderer. This ensures
+ // block widths fit inside the padding wrapper without overflow.
if ( 'full' !== $alignment ) {
$layout_width -= $this->parse_number_from_string_with_pixels( $block['email_attrs']['root-padding-left'] ?? '0px' );
$layout_width -= $this->parse_number_from_string_with_pixels( $block['email_attrs']['root-padding-right'] ?? '0px' );
+ // Container padding may be preset references (var:preset|spacing|20).
+ $layout_width -= $this->parse_number_from_string_with_pixels( $this->resolve_preset_value( $block['email_attrs']['container-padding-left'] ?? '0px', $variables_map ) );
+ $layout_width -= $this->parse_number_from_string_with_pixels( $this->resolve_preset_value( $block['email_attrs']['container-padding-right'] ?? '0px', $variables_map ) );
}
// Resolve block padding — may be preset references like var:preset|spacing|20.
- $block_padding_left = $this->resolve_preset_value( $block['attrs']['style']['spacing']['padding']['left'] ?? '0px', $variables_map );
- $block_padding_right = $this->resolve_preset_value( $block['attrs']['style']['spacing']['padding']['right'] ?? '0px', $variables_map );
+ // When suppress-horizontal-padding is set, the block's horizontal padding
+ // has been distributed per-block by the Spacing_Preprocessor. Zero it out
+ // so children get the full available width.
+ $suppress_h_padding = ! empty( $block['email_attrs']['suppress-horizontal-padding'] );
+ $block_padding_left = $suppress_h_padding ? '0px' : $this->resolve_preset_value( $block['attrs']['style']['spacing']['padding']['left'] ?? '0px', $variables_map );
+ $block_padding_right = $suppress_h_padding ? '0px' : $this->resolve_preset_value( $block['attrs']['style']['spacing']['padding']['right'] ?? '0px', $variables_map );
$width_input = $block['attrs']['width'] ?? '100%';
// Currently we support only % and px units in case only the number is provided we assume it's %
diff --git a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-spacing-preprocessor.php b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-spacing-preprocessor.php
index 14ee33b0810..cf4134f0995 100644
--- a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-spacing-preprocessor.php
+++ b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-spacing-preprocessor.php
@@ -29,11 +29,44 @@ class Spacing_Preprocessor implements Preprocessor {
* @return array
*/
public function preprocess( array $parsed_blocks, array $layout, array $styles ): array {
- $root_padding = $this->get_root_padding( $styles );
- $parsed_blocks = $this->add_block_gaps( $parsed_blocks, $styles['spacing']['blockGap'] ?? '', null, $root_padding );
+ $root_padding = $this->get_root_padding( $styles );
+ $container_padding = $styles['__container_padding'] ?? array();
+ $parsed_blocks = $this->add_block_gaps( $parsed_blocks, $styles['spacing']['blockGap'] ?? '', null, $root_padding, false, $container_padding );
return $parsed_blocks;
}
+ /**
+ * Extract and validate horizontal padding from a block's style attributes.
+ *
+ * @param array $block The block to extract padding from.
+ * @return array Padding with 'left' and 'right' keys, or empty array if invalid/absent.
+ */
+ private function get_block_horizontal_padding( array $block ): array {
+ $padding = $block['attrs']['style']['spacing']['padding'] ?? array();
+ $has_left = isset( $padding['left'] );
+ $has_right = isset( $padding['right'] );
+
+ if ( ! $has_left && ! $has_right ) {
+ return array();
+ }
+
+ $left = $has_left ? $padding['left'] : '0px';
+ $right = $has_right ? $padding['right'] : '0px';
+
+ if ( ! is_string( $left ) || ! is_string( $right ) || preg_match( '/[<>"\']/', $left . $right ) ) {
+ return array();
+ }
+
+ if ( $this->is_zero_value( $left ) && $this->is_zero_value( $right ) ) {
+ return array();
+ }
+
+ return array(
+ 'left' => $left,
+ 'right' => $right,
+ );
+ }
+
/**
* Container block names that delegate root padding to their children
* instead of receiving it themselves.
@@ -49,6 +82,12 @@ class Spacing_Preprocessor implements Preprocessor {
* padding to their children instead of taking it themselves. This enables
* alignfull blocks to skip root padding and span the full email width.
*
+ * Container padding works similarly: when a template group wrapping post-content
+ * has its own horizontal padding, that padding is distributed per-block alongside
+ * root padding. Alignfull blocks skip both padding types and span the full
+ * contentSize. The template group gets a suppress-horizontal-padding flag so its
+ * renderer omits horizontal padding from its own CSS output.
+ *
* Blocks fall into three categories for root padding:
* - Zero padding (has_zero_padding): skip root padding entirely — edge-to-edge intent.
* - Non-zero explicit padding (has_own_padding, !has_zero_padding): receive root padding
@@ -61,9 +100,10 @@ class Spacing_Preprocessor implements Preprocessor {
* @param array|null $parent_block Parent block.
* @param array $root_padding Root horizontal padding with 'left' and 'right' keys.
* @param bool $apply_root_padding Whether this block should receive root padding (delegated by parent container).
+ * @param array $container_padding Container horizontal padding with 'left' and 'right' keys.
* @return array
*/
- private function add_block_gaps( array $parsed_blocks, string $gap = '', $parent_block = null, array $root_padding = array(), bool $apply_root_padding = false ): array {
+ private function add_block_gaps( array $parsed_blocks, string $gap = '', $parent_block = null, array $root_padding = array(), bool $apply_root_padding = false, array $container_padding = array() ): array {
foreach ( $parsed_blocks as $key => $block ) {
$block_name = $block['blockName'] ?? '';
$parent_block_name = $parent_block['blockName'] ?? '';
@@ -102,7 +142,7 @@ class Spacing_Preprocessor implements Preprocessor {
$alignment = $block['attrs']['align'] ?? null;
$has_zero_padding = $this->has_zero_horizontal_padding( $block );
$has_own_padding = $this->has_explicit_horizontal_padding( $block );
- $wraps_post_content = $apply_root_padding && $is_container && ! $has_own_padding && $this->contains_post_content( $block );
+ $wraps_post_content = $apply_root_padding && $is_container && $this->contains_post_content( $block );
$should_apply = $apply_root_padding || ( $is_root_level && ! $is_container ) || ( $is_root_level && $is_container && $has_own_padding );
$post_content_block_names = $this->get_post_content_block_names();
@@ -111,6 +151,13 @@ class Spacing_Preprocessor implements Preprocessor {
$block['email_attrs']['root-padding-right'] = $root_padding['right'];
}
+ // Apply container padding (from template group wrapping post-content).
+ // Alignfull blocks skip both root and container padding.
+ if ( $should_apply && ! $has_zero_padding && 'full' !== $alignment && ! in_array( $block_name, $post_content_block_names, true ) && ! $wraps_post_content && ! empty( $container_padding ) ) {
+ $block['email_attrs']['container-padding-left'] = $container_padding['left'];
+ $block['email_attrs']['container-padding-right'] = $container_padding['right'];
+ }
+
// Determine whether children should receive root padding delegation.
// Root-level containers delegate to their children.
// Post-content blocks that received delegation also pass it through.
@@ -118,16 +165,39 @@ class Spacing_Preprocessor implements Preprocessor {
// so that user blocks inside post-content get padding individually.
// Containers with explicit horizontal padding stop delegation — they
// manage their own layout.
- $children_apply = false;
+ $children_apply = false;
+ $children_container_pad = $container_padding;
if ( $is_root_level && $is_container && ! $has_own_padding ) {
$children_apply = true;
} elseif ( $apply_root_padding && in_array( $block_name, $post_content_block_names, true ) ) {
$children_apply = true;
} elseif ( $wraps_post_content ) {
$children_apply = true;
+
+ // When a container wrapping post-content has its own non-zero
+ // horizontal padding, distribute it as container-padding to
+ // descendant blocks and suppress the container's own CSS padding.
+ $block_padding = $this->get_block_horizontal_padding( $block );
+ if ( ! empty( $block_padding ) ) {
+ $children_container_pad = $block_padding;
+ $block['email_attrs']['suppress-horizontal-padding'] = true;
+ }
+ } elseif ( $is_root_level && $is_container && $has_own_padding && ! $has_zero_padding && $this->contains_post_content( $block ) ) {
+ // Root-level container with own padding that wraps post-content:
+ // distribute its padding as container-padding and suppress its own CSS.
+ $children_apply = true;
+ $block_padding = $this->get_block_horizontal_padding( $block );
+ if ( ! empty( $block_padding ) ) {
+ $children_container_pad = $block_padding;
+ $block['email_attrs']['suppress-horizontal-padding'] = true;
+ }
+
+ // This container also should not receive root padding itself
+ // (it delegates everything to children).
+ unset( $block['email_attrs']['root-padding-left'], $block['email_attrs']['root-padding-right'] );
}
- $block['innerBlocks'] = $this->add_block_gaps( $block['innerBlocks'] ?? array(), $gap, $block, $root_padding, $children_apply );
+ $block['innerBlocks'] = $this->add_block_gaps( $block['innerBlocks'] ?? array(), $gap, $block, $root_padding, $children_apply, $children_container_pad );
$parsed_blocks[ $key ] = $block;
}
diff --git a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/class-content-renderer.php b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/class-content-renderer.php
index 1858f51b740..43211c763e9 100644
--- a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/class-content-renderer.php
+++ b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/class-content-renderer.php
@@ -108,6 +108,16 @@ class Content_Renderer {
*/
private ?string $post_content_width = null;
+ /**
+ * Container padding from the template group wrapping post-content.
+ *
+ * Stored during the first preprocessing pass and passed to user blocks
+ * in the second pass so they receive the container padding per-block.
+ *
+ * @var array{left?: string, right?: string}
+ */
+ private array $container_padding = array();
+
/**
* CSS inliner
*
@@ -242,13 +252,20 @@ class Content_Renderer {
if ( $post_content_num < $content_size_num - 0.01 ) {
unset( $styles['spacing']['padding']['left'], $styles['spacing']['padding']['right'] );
}
+
+ // Pass container padding from the first pass so the
+ // Spacing_Preprocessor can distribute it to user blocks.
+ if ( ! empty( $this->container_padding ) ) {
+ $styles['__container_padding'] = $this->container_padding;
+ }
}
$result = $this->process_manager->preprocess( $parsed_blocks, $layout, $styles );
- // After the first pass: find the post-content block's width.
+ // After the first pass: find the post-content block's width and container padding.
if ( null === $this->post_content_width ) {
$this->post_content_width = $this->find_post_content_width( $result );
+ $this->container_padding = $this->find_container_padding( $result );
}
return $result;
@@ -284,6 +301,43 @@ class Content_Renderer {
return null;
}
+ /**
+ * Find the container padding from blocks with suppress-horizontal-padding flag.
+ *
+ * Searches the preprocessed template blocks for a container that wraps
+ * post-content and had its horizontal padding distributed per-block.
+ *
+ * @param array $blocks Preprocessed blocks.
+ * @return array{left?: string, right?: string} Container padding values, or empty array.
+ */
+ private function find_container_padding( array $blocks ): array {
+ $variables_map = $this->theme_controller->get_variables_values_map();
+
+ foreach ( $blocks as $block ) {
+ $email_attrs = $block['email_attrs'] ?? array();
+ if ( ! empty( $email_attrs['suppress-horizontal-padding'] ) ) {
+ $padding = $block['attrs']['style']['spacing']['padding'] ?? array();
+ $result = array();
+ if ( isset( $padding['left'] ) && is_string( $padding['left'] ) ) {
+ $result['left'] = $this->resolve_preset_padding( $padding['left'], $variables_map );
+ }
+ if ( isset( $padding['right'] ) && is_string( $padding['right'] ) ) {
+ $result['right'] = $this->resolve_preset_padding( $padding['right'], $variables_map );
+ }
+ if ( ! empty( $result ) ) {
+ return $result;
+ }
+ }
+ if ( ! empty( $block['innerBlocks'] ) ) {
+ $found = $this->find_container_padding( $block['innerBlocks'] );
+ if ( ! empty( $found ) ) {
+ return $found;
+ }
+ }
+ }
+ return array();
+ }
+
/**
* Renders block
* Translates block's HTML to HTML suitable for email clients. The method is intended as a callback for 'render_block' filter.
@@ -345,24 +399,35 @@ class Content_Renderer {
}
/**
- * Wrap block output with root horizontal padding.
+ * Wrap block output with horizontal padding (root + container).
*
* Root padding is distributed by the Spacing_Preprocessor from the outer
- * email container to individual blocks. This method applies it uniformly
- * to all blocks regardless of whether they use Abstract_Block_Renderer
- * or a custom render_email_callback.
+ * email container to individual blocks. Container padding comes from
+ * template groups wrapping post-content. Both are combined into a single
+ * CSS padding wrapper. This method applies padding uniformly to all blocks
+ * regardless of whether they use Abstract_Block_Renderer or a custom
+ * render_email_callback.
*
* @param string $content The rendered block content.
* @param array $email_attrs The email attributes from the parsed block.
- * @return string The content wrapped with horizontal padding, or unchanged if no root padding.
+ * @return string The content wrapped with horizontal padding, or unchanged if no padding.
*/
private function add_root_horizontal_padding( string $content, array $email_attrs ): string {
+ $padding_left = $this->sum_padding_values(
+ $email_attrs['root-padding-left'] ?? null,
+ $email_attrs['container-padding-left'] ?? null
+ );
+ $padding_right = $this->sum_padding_values(
+ $email_attrs['root-padding-right'] ?? null,
+ $email_attrs['container-padding-right'] ?? null
+ );
+
$css_attrs = array();
- if ( isset( $email_attrs['root-padding-left'] ) ) {
- $css_attrs['padding-left'] = $email_attrs['root-padding-left'];
+ if ( $padding_left > 0 ) {
+ $css_attrs['padding-left'] = $padding_left . 'px';
}
- if ( isset( $email_attrs['root-padding-right'] ) ) {
- $css_attrs['padding-right'] = $email_attrs['root-padding-right'];
+ if ( $padding_right > 0 ) {
+ $css_attrs['padding-right'] = $padding_right . 'px';
}
if ( empty( $css_attrs ) ) {
return $content;
@@ -391,6 +456,43 @@ class Content_Renderer {
return Table_Wrapper_Helper::render_outlook_table_wrapper( $div_content, $table_attrs, $cell_attrs );
}
+ /**
+ * Sum two CSS pixel padding values.
+ *
+ * @param string|null $value1 First padding value (e.g., '20px').
+ * @param string|null $value2 Second padding value (e.g., '10px').
+ * @return float The sum in pixels.
+ */
+ private function sum_padding_values( ?string $value1, ?string $value2 ): float {
+ $sum = 0.0;
+ if ( null !== $value1 ) {
+ $sum += (float) str_replace( 'px', '', $value1 );
+ }
+ if ( null !== $value2 ) {
+ $sum += (float) str_replace( 'px', '', $value2 );
+ }
+ return $sum;
+ }
+
+ /**
+ * Resolve a CSS value that may contain a preset variable reference.
+ *
+ * Block attributes store padding as preset references like
+ * "var:preset|spacing|20" which resolve to actual pixel values.
+ *
+ * @param string $value The CSS value, possibly a preset reference.
+ * @param array $variables_map Map of CSS variable names to resolved values.
+ * @return string The resolved value (e.g. "8px") or the original value.
+ */
+ private function resolve_preset_padding( string $value, array $variables_map ): string {
+ if ( strpos( $value, 'var:preset|' ) !== 0 ) {
+ return $value;
+ }
+
+ $css_var_name = '--wp--' . str_replace( '|', '--', str_replace( 'var:', '', $value ) );
+ return $variables_map[ $css_var_name ] ?? $value;
+ }
+
/**
* Set template globals
*
@@ -424,6 +526,7 @@ class Content_Renderer {
remove_filter( 'woocommerce_email_blocks_renderer_parsed_blocks', array( $this, 'preprocess_parsed_blocks' ) );
$this->post_content_width = null;
+ $this->container_padding = array();
// Restore the original core/post-content render callback.
// Note: We always restore it, even if it was null originally.
diff --git a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-group.php b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-group.php
index 83cc81211a8..10da2d431ea 100644
--- a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-group.php
+++ b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-group.php
@@ -65,7 +65,13 @@ class Group extends Abstract_Block_Renderer {
);
// Padding properties need to be added to the table cell.
+ // When suppress-horizontal-padding is set, the container's horizontal
+ // padding has been distributed per-block by the Spacing_Preprocessor.
+ // Only vertical padding is kept on the group's own CSS output.
$cell_styles = Styles_Helper::get_block_styles( $block_attributes, $rendering_context, array( 'padding' ) );
+ if ( ! empty( $parsed_block['email_attrs']['suppress-horizontal-padding'] ) ) {
+ $cell_styles = $this->remove_horizontal_padding( $cell_styles );
+ }
$table_attrs = array(
'class' => 'email-block-group ' . $original_classname,
@@ -81,4 +87,28 @@ class Group extends Abstract_Block_Renderer {
return Table_Wrapper_Helper::render_table_wrapper( '{group_content}', $table_attrs, $cell_attrs );
}
+
+ /**
+ * Remove horizontal padding properties from compiled cell styles.
+ *
+ * Used when suppress-horizontal-padding is set to prevent the group
+ * from applying its own horizontal padding (which has been distributed
+ * per-block by the Spacing_Preprocessor).
+ *
+ * @param array $cell_styles The compiled cell styles from Styles_Helper::get_block_styles().
+ * @return array Styles with horizontal padding removed.
+ */
+ private function remove_horizontal_padding( array $cell_styles ): array {
+ if ( ! isset( $cell_styles['declarations'] ) || ! is_array( $cell_styles['declarations'] ) ) {
+ return $cell_styles;
+ }
+
+ unset(
+ $cell_styles['declarations']['padding-left'],
+ $cell_styles['declarations']['padding-right']
+ );
+
+ // Rebuild CSS string from remaining declarations.
+ return Styles_Helper::extend_block_styles( $cell_styles, array() );
+ }
}
diff --git a/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Content_Renderer_Test.php b/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Content_Renderer_Test.php
index 60e6d995392..f5eee42d9f1 100644
--- a/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Content_Renderer_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Content_Renderer_Test.php
@@ -209,12 +209,14 @@ class Content_Renderer_Test extends \Email_Editor_Integration_Test_Case {
}
/**
- * Test preprocess_parsed_blocks skips root padding in second pass when
- * a container above post-content absorbed it (WooCommerce template pattern).
+ * Test preprocess_parsed_blocks distributes both root and container padding
+ * in second pass when a container above post-content has own padding
+ * (WooCommerce template pattern).
*/
- public function testItSkipsRootPaddingInSecondPassWhenAbsorbed(): void {
+ public function testItDistributesBothPaddingsInSecondPassWhenContainerHasPadding(): void {
// First pass: template blocks with a group wrapping post-content.
- // The group has own padding so it absorbs root padding and stops delegation.
+ // The group has own padding — it distributes both root padding and
+ // container padding per-block to user blocks.
$template_blocks = array(
array(
'blockName' => 'core/group',
@@ -240,9 +242,13 @@ class Content_Renderer_Test extends \Email_Editor_Integration_Test_Case {
$first_result = $this->renderer->preprocess_parsed_blocks( $template_blocks );
- // post-content should have a narrower width than contentSize
- // (root padding + group padding subtracted).
- $post_content = $first_result[0]['innerBlocks'][0];
+ // Root group should have suppress-horizontal-padding flag.
+ $root_group = $first_result[0];
+ $this->assertTrue( $root_group['email_attrs']['suppress-horizontal-padding'] );
+
+ // post-content should have full contentSize width (padding is distributed
+ // per-block, not subtracted from the group).
+ $post_content = $root_group['innerBlocks'][0];
$this->assertArrayHasKey( 'width', $post_content['email_attrs'] );
// Second pass: user blocks (simulating post-content rendering).
@@ -256,9 +262,18 @@ class Content_Renderer_Test extends \Email_Editor_Integration_Test_Case {
$second_result = $this->renderer->preprocess_parsed_blocks( $user_blocks );
- // User blocks should NOT have root padding (it was absorbed upstream).
- $this->assertArrayNotHasKey( 'root-padding-left', $second_result[0]['email_attrs'] );
- $this->assertArrayNotHasKey( 'root-padding-right', $second_result[0]['email_attrs'] );
+ // User blocks should have root padding (delegated, not absorbed).
+ $this->assertArrayHasKey( 'root-padding-left', $second_result[0]['email_attrs'] );
+ $this->assertArrayHasKey( 'root-padding-right', $second_result[0]['email_attrs'] );
+
+ // User blocks should also have container padding from the template group.
+ $this->assertArrayHasKey( 'container-padding-left', $second_result[0]['email_attrs'] );
+ $this->assertArrayHasKey( 'container-padding-right', $second_result[0]['email_attrs'] );
+ $this->assertEquals( '20px', $second_result[0]['email_attrs']['container-padding-left'] );
+ $this->assertEquals( '20px', $second_result[0]['email_attrs']['container-padding-right'] );
+
+ // Width should account for both root and container padding.
+ $this->assertArrayHasKey( 'width', $second_result[0]['email_attrs'] );
}
/**
@@ -300,6 +315,98 @@ class Content_Renderer_Test extends \Email_Editor_Integration_Test_Case {
$this->assertArrayHasKey( 'root-padding-right', $second_result[0]['email_attrs'] );
}
+ /**
+ * Test render_block applies combined root + container horizontal padding
+ */
+ public function testItAppliesCombinedRootAndContainerPadding(): void {
+ register_block_type(
+ 'test/combined-padding-block',
+ array(
+ 'render_email_callback' => function () {
+ return '<p>combined padding</p>';
+ },
+ )
+ );
+
+ $result = $this->renderer->render_block(
+ 'content',
+ array(
+ 'blockName' => 'test/combined-padding-block',
+ 'email_attrs' => array(
+ 'root-padding-left' => '10px',
+ 'root-padding-right' => '10px',
+ 'container-padding-left' => '20px',
+ 'container-padding-right' => '20px',
+ ),
+ )
+ );
+
+ $this->assertStringContainsString( 'combined padding', $result );
+ $this->assertStringContainsString( 'email-root-padding', $result );
+ // Combined: 10 + 20 = 30px each side.
+ $this->assertStringContainsString( 'padding-left:30px', $result );
+ $this->assertStringContainsString( 'padding-right:30px', $result );
+ \WP_Block_Type_Registry::get_instance()->unregister( 'test/combined-padding-block' );
+ }
+
+ /**
+ * Test render_block skips padding wrapper for alignfull blocks (no padding attrs)
+ */
+ public function testItSkipsPaddingForAlignfullBlocks(): void {
+ register_block_type(
+ 'test/alignfull-block',
+ array(
+ 'render_email_callback' => function () {
+ return '<p>full width content</p>';
+ },
+ )
+ );
+
+ $result = $this->renderer->render_block(
+ 'content',
+ array(
+ 'blockName' => 'test/alignfull-block',
+ 'email_attrs' => array(),
+ )
+ );
+
+ $this->assertEquals( '<p>full width content</p>', $result );
+ $this->assertStringNotContainsString( 'email-root-padding', $result );
+ \WP_Block_Type_Registry::get_instance()->unregister( 'test/alignfull-block' );
+ }
+
+ /**
+ * Test render_block applies asymmetric combined padding correctly
+ */
+ public function testItAppliesAsymmetricCombinedPadding(): void {
+ register_block_type(
+ 'test/asymmetric-padding-block',
+ array(
+ 'render_email_callback' => function () {
+ return '<p>asymmetric</p>';
+ },
+ )
+ );
+
+ $result = $this->renderer->render_block(
+ 'content',
+ array(
+ 'blockName' => 'test/asymmetric-padding-block',
+ 'email_attrs' => array(
+ 'root-padding-left' => '10px',
+ 'root-padding-right' => '15px',
+ 'container-padding-left' => '20px',
+ 'container-padding-right' => '25px',
+ ),
+ )
+ );
+
+ // Combined: left = 10 + 20 = 30px, right = 15 + 25 = 40px.
+ $this->assertStringContainsString( 'padding-left:30px', $result );
+ $this->assertStringContainsString( 'padding-right:40px', $result );
+ \WP_Block_Type_Registry::get_instance()->unregister( 'test/asymmetric-padding-block' );
+ }
+
/**
* Get the value of the style attribute for a given tag in the HTML.
*
diff --git a/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Blocks_Width_Preprocessor_Test.php b/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Blocks_Width_Preprocessor_Test.php
index d503859c6bc..d02a93d3405 100644
--- a/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Blocks_Width_Preprocessor_Test.php
+++ b/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Blocks_Width_Preprocessor_Test.php
@@ -582,6 +582,54 @@ class Blocks_Width_Preprocessor_Test extends \Email_Editor_Unit_Test {
$this->assertEquals( '225px', $image_block['email_attrs']['width'] );
}
+ /**
+ * Test it subtracts container padding from block widths
+ */
+ public function testItSubtractsContainerPaddingFromEmailAttrs(): void {
+ $blocks = array(
+ // Block with both root and container padding.
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'email_attrs' => array(
+ 'root-padding-left' => '10px',
+ 'root-padding-right' => '10px',
+ 'container-padding-left' => '20px',
+ 'container-padding-right' => '20px',
+ ),
+ 'innerBlocks' => array(),
+ ),
+ // Alignfull block: container padding should NOT be subtracted.
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(
+ 'align' => 'full',
+ ),
+ 'email_attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ // Block with only container padding.
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'email_attrs' => array(
+ 'container-padding-left' => '15px',
+ 'container-padding-right' => '15px',
+ ),
+ 'innerBlocks' => array(),
+ ),
+ );
+ $result = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+
+ $this->assertCount( 3, $result );
+ // 660 - 10 - 10 (root) - 20 - 20 (container) = 600.
+ $this->assertEquals( '600px', $result[0]['email_attrs']['width'] );
+ // Full-width: 660px (nothing subtracted).
+ $this->assertEquals( '660px', $result[1]['email_attrs']['width'] );
+ // 660 - 15 - 15 (container only) = 630.
+ $this->assertEquals( '630px', $result[2]['email_attrs']['width'] );
+ }
+
/**
* Test it handles non-string width values
*/
diff --git a/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Spacing_Preprocessor_Test.php b/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Spacing_Preprocessor_Test.php
index 267f1f9cdf8..f8c0c5c4bf7 100644
--- a/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Spacing_Preprocessor_Test.php
+++ b/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Spacing_Preprocessor_Test.php
@@ -676,6 +676,205 @@ class Spacing_Preprocessor_Test extends \Email_Editor_Unit_Test {
$this->assertEquals( '10px', $plain_paragraph['email_attrs']['root-padding-right'] );
}
+ /**
+ * Test root-level group with own padding wrapping post-content distributes container padding
+ */
+ public function testItDistributesContainerPaddingFromRootGroupWrappingPostContent(): void {
+ $blocks = array(
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(
+ 'style' => array(
+ 'spacing' => array(
+ 'padding' => array(
+ 'left' => '20px',
+ 'right' => '20px',
+ 'top' => '15px',
+ 'bottom' => '15px',
+ ),
+ ),
+ ),
+ ),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/post-content',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array( 'align' => 'full' ),
+ 'innerBlocks' => array(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ $result = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+ $root_group = $result[0];
+ $post_content = $root_group['innerBlocks'][0];
+ $paragraph = $post_content['innerBlocks'][0];
+ $alignfull = $post_content['innerBlocks'][1];
+
+ // Root group should have suppress-horizontal-padding flag.
+ $this->assertTrue( $root_group['email_attrs']['suppress-horizontal-padding'] );
+
+ // Root group should NOT have root padding (delegates everything).
+ $this->assertArrayNotHasKey( 'root-padding-left', $root_group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'root-padding-right', $root_group['email_attrs'] );
+
+ // Post-content should not get container padding (it's a pass-through).
+ $this->assertArrayNotHasKey( 'container-padding-left', $post_content['email_attrs'] );
+ $this->assertArrayNotHasKey( 'container-padding-right', $post_content['email_attrs'] );
+
+ // Normal paragraph should get both root and container padding.
+ $this->assertEquals( '10px', $paragraph['email_attrs']['root-padding-left'] );
+ $this->assertEquals( '10px', $paragraph['email_attrs']['root-padding-right'] );
+ $this->assertEquals( '20px', $paragraph['email_attrs']['container-padding-left'] );
+ $this->assertEquals( '20px', $paragraph['email_attrs']['container-padding-right'] );
+
+ // Alignfull block should skip BOTH root and container padding.
+ $this->assertArrayNotHasKey( 'root-padding-left', $alignfull['email_attrs'] );
+ $this->assertArrayNotHasKey( 'root-padding-right', $alignfull['email_attrs'] );
+ $this->assertArrayNotHasKey( 'container-padding-left', $alignfull['email_attrs'] );
+ $this->assertArrayNotHasKey( 'container-padding-right', $alignfull['email_attrs'] );
+ }
+
+ /**
+ * Test container padding is distributed from nested group wrapping post-content
+ */
+ public function testItDistributesContainerPaddingFromNestedGroupWrappingPostContent(): void {
+ $blocks = array(
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(
+ 'style' => array(
+ 'spacing' => array(
+ 'padding' => array(
+ 'left' => '25px',
+ 'right' => '25px',
+ ),
+ ),
+ ),
+ ),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/post-content',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ $result = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+ $root_group = $result[0];
+ $content_group = $root_group['innerBlocks'][0];
+ $paragraph = $content_group['innerBlocks'][0]['innerBlocks'][0];
+
+ // Content group wrapping post-content should have suppress flag.
+ $this->assertTrue( $content_group['email_attrs']['suppress-horizontal-padding'] );
+
+ // Paragraph inside post-content should get container padding.
+ $this->assertEquals( '25px', $paragraph['email_attrs']['container-padding-left'] );
+ $this->assertEquals( '25px', $paragraph['email_attrs']['container-padding-right'] );
+ }
+
+ /**
+ * Test container padding is passed from styles (second pass) to user blocks
+ */
+ public function testItAppliesContainerPaddingFromStyles(): void {
+ $styles = $this->styles;
+ $styles['__container_padding'] = array(
+ 'left' => '20px',
+ 'right' => '20px',
+ );
+
+ // Simulate second pass: user blocks at top level (as post-content renders them).
+ $blocks = array(
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array( 'align' => 'full' ),
+ 'innerBlocks' => array(),
+ ),
+ );
+
+ $result = $this->preprocessor->preprocess( $blocks, $this->layout, $styles );
+ $paragraph = $result[0];
+ $alignfull = $result[1];
+
+ // Normal block gets container padding.
+ $this->assertEquals( '20px', $paragraph['email_attrs']['container-padding-left'] );
+ $this->assertEquals( '20px', $paragraph['email_attrs']['container-padding-right'] );
+
+ // Alignfull block skips container padding.
+ $this->assertArrayNotHasKey( 'container-padding-left', $alignfull['email_attrs'] );
+ $this->assertArrayNotHasKey( 'container-padding-right', $alignfull['email_attrs'] );
+ }
+
+ /**
+ * Test template group without own padding does NOT set container padding
+ */
+ public function testItDoesNotSetContainerPaddingWhenGroupHasNoPadding(): void {
+ $blocks = array(
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/post-content',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ $result = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+ $group = $result[0];
+ $paragraph = $group['innerBlocks'][0]['innerBlocks'][0];
+
+ // Group should NOT have suppress flag.
+ $this->assertArrayNotHasKey( 'suppress-horizontal-padding', $group['email_attrs'] );
+
+ // Paragraph should NOT have container padding.
+ $this->assertArrayNotHasKey( 'container-padding-left', $paragraph['email_attrs'] );
+ $this->assertArrayNotHasKey( 'container-padding-right', $paragraph['email_attrs'] );
+
+ // Paragraph should still get root padding.
+ $this->assertEquals( '10px', $paragraph['email_attrs']['root-padding-left'] );
+ $this->assertEquals( '10px', $paragraph['email_attrs']['root-padding-right'] );
+ }
+
/**
* Test it rejects malicious values in blockGap
*/