From 12230ac467877e2413f57e98b7691f8bcf5c47e7 Mon Sep 17 00:00:00 2001 From: Ellen Bauer Date: Tue, 13 Oct 2026 22:22:33 +0700 Subject: [PATCH] fix(php-transformer): stack declared block menu items in core flow layout An in-flow list navigation renders its generated list as display:contents, so the items lay out in core/navigation itself. When neither the source nav nor its list declares a display, core's flex row stood in for the source's block list, and items the source stacked full width shrank to their min-width. On a Wix phone menu that cut the longer labels off. Record core's flow layout only with item evidence: every list item declares a block-level display. Unstyled menus, inline items and flex lists keep core's default. Refs #2726 Co-Authored-By: Claude Opus 5.5 --- php-transformer/composer.json | 1 + .../Patterns/NavigationPattern.php | 46 +++++++++++ .../expected.json | 18 ++-- .../navigation-stacked-block-list-layout.php | 82 +++++++++++++++++++ .../capture-evidence-equivalence/README.md | 6 ++ 5 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 php-transformer/tests/unit/navigation-stacked-block-list-layout.php diff --git a/php-transformer/composer.json b/php-transformer/composer.json index 49d97cf82..78bc1f5cc 100644 --- a/php-transformer/composer.json +++ b/php-transformer/composer.json @@ -347,6 +347,7 @@ "php tests/unit/navigation-resolved-projection-spacing.php", "php tests/unit/source-target-projection-state.php", "php tests/unit/navigation-layout-default.php", + "php tests/unit/navigation-stacked-block-list-layout.php", "php tests/unit/navigation-vertical-orientation.php", "php tests/unit/navigation-source-provenance.php", "php tests/unit/navigation-dropped-anchors.php", diff --git a/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php b/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php index 7738fe6a8..cf496d897 100644 --- a/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php +++ b/php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php @@ -252,6 +252,9 @@ public function recognize(DOMElement $element, PatternContext $context): ?Patter $navigationAttrs['layout'] = array( 'type' => 'flex', 'orientation' => 'vertical' ); } $navigationAttrs['overlayMenu'] = $this->overlayMenu($element, $navigationContext); + if ( 'never' === $navigationAttrs['overlayMenu'] && ! $label instanceof DOMElement ) { + $navigationAttrs = $this->withStackedBlockListLayout($navigationAttrs, $navigationSource, $listSource, $navigationContext); + } if ( 'mobile' === $navigationAttrs['overlayMenu'] ) { $navigationAttrs = $this->withClassName($navigationAttrs, 'blocks-engine-native-responsive-navigation'); $navigationAttrs = $this->withResponsiveToggleMarker($navigationAttrs, $element, $navigationContext); @@ -1122,6 +1125,49 @@ private function withResolvedNonFlexNavigationLayout(array $attrs, DOMElement $e return $attrs; } + /** + * Carry a source list that stacks block items onto core's flow layout. + * + * An in-flow list navigation renders its generated list as + * `display:contents`, so the items lay out in the navigation host. With no + * layout the host is core's flex row, and items the source stacked full + * width shrink to their min-width instead. When neither the host nor its + * list declares a display, the source list is a block box; the items prove + * the stack only when every one declares a block-level display. Floated + * block items still need flow: a flex row ignores the float that the + * projected item rule carries. Without item evidence core's default stays. + * + * @param array $attrs + * @return array + */ + private function withStackedBlockListLayout(array $attrs, DOMElement $host, ?DOMElement $list, ?NavigationPatternContext $navigationContext): array + { + if ( null === $navigationContext || ! $list instanceof DOMElement || is_array($attrs['layout'] ?? null) + || '' !== trim($navigationContext->resolvedDisplay($host)) + || '' !== trim($navigationContext->resolvedDisplay($list)) + ) { + return $attrs; + } + + $items = 0; + foreach ( $list->childNodes as $child ) { + if ( ! $child instanceof DOMElement || 'li' !== strtolower($child->tagName) ) { + continue; + } + $display = strtolower(trim($navigationContext->resolvedDisplay($child))); + if ( ! in_array($display, array( 'block', 'list-item', 'flow-root' ), true) ) { + return $attrs; + } + ++$items; + } + + if ( 0 < $items ) { + $attrs['layout'] = array( 'type' => 'default' ); + } + + return $attrs; + } + /** * Carry a source menu whose cascade stacks its items in a column onto * core/navigation's vertical orientation. diff --git a/php-transformer/tests/fixtures/capture-evidence-equivalence/expected.json b/php-transformer/tests/fixtures/capture-evidence-equivalence/expected.json index 100c50af6..c521c10e0 100644 --- a/php-transformer/tests/fixtures/capture-evidence-equivalence/expected.json +++ b/php-transformer/tests/fixtures/capture-evidence-equivalence/expected.json @@ -275,7 +275,7 @@ { "id": "2448c3c2ebf7c03839a1ad542beec24efd2a805c", "entry": "HtmlTransformer::transform", - "expected_sha256": "241e746902379c4decbeb3b64549c6310791c6d6535c51b83b01b83059f98224", + "expected_sha256": "71634f402ac1c35b2b58aa7a84368a05df1eefae88fd57710e8613f985781b01", "outcome": "output" }, { @@ -323,7 +323,7 @@ { "id": "2890e8f1ed5ae188e261c80d3c9a1ad1e1928cd2", "entry": "HtmlTransformer::transform", - "expected_sha256": "eb37f1652823bd47a70755937fe0632ca5d1464660cad51b999e4dabc1419163", + "expected_sha256": "3eebcdcab44579a25d3086471a45fc14df01fdbbc47b55f8143a61575e554259", "outcome": "output" }, { @@ -539,7 +539,7 @@ { "id": "448ead36f649a72e98e642cebc29f9f71af779f5", "entry": "HtmlTransformer::transform", - "expected_sha256": "631b17e61a09b144cf92acf35c1388d3cbf1542fc15e5aeb858a0dfacac142b7", + "expected_sha256": "788553d7c9d9def419dcde4bcceb82aa342e2738cf5ddc4337578d1e77aab4a9", "outcome": "output" }, { @@ -569,7 +569,7 @@ { "id": "46f79246a3c128cd0672e296d1164e4d6882750a", "entry": "HtmlTransformer::transform", - "expected_sha256": "7bd66a28531c17d5e2b7c45a491a8ab8c1acaa98f59adc4b748fbfe65afcf2b4", + "expected_sha256": "f26a1bc83d32db0bf72caffb515b27c3c8ca10e6806b70220ce8572cb31da295", "outcome": "output" }, { @@ -1349,7 +1349,7 @@ { "id": "a5a08a8615d2682ce0d38a5053bcec23199be36f", "entry": "HtmlTransformer::transform", - "expected_sha256": "7fb14b9b715633998d19b1f060d54e71396285c5e63c30103dfe33760db2c034", + "expected_sha256": "e5b0e2aff14da96082ecfcee6553949478c0a26b6591bd00f94151319390938a", "outcome": "output" }, { @@ -1439,7 +1439,7 @@ { "id": "b550beb0271acdd852382b101d2fc6653b1cb107", "entry": "HtmlTransformer::transform", - "expected_sha256": "03c14aaf5f28dc4d2bc8846e033f7b7a8d4e3e4e92b081af72582825de6f956c", + "expected_sha256": "20727b3683cd0dec389f6ddb8b48258c920462384880aaa2771bdb7c29b37f4d", "outcome": "output" }, { @@ -1607,7 +1607,7 @@ { "id": "c7fff215fed715e93aa155c4e0e5e31d7db084b1", "entry": "HtmlTransformer::transform", - "expected_sha256": "fc8db45234751aac47d0efffd24822682881715c898e077c103027fd034ad0ff", + "expected_sha256": "22d116a5a062c7379806e180be35a9a0707b2fac02bcecb20e2d1da472c1d3bd", "outcome": "output" }, { @@ -1817,7 +1817,7 @@ { "id": "e2e35d0c5c02ff06b23bc87fb59bc585201bc46f", "entry": "HtmlTransformer::transform", - "expected_sha256": "21ac153f4c03dc58a8aa26d880caed406d3a1c5eff6b433998c6a6be5356f24a", + "expected_sha256": "ae10e2c73a21de5017246eb1b17695f380c99f9a151fc48d7d31dec731980f95", "outcome": "output" }, { @@ -1937,7 +1937,7 @@ { "id": "f025cf281f15d03cf61d0c76c1f4bd7adc3b257e", "entry": "HtmlTransformer::transform", - "expected_sha256": "e91fb22dc0beafe7c96b49a1e2a7c93693305dc1d09c65380f521165d4f4de6b", + "expected_sha256": "338b39a6c253afaa0d6ecf21c3bed05ecc39b40bb8ebddf0dc47a6cea377e8c4", "outcome": "output" }, { diff --git a/php-transformer/tests/unit/navigation-stacked-block-list-layout.php b/php-transformer/tests/unit/navigation-stacked-block-list-layout.php new file mode 100644 index 000000000..cc0aecc61 --- /dev/null +++ b/php-transformer/tests/unit/navigation-stacked-block-list-layout.php @@ -0,0 +1,82 @@ +ul li{display:block}` + * with no display on the nav or the list) came out as flex items sized by their + * `min-width`, and the longer labels were cut off. Only item evidence switches + * the default: an unstyled menu, inline items, and a flex list keep + * core's row. + */ + +require dirname(__DIR__, 2) . '/vendor/autoload.php'; + +use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\HtmlTransformer; + +$failures = 0; +$passes = 0; +$assert = static function (bool $ok, string $message, string $detail = '') use (&$failures, &$passes): void { + if ( $ok ) { + ++$passes; + return; + } + ++$failures; + fwrite(STDERR, 'FAIL: ' . $message . ( '' !== $detail ? ' - ' . $detail : '' ) . PHP_EOL); +}; + +$items = ''; +foreach ( array( 'Contact Us' => '/contact', 'Annual Meetings' => '/meetings', 'News & Events' => '/events' ) as $label => $href ) { + $items .= '
  • '; +} +$layout = static function (string $css, string $scope = '') use ($items): ?array { + $body = ''; + if ( '' !== $scope ) { + $body = '
    ' + . '
    ' . $body . '

    Body

    '; + } + $serialized = (string) (( new HtmlTransformer() )->transform('' . $body, array())->toArray()['serialized_blocks'] ?? ''); + if ( 1 !== preg_match('//', $serialized, $match) ) { + return array( 'missing' => true ); + } + $attrs = json_decode($match[1], true); + + return is_array($attrs) && is_array($attrs['layout'] ?? null) ? $attrs['layout'] : null; +}; +$menu = static fn (string $scope, string $itemRule): string => $scope . '.menu{direction:ltr}' + . $scope . '.menu>ul{box-sizing:border-box;width:100%}' + . $scope . '.menu>ul li{' . $itemRule . '}' + . $scope . '.item{min-width:100px}' + . $scope . '.item .box{display:grid;height:45px}'; + +$scope = ':where([data-dla-device-document="mobile"]) '; +$assert( + array( 'type' => 'default' ) === $layout($menu($scope, 'display:block'), $scope), + 'a device-scoped menu whose items are declared block stacks in core flow', + json_encode($layout($menu($scope, 'display:block'), $scope)) +); +$assert( + array( 'type' => 'default' ) === $layout($menu('', 'display:block')), + 'the same menu without a device scope stacks in core flow' +); +$assert( + array( 'type' => 'default' ) === $layout($menu('', 'display:list-item')), + 'list-item items are block-level too' +); +$assert(null === $layout($menu('', 'display:inline-block')), 'inline-block items keep core\'s row'); +$assert( + array( 'type' => 'default' ) === $layout($menu('', 'display:block;float:left')), + 'floated block items take flow, where their projected float still applies' +); +$assert(null === $layout($menu('', 'display:block') . '.menu>ul{display:flex}'), 'a flex list keeps core\'s row'); +$assert(null === $layout('.menu{direction:ltr}'), 'a menu without item display evidence keeps core\'s default'); + +if ( 0 < $failures ) { + fwrite(STDERR, "Navigation stacked block list layout: {$failures} failed, {$passes} passed\n"); + exit(1); +} + +echo "Navigation stacked block list layout passed: {$passes} assertions\n"; diff --git a/php-transformer/tools/capture-evidence-equivalence/README.md b/php-transformer/tools/capture-evidence-equivalence/README.md index 61da2d0c9..19d2e08e0 100644 --- a/php-transformer/tools/capture-evidence-equivalence/README.md +++ b/php-transformer/tools/capture-evidence-equivalence/README.md @@ -53,6 +53,12 @@ replay by case: hashes and sizes derived from it change in `0a9c1bea`, `15082c2d`, `21e5cdbf`, `2915d21f`, `37284b0e`, `55b724ac`, `7a7af898`, `7c2d1217`, `927aa368`, `944dd63d`, `df9c24a8`, `e5ea5743`. +- **Declared block menu items stack in core flow** (#2726). A list + navigation whose items all declare a block-level display, with no display + on the nav or its list, records `"layout":{"type":"default"}`. That adds + the attribute in `46f79246` and `e2e35d0c`. The extra display lookups only + move the style-rule candidate cache counters in `2448c3c2`, `2890e8f1`, + `448ead36`, `a5a08a86`, `b550beb0`, `c7fff215` and `f025cf28`. When regenerating from the pre-move commit, re-record these cases from the checkout that introduced them, and only after checking the difference again.