From 408d4d987b75853bf5dc4d8fbdbd1fae32e06be9 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 29 Oct 2026 18:10:59 -0400 Subject: [PATCH] fix(php-transformer): keep a centered button's margins inside its block-flow item --- .../Style/GeneratedBlockStyleProjector.php | 21 +++++++++-- .../fixtures/centered-button-margins.html | 10 ++++++ ...erited-text-align-button-justification.php | 11 ++---- .../tools/visual-parity/package.json | 2 +- .../tests/centered-button-margins.mjs | 35 +++++++++++++++++++ 5 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 php-transformer/tests/fixtures/centered-button-margins.html create mode 100644 php-transformer/tools/visual-parity/tests/centered-button-margins.mjs diff --git a/php-transformer/src/HtmlToBlocks/Style/GeneratedBlockStyleProjector.php b/php-transformer/src/HtmlToBlocks/Style/GeneratedBlockStyleProjector.php index a3d58f763..c93fc81eb 100644 --- a/php-transformer/src/HtmlToBlocks/Style/GeneratedBlockStyleProjector.php +++ b/php-transformer/src/HtmlToBlocks/Style/GeneratedBlockStyleProjector.php @@ -284,8 +284,14 @@ public function registerNativeButtonStyleRule( // width; an intrinsic one would shrink it to its label. } elseif ( ! $hasAuthoredWidth && in_array(CssValueInspector::comparable((string) ($sourceDeclarations['display'] ?? '')), array( 'flex', 'inline-flex', 'inline-block', 'inline-grid', 'inline-table' ), true) ) { if ( 'center' === $inheritedTextAlignment ) { - $outerWrapperDeclarations[] = 'width:100%'; - $outerWrapperDeclarations[] = 'max-width:100%'; + // A flex or grid item shrinks to its content unless told to + // fill. In block flow the wrapper already spans its line, + // inside the margins the control's rules may give it; a + // 100% width would push it past them and off center. + if ( ! $this->sourceParentIsBlockContainer($sourceControl) ) { + $outerWrapperDeclarations[] = 'width:100%'; + $outerWrapperDeclarations[] = 'max-width:100%'; + } } else { $outerWrapperDeclarations[] = 'width:max-content'; $outerWrapperDeclarations[] = 'max-width:100%'; @@ -817,6 +823,17 @@ private function sourceControlFillsFlowLine(DOMElement $sourceControl, array $so return in_array($parentDisplay, array( '', 'block', 'flow-root', 'list-item' ), true); } + private function sourceParentIsBlockContainer(DOMElement $sourceControl): bool + { + $parent = $sourceControl->parentNode; + if ( ! $parent instanceof DOMElement ) { + return false; + } + $display = CssValueInspector::comparable((string) ($this->styleResolver->cssDeclarations($this->styleResolver->specificityResolvedPresentationStyle($parent))['display'] ?? '')); + + return in_array($display, array( '', 'block', 'flow-root', 'list-item', 'inline-block', 'table-cell' ), true); + } + private function sourceControlFillsGridCell(DOMElement $sourceControl, array $sourceDeclarations): bool { if ( ! in_array(CssValueInspector::comparable((string) ($sourceDeclarations['display'] ?? '')), array( 'flex', 'grid', 'table' ), true) ) { diff --git a/php-transformer/tests/fixtures/centered-button-margins.html b/php-transformer/tests/fixtures/centered-button-margins.html new file mode 100644 index 000000000..5f4a7eefa --- /dev/null +++ b/php-transformer/tests/fixtures/centered-button-margins.html @@ -0,0 +1,10 @@ + + +
+ diff --git a/php-transformer/tests/unit/inherited-text-align-button-justification.php b/php-transformer/tests/unit/inherited-text-align-button-justification.php index 826ae7291..9876512b9 100644 --- a/php-transformer/tests/unit/inherited-text-align-button-justification.php +++ b/php-transformer/tests/unit/inherited-text-align-button-justification.php @@ -6,7 +6,7 @@ * `text-align` becomes a core/buttons flex wrapper whose default main-axis * alignment is left. The inherited alignment must be restated as * layout.justifyContent or the source's centering is lost. The restated - * justification only has room to act when the wrapper fills its parent, so + * justification only has room to act when the wrapper spans its line, so * the generated wrapper rule must not shrink-wrap a centered wrapper. * * Regression for Automattic/blocks-engine#2150: the wrapper consulted only the @@ -93,8 +93,8 @@ $centeredButtons ); -// The restated justification has no room to act on a shrink-wrapped wrapper: -// the generated wrapper rule must fill the parent instead, the way the source +// The restated justification has no room to act on a shrink-wrapped wrapper. +// In block flow the flex wrapper spans its line on its own, the way the source // block the control sat in did, while the button itself still hugs its label. $centeredMarker = $controlMarkerOf($centeredButtons); $centeredCss = $cssFor($centeredOut); @@ -103,11 +103,6 @@ 'the centered control carries a generated control marker', $centeredButtons ); -$assert( - 1 === preg_match('/' . preg_quote($centeredMarker, '/') . '\.' . preg_quote($centeredMarker, '/') . '\.wp-block-buttons\{width:100%;max-width:100%\}/', $centeredCss), - 'a centered inline control fills its parent buttons wrapper instead of shrink-wrapping it', - $centeredCss -); $assert( ! preg_match('/' . preg_quote($centeredMarker, '/') . '\.' . preg_quote($centeredMarker, '/') . '\.wp-block-buttons\{width:max-content/', $centeredCss), 'the centered buttons wrapper is not pinned to max-content', diff --git a/php-transformer/tools/visual-parity/package.json b/php-transformer/tools/visual-parity/package.json index d601e952f..79da4bf27 100644 --- a/php-transformer/tools/visual-parity/package.json +++ b/php-transformer/tools/visual-parity/package.json @@ -22,7 +22,7 @@ "test:navigation-cluster-control": "node tests/navigation-cluster-trailing-control.mjs", "test:conditional-native-button": "node tests/conditional-native-button.mjs", "test:declared-body": "node tests/declared-document-role-button.mjs", - "test": "node tests/smoke.mjs && node tests/blockquote-margin-reset.mjs && node tests/synthetic-inline-image-percentage.mjs && node tests/linked-flex-item-image.mjs && node tests/inline-block-item-spacing.mjs && node tests/exact-fit-inline-flow.mjs && node tests/layout-shell-editor-dom.mjs && node tests/layout-shell-editor-geometry.mjs && node tests/layout-shell-editor-selectability.mjs && node tests/custom-video-host-editor-geometry.mjs && node tests/issue-1493-reduced-public-fixture.mjs && node tests/responsive-document-variants.mjs && node tests/image-custom-host-promotion.mjs && node tests/empty-group-editor-height.mjs && node tests/button-height-ownership.mjs && node tests/button-padding-height.mjs && node tests/positioned-button-containing-block.mjs && node tests/positioned-button-translate-once.mjs && node tests/positioned-button-shrink-to-fit.mjs && node tests/inline-control-line-box.mjs && node tests/flex-button-row-shrink-to-fit.mjs && node tests/navigation-layout-default.mjs && node tests/navigation-hover-focus-projection.mjs && node tests/navigation-overlay-duplicate-collapse.mjs && node tests/navigation-responsive-desktop-justification.mjs && node tests/navigation-overlay-source-breakpoint.mjs && node tests/navigation-open-button-source-placement.mjs && node tests/form-row-two-column-geometry.mjs && node tests/color-scheme-projection.mjs && node tests/shared-shell-route-cascade.mjs && node tests/responsive-counterpart-text-sync.mjs && node tests/positioned-carousel-visibility.mjs && node tests/authored-carousel-source-scope.mjs && node tests/empty-overlay-opacity.mjs && node tests/authored-carousel-stage-geometry.mjs && node tests/gallery-stage-controls.mjs && node tests/nested-responsive-font-size.mjs && node tests/responsive-layered-block-margin.mjs && node tests/svg-checkable-control.mjs && node tests/source-root-includes.mjs && node tests/captured-choice-group-runtime.mjs && node tests/differential-corpus.mjs", + "test": "node tests/smoke.mjs && node tests/blockquote-margin-reset.mjs && node tests/synthetic-inline-image-percentage.mjs && node tests/linked-flex-item-image.mjs && node tests/centered-button-margins.mjs && node tests/inline-block-item-spacing.mjs && node tests/exact-fit-inline-flow.mjs && node tests/layout-shell-editor-dom.mjs && node tests/layout-shell-editor-geometry.mjs && node tests/layout-shell-editor-selectability.mjs && node tests/custom-video-host-editor-geometry.mjs && node tests/issue-1493-reduced-public-fixture.mjs && node tests/responsive-document-variants.mjs && node tests/image-custom-host-promotion.mjs && node tests/empty-group-editor-height.mjs && node tests/button-height-ownership.mjs && node tests/button-padding-height.mjs && node tests/positioned-button-containing-block.mjs && node tests/positioned-button-translate-once.mjs && node tests/positioned-button-shrink-to-fit.mjs && node tests/inline-control-line-box.mjs && node tests/flex-button-row-shrink-to-fit.mjs && node tests/navigation-layout-default.mjs && node tests/navigation-hover-focus-projection.mjs && node tests/navigation-overlay-duplicate-collapse.mjs && node tests/navigation-responsive-desktop-justification.mjs && node tests/navigation-overlay-source-breakpoint.mjs && node tests/navigation-open-button-source-placement.mjs && node tests/form-row-two-column-geometry.mjs && node tests/color-scheme-projection.mjs && node tests/shared-shell-route-cascade.mjs && node tests/responsive-counterpart-text-sync.mjs && node tests/positioned-carousel-visibility.mjs && node tests/authored-carousel-source-scope.mjs && node tests/empty-overlay-opacity.mjs && node tests/authored-carousel-stage-geometry.mjs && node tests/gallery-stage-controls.mjs && node tests/nested-responsive-font-size.mjs && node tests/responsive-layered-block-margin.mjs && node tests/svg-checkable-control.mjs && node tests/source-root-includes.mjs && node tests/captured-choice-group-runtime.mjs && node tests/differential-corpus.mjs", "test:differential": "node tests/differential-corpus.mjs" }, "devDependencies": { diff --git a/php-transformer/tools/visual-parity/tests/centered-button-margins.mjs b/php-transformer/tools/visual-parity/tests/centered-button-margins.mjs new file mode 100644 index 000000000..b14472667 --- /dev/null +++ b/php-transformer/tools/visual-parity/tests/centered-button-margins.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { chromium } from 'playwright'; + +// A centered inline-block control with side margins keeps those margins +// inside its block-flow item: the item is as wide as in the source and the +// control sits at the same offset within it. +const root = new URL('../../..', import.meta.url).pathname; +const source = readFileSync(`${root}/tests/fixtures/centered-button-margins.html`, 'utf8'); +const result = JSON.parse(execFileSync('php', ['-r', 'require $argv[1] . "/vendor/autoload.php"; echo json_encode((new Automattic\\BlocksEngine\\PhpTransformer\\HtmlToBlocks\\HtmlTransformer())->transform(base64_decode($argv[2]))->toArray());', root, Buffer.from(source).toString('base64')], { encoding: 'utf8' })); +const css = result.assets.filter(asset => asset.kind === 'css').map(asset => asset.content).join('\n'); +// WordPress renders a flex layout's justifyContent as a justification class. +const blocks = result.serialized_blocks.replace(/\s*