Commit 60b7a2159da for woocommerce
commit 60b7a2159da2e591c7774e6c148bfffc41993195
Author: Allison Levine <1689238+allilevine@users.noreply.github.com>
Date: Thu Mar 12 11:22:33 2026 -0400
Email Editor: Distribute root horizontal padding to block wrappers (#63359)
* Email Editor: Distribute root horizontal padding to individual block wrappers
Move horizontal padding from the outer email_layout_wrapper to individual
block spacer wrappers via the Spacing_Preprocessor. This is a foundational
change for future alignfull block support — blocks marked as full-width can
skip root padding to span the full email width.
Container blocks (groups, post-content) at the root level delegate padding
to their children instead of receiving it themselves. Groups that wrap
post-content are transparent, delegating further so user blocks inside
post-content get individual padding. The rendered email looks identical
after this change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Sanitize root padding values in Spacing_Preprocessor
Extract root padding into a get_root_padding() helper that validates
against injection characters (<, >, ", ') matching the existing guard
in get_columns_block_gap() for defense-in-depth consistency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update integration test for distributed root padding
The wrapper no longer has horizontal padding — it's distributed to
individual block wrappers. Update testItInlinesWrappersStyles to
assert the absence of padding-left/right on the wrapper.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Skip root padding when theme defines no horizontal padding
Return empty array from get_root_padding() when neither left nor right
key exists in theme styles, avoiding unnecessary 0px values on every
block wrapper.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make contains_post_content() recursive for deeply nested templates
Search through container blocks recursively so that template structures
like group → group → group → post-content are handled correctly. Without
this, intermediate groups would absorb root padding and stop delegation,
leaving user blocks inside post-content without padding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Respect explicit block padding; make CONTAINER_BLOCKS private
Blocks that explicitly define their own horizontal padding (e.g., a
full-width banner with 0px padding or a group with 40px padding) now
skip root padding. Containers with explicit padding also stop
delegation so their children follow the container's own layout. This
matches the editor behavior where edge-to-edge blocks have no margins.
Also marks the CONTAINER_BLOCKS constant as private per review feedback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
diff --git a/packages/php/email-editor/changelog/email-editor-distribute-root-padding b/packages/php/email-editor/changelog/email-editor-distribute-root-padding
new file mode 100644
index 00000000000..06b55a46d5c
--- /dev/null
+++ b/packages/php/email-editor/changelog/email-editor-distribute-root-padding
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Distribute root horizontal padding to individual block wrappers for future alignfull support.
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 f56d909c433..4ffa638fda3 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
@@ -22,20 +22,37 @@ class Spacing_Preprocessor implements Preprocessor {
* @return array
*/
public function preprocess( array $parsed_blocks, array $layout, array $styles ): array {
- $parsed_blocks = $this->add_block_gaps( $parsed_blocks, $styles['spacing']['blockGap'] ?? '', null );
+ $root_padding = $this->get_root_padding( $styles );
+
+ $parsed_blocks = $this->add_block_gaps( $parsed_blocks, $styles['spacing']['blockGap'] ?? '', null, $root_padding );
return $parsed_blocks;
}
/**
- * Adds margin-top to blocks that are not first or last in the columns block.
+ * Container block names that delegate root padding to their children
+ * instead of receiving it themselves.
+ */
+ private const CONTAINER_BLOCKS = array( 'core/group', 'core/post-content' );
+
+ /**
+ * Adds spacing to blocks: margin-top for vertical gaps, horizontal padding for
+ * column gaps, and root padding for children of root-level containers.
+ *
+ * Root padding is distributed from the outer email wrapper to individual block
+ * wrappers. Container blocks (groups, post-content) at the root level delegate
+ * padding to their children instead of taking it themselves. This enables
+ * alignfull blocks to skip root padding and span the full email width.
*
* @param array $parsed_blocks Parsed blocks.
* @param string $gap Gap.
* @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).
* @return array
*/
- private function add_block_gaps( array $parsed_blocks, string $gap = '', $parent_block = null ): array {
+ private function add_block_gaps( array $parsed_blocks, string $gap = '', $parent_block = null, array $root_padding = array(), bool $apply_root_padding = false ): array {
foreach ( $parsed_blocks as $key => $block ) {
+ $block_name = $block['blockName'] ?? '';
$parent_block_name = $parent_block['blockName'] ?? '';
// Ensure that email_attrs are set.
$block['email_attrs'] = $block['email_attrs'] ?? array();
@@ -57,13 +74,120 @@ class Spacing_Preprocessor implements Preprocessor {
}
}
- $block['innerBlocks'] = $this->add_block_gaps( $block['innerBlocks'] ?? array(), $gap, $block );
+ // Distribute root horizontal padding.
+ // Container blocks (group, post-content) at root level do NOT get padding;
+ // they delegate it to their children. Non-container blocks at root level
+ // (e.g., columns, paragraph) get padding directly.
+ // Blocks flagged with $apply_root_padding (children of root containers)
+ // also get padding, unless they are post-content or a container wrapping
+ // post-content (both delegate further down the tree).
+ // Blocks that explicitly define their own horizontal padding are managing
+ // their own layout and skip root padding entirely. Containers with explicit
+ // padding also stop delegation so their children follow the container's padding.
+ $is_root_level = null === $parent_block;
+ $is_container = in_array( $block_name, self::CONTAINER_BLOCKS, true );
+ $alignment = $block['attrs']['align'] ?? null;
+ $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 );
+ $should_apply = $apply_root_padding || ( $is_root_level && ! $is_container );
+
+ if ( $should_apply && ! $has_own_padding && 'full' !== $alignment && 'core/post-content' !== $block_name && ! $wraps_post_content && ! empty( $root_padding ) ) {
+ $block['email_attrs']['padding-left'] = $root_padding['left'];
+ $block['email_attrs']['padding-right'] = $root_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.
+ // Containers wrapping post-content that received delegation also delegate,
+ // 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;
+ if ( $is_root_level && $is_container && ! $has_own_padding ) {
+ $children_apply = true;
+ } elseif ( $apply_root_padding && 'core/post-content' === $block_name ) {
+ $children_apply = true;
+ } elseif ( $wraps_post_content ) {
+ $children_apply = true;
+ }
+
+ $block['innerBlocks'] = $this->add_block_gaps( $block['innerBlocks'] ?? array(), $gap, $block, $root_padding, $children_apply );
$parsed_blocks[ $key ] = $block;
}
return $parsed_blocks;
}
+ /**
+ * Checks whether a block contains a core/post-content descendant.
+ *
+ * Searches recursively through container blocks (groups) so that
+ * deeply nested template structures like group → group → post-content
+ * are handled correctly.
+ *
+ * @param array $block The block to check.
+ * @return bool True if the block has a post-content descendant.
+ */
+ private function contains_post_content( array $block ): bool {
+ foreach ( $block['innerBlocks'] ?? array() as $inner_block ) {
+ $name = $inner_block['blockName'] ?? '';
+ if ( 'core/post-content' === $name ) {
+ return true;
+ }
+ if ( in_array( $name, self::CONTAINER_BLOCKS, true ) && $this->contains_post_content( $inner_block ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks whether a block explicitly defines its own horizontal padding.
+ *
+ * When a block has explicit padding-left or padding-right in its style
+ * attributes, it is managing its own layout. Root padding should not
+ * be added on top, and containers with explicit padding should not
+ * delegate root padding to their children.
+ *
+ * @param array $block The block to check.
+ * @return bool True if the block defines horizontal padding.
+ */
+ private function has_explicit_horizontal_padding( array $block ): bool {
+ $padding = $block['attrs']['style']['spacing']['padding'] ?? array();
+ return isset( $padding['left'] ) || isset( $padding['right'] );
+ }
+
+ /**
+ * Extracts and sanitizes root horizontal padding from theme styles.
+ *
+ * @param array $styles Theme styles.
+ * @return array Root padding with 'left' and 'right' keys, or empty array if invalid.
+ */
+ private function get_root_padding( array $styles ): array {
+ $padding = $styles['spacing']['padding'] ?? array();
+ $has_left = isset( $padding['left'] );
+ $has_right = isset( $padding['right'] );
+
+ // If neither horizontal padding key is defined, skip root padding entirely.
+ if ( ! $has_left && ! $has_right ) {
+ return array();
+ }
+
+ $left = $has_left ? $padding['left'] : '0px';
+ $right = $has_right ? $padding['right'] : '0px';
+
+ // Validate against potentially malicious values.
+ if ( ! is_string( $left ) || ! is_string( $right ) || preg_match( '/[<>"\']/', $left . $right ) ) {
+ return array();
+ }
+
+ return array(
+ 'left' => $left,
+ 'right' => $right,
+ );
+ }
+
/**
* Extracts the horizontal blockGap from a columns 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 1c3b0f5dd3f..448fa0bdc2b 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
@@ -196,13 +196,20 @@ class Content_Renderer {
}
/**
- * Preprocess parsed blocks
+ * Preprocess parsed blocks.
+ *
+ * Called for both template blocks and post-content user blocks. The
+ * Spacing_Preprocessor handles root padding distribution: container
+ * blocks (groups wrapping post-content) are transparent, delegating
+ * padding to their children so user blocks get individual padding.
*
* @param array $parsed_blocks Parsed blocks.
* @return array
*/
public function preprocess_parsed_blocks( array $parsed_blocks ): array {
- return $this->process_manager->preprocess( $parsed_blocks, $this->theme_controller->get_layout_settings(), $this->theme_controller->get_styles() );
+ $styles = $this->theme_controller->get_styles();
+
+ return $this->process_manager->preprocess( $parsed_blocks, $this->theme_controller->get_layout_settings(), $styles );
}
/**
diff --git a/packages/php/email-editor/src/Engine/Renderer/class-renderer.php b/packages/php/email-editor/src/Engine/Renderer/class-renderer.php
index 913653f98a6..d3baa51225b 100644
--- a/packages/php/email-editor/src/Engine/Renderer/class-renderer.php
+++ b/packages/php/email-editor/src/Engine/Renderer/class-renderer.php
@@ -133,8 +133,6 @@ class Renderer {
'color' => $email_styles['color']['text'] ?? 'inherit',
'padding-top' => $email_styles['spacing']['padding']['top'] ?? '0px',
'padding-bottom' => $email_styles['spacing']['padding']['bottom'] ?? '0px',
- 'padding-left' => $email_styles['spacing']['padding']['left'] ?? '0px',
- 'padding-right' => $email_styles['spacing']['padding']['right'] ?? '0px',
'font-family' => $email_styles['typography']['fontFamily'] ?? 'inherit',
'line-height' => $email_styles['typography']['lineHeight'] ?? '1.5',
'font-size' => $email_styles['typography']['fontSize'] ?? 'inherit',
diff --git a/packages/php/email-editor/tests/integration/Engine/Renderer/Renderer_Test.php b/packages/php/email-editor/tests/integration/Engine/Renderer/Renderer_Test.php
index 495d4c944dc..8103e7b32d0 100644
--- a/packages/php/email-editor/tests/integration/Engine/Renderer/Renderer_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Renderer/Renderer_Test.php
@@ -158,8 +158,9 @@ class Renderer_Test extends \Email_Editor_Integration_Test_Case {
$this->assertStringContainsString( 'font-family: Test Font Family;', $style );
$this->assertStringContainsString( 'padding-top: 3px;', $style );
$this->assertStringContainsString( 'padding-bottom: 4px;', $style );
- $this->assertStringContainsString( 'padding-left: 2px;', $style );
- $this->assertStringContainsString( 'padding-right: 1px;', $style );
+ // Horizontal padding is now distributed to individual block wrappers via Spacing_Preprocessor.
+ $this->assertStringNotContainsString( 'padding-left:', $style );
+ $this->assertStringNotContainsString( 'padding-right:', $style );
$this->assertStringContainsString( 'max-width: 660px;', $style );
}
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 066bbe44ad3..3411648d097 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
@@ -111,6 +111,16 @@ class Spacing_Preprocessor_Test extends \Email_Editor_Unit_Test {
$this->assertArrayNotHasKey( 'margin-top', $nested_column_first_item['email_attrs'] );
$this->assertArrayHasKey( 'margin-top', $nested_column_second_item['email_attrs'] );
$this->assertEquals( '10px', $nested_column_second_item['email_attrs']['margin-top'] );
+
+ // Root-level blocks should have root padding.
+ $this->assertEquals( '10px', $first_columns['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $first_columns['email_attrs']['padding-right'] );
+ $this->assertEquals( '10px', $second_columns['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $second_columns['email_attrs']['padding-right'] );
+
+ // Nested blocks should not have root padding.
+ $this->assertArrayNotHasKey( 'padding-right', $nested_column_first_item['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $nested_column_second_item['email_attrs'] );
}
/**
@@ -244,6 +254,414 @@ class Spacing_Preprocessor_Test extends \Email_Editor_Unit_Test {
$this->assertEquals( '10px', $second_column['email_attrs']['padding-left'] );
}
+ /**
+ * Test it skips root padding for core/post-content but applies it to its children
+ */
+ public function testItDistributesRootPaddingThroughPostContent(): void {
+ $blocks = array(
+ array(
+ 'blockName' => 'core/post-content',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ array(
+ 'blockName' => 'core/image',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ ),
+ ),
+ );
+
+ $result = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+ $post_content = $result[0];
+ $first_child = $post_content['innerBlocks'][0];
+ $second_child = $post_content['innerBlocks'][1];
+
+ // core/post-content itself should NOT get root padding (it's a pass-through).
+ $this->assertArrayNotHasKey( 'padding-left', $post_content['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $post_content['email_attrs'] );
+
+ // Direct children of post-content should get root padding.
+ $this->assertEquals( '10px', $first_child['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $first_child['email_attrs']['padding-right'] );
+ $this->assertEquals( '10px', $second_child['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $second_child['email_attrs']['padding-right'] );
+ }
+
+ /**
+ * Test it distributes root padding through post-content nested inside a root-level group
+ */
+ public function testItDistributesRootPaddingThroughNestedPostContent(): 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(),
+ ),
+ array(
+ 'blockName' => 'core/image',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ $result = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+ $group = $result[0];
+ $post_content = $group['innerBlocks'][0];
+ $first_child = $post_content['innerBlocks'][0];
+ $second_child = $post_content['innerBlocks'][1];
+
+ // Root-level group is a container — it does NOT get root padding itself.
+ $this->assertArrayNotHasKey( 'padding-left', $group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $group['email_attrs'] );
+
+ // Nested post-content should NOT get root padding (post-content never gets padding).
+ $this->assertArrayNotHasKey( 'padding-left', $post_content['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $post_content['email_attrs'] );
+
+ // Children of post-content inside root group SHOULD get padding (delegation chain).
+ $this->assertEquals( '10px', $first_child['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $first_child['email_attrs']['padding-right'] );
+ $this->assertEquals( '10px', $second_child['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $second_child['email_attrs']['padding-right'] );
+ }
+
+ /**
+ * Test content-group wrapping post-content is transparent in template-like structure
+ */
+ public function testItMakesContentGroupTransparentWhenWrappingPostContent(): void {
+ $blocks = array(
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/site-title',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/post-content',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ ),
+ ),
+ array(
+ 'blockName' => 'core/group',
+ '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];
+ $site_title = $root_group['innerBlocks'][0];
+ $content_group = $root_group['innerBlocks'][1];
+ $post_content = $content_group['innerBlocks'][0];
+ $footer_group = $root_group['innerBlocks'][2];
+
+ // Root group: no padding (root container delegates).
+ $this->assertArrayNotHasKey( 'padding-left', $root_group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $root_group['email_attrs'] );
+
+ // Site title: gets root padding (non-container, receives delegation).
+ $this->assertEquals( '10px', $site_title['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $site_title['email_attrs']['padding-right'] );
+
+ // Content group: transparent (wraps post-content, delegates further).
+ $this->assertArrayNotHasKey( 'padding-left', $content_group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $content_group['email_attrs'] );
+
+ // Post-content: no padding (post-content never gets padding).
+ $this->assertArrayNotHasKey( 'padding-left', $post_content['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $post_content['email_attrs'] );
+
+ // Footer group: gets root padding (doesn't wrap post-content).
+ $this->assertEquals( '10px', $footer_group['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $footer_group['email_attrs']['padding-right'] );
+ }
+
+ /**
+ * Test deeply nested post-content (group → group → post-content) delegates correctly
+ */
+ public function testItDelegatesThroughDeeplyNestedPostContent(): void {
+ $blocks = array(
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(),
+ 'innerBlocks' => 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 );
+ $root_group = $result[0];
+ $middle_group = $root_group['innerBlocks'][0];
+ $inner_group = $middle_group['innerBlocks'][0];
+ $post_content = $inner_group['innerBlocks'][0];
+ $user_block = $post_content['innerBlocks'][0];
+
+ // All container groups in the chain should be transparent (no padding).
+ $this->assertArrayNotHasKey( 'padding-left', $root_group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-left', $middle_group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-left', $inner_group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-left', $post_content['email_attrs'] );
+
+ // User block inside post-content should get root padding.
+ $this->assertEquals( '10px', $user_block['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $user_block['email_attrs']['padding-right'] );
+ }
+
+ /**
+ * Test it skips root padding for alignfull children of root-level containers
+ */
+ public function testItSkipsRootPaddingForAlignfullBlocks(): void {
+ $blocks = array(
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array( 'align' => 'full' ),
+ 'innerBlocks' => array(),
+ ),
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ ),
+ ),
+ );
+
+ $result = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+ $root_group = $result[0];
+ $alignfull_child = $root_group['innerBlocks'][0];
+ $normal_child = $root_group['innerBlocks'][1];
+
+ // Root-level group is a container — it does NOT get root padding itself.
+ $this->assertArrayNotHasKey( 'padding-left', $root_group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $root_group['email_attrs'] );
+
+ // Alignfull child should NOT get root padding (skipped for full-width).
+ $this->assertArrayNotHasKey( 'padding-left', $alignfull_child['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $alignfull_child['email_attrs'] );
+
+ // Normal child should get root padding.
+ $this->assertEquals( '10px', $normal_child['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $normal_child['email_attrs']['padding-right'] );
+ }
+
+ /**
+ * Test root-level group delegates padding to direct children but not deeper
+ */
+ public function testItDelegatesPaddingToDirectChildrenOnly(): void {
+ $blocks = array(
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/image',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ $result = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+ $root_group = $result[0];
+ $nested_paragraph = $root_group['innerBlocks'][0];
+ $nested_group = $root_group['innerBlocks'][1];
+ $deeply_nested = $nested_group['innerBlocks'][0];
+
+ // Root-level group is a container — it does NOT get root padding itself.
+ $this->assertArrayNotHasKey( 'padding-left', $root_group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $root_group['email_attrs'] );
+
+ // Direct children of root group SHOULD get root padding.
+ $this->assertEquals( '10px', $nested_paragraph['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $nested_paragraph['email_attrs']['padding-right'] );
+ $this->assertEquals( '10px', $nested_group['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $nested_group['email_attrs']['padding-right'] );
+
+ // Deeply nested blocks should NOT get root padding.
+ $this->assertArrayNotHasKey( 'padding-left', $deeply_nested['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $deeply_nested['email_attrs'] );
+ }
+
+ /**
+ * Test blocks with explicit horizontal padding skip root padding
+ */
+ public function testItSkipsRootPaddingForBlocksWithExplicitPadding(): void {
+ $blocks = array(
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ // Group with explicit 0px padding (edge-to-edge banner).
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(
+ 'style' => array(
+ 'spacing' => array(
+ 'padding' => array(
+ 'left' => '0px',
+ 'right' => '0px',
+ ),
+ ),
+ ),
+ ),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ ),
+ ),
+ // Columns with explicit 0px padding.
+ array(
+ 'blockName' => 'core/columns',
+ 'attrs' => array(
+ 'style' => array(
+ 'spacing' => array(
+ 'padding' => array(
+ 'left' => '0px',
+ 'right' => '0px',
+ ),
+ ),
+ ),
+ ),
+ 'innerBlocks' => array(),
+ ),
+ // Group with explicit 40px padding.
+ array(
+ 'blockName' => 'core/group',
+ 'attrs' => array(
+ 'style' => array(
+ 'spacing' => array(
+ 'padding' => array(
+ 'left' => '40px',
+ 'right' => '40px',
+ ),
+ ),
+ ),
+ ),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ ),
+ ),
+ // Paragraph with no explicit padding (should get root padding).
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ ),
+ ),
+ ),
+ );
+
+ $result = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+ $root_group = $result[0];
+ $banner_group = $root_group['innerBlocks'][0];
+ $banner_child = $banner_group['innerBlocks'][0];
+ $columns = $root_group['innerBlocks'][1];
+ $padded_group = $root_group['innerBlocks'][2];
+ $padded_child = $padded_group['innerBlocks'][0];
+ $plain_paragraph = $root_group['innerBlocks'][3];
+
+ // Banner group (0px padding): skips root padding, children don't get delegation.
+ $this->assertArrayNotHasKey( 'padding-left', $banner_group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $banner_group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-left', $banner_child['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $banner_child['email_attrs'] );
+
+ // Columns (0px padding): skips root padding.
+ $this->assertArrayNotHasKey( 'padding-left', $columns['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $columns['email_attrs'] );
+
+ // Padded group (40px): skips root padding, children don't get delegation.
+ $this->assertArrayNotHasKey( 'padding-left', $padded_group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $padded_group['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-left', $padded_child['email_attrs'] );
+ $this->assertArrayNotHasKey( 'padding-right', $padded_child['email_attrs'] );
+
+ // Plain paragraph (no explicit padding): gets root padding.
+ $this->assertEquals( '10px', $plain_paragraph['email_attrs']['padding-left'] );
+ $this->assertEquals( '10px', $plain_paragraph['email_attrs']['padding-right'] );
+ }
+
/**
* Test it rejects malicious values in blockGap
*/