diff --git a/php-transformer/composer.json b/php-transformer/composer.json index f8427df2a..d322639a6 100644 --- a/php-transformer/composer.json +++ b/php-transformer/composer.json @@ -351,6 +351,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 096331484..581eb18be 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); @@ -1203,6 +1206,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 c4da97187..6a7934339 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": "617d6414e9b6c7ea4b1449c99e3032204fadbed75f2928c5fdaaa926d54dba4a", + "expected_sha256": "106e074f127f6f0447ad54bdae1ace6d467caf6cbe642625c1fb6ed05e4d9064", "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": "d593dbb9d4e959252de18457290377da1fb9ccd0b8a840a2cd747d9349c0916d", + "expected_sha256": "fd454ce10af80c82684627e69547bdb36f3fcb030dfcc3e5a2eac6e3c160fd94", "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 ec2ea0e3e..f530dd381 100644 --- a/php-transformer/tools/capture-evidence-equivalence/README.md +++ b/php-transformer/tools/capture-evidence-equivalence/README.md @@ -66,6 +66,12 @@ replay by case: source trigger buttons with `data-blocks-engine-tab-trigger`, so their box styling carries onto native tab buttons. The projected HTML gains the marker and the tab-list carrier class hash changes in `173a88a2`, `56cfc02b`, `5ad295f6`, `5af9c0c1`, `6657cdac`, `6a79dc90`, `88ac82df`, `8a631c3c`, `8c22a321`, `983aa909`, `9885239b`, `ca0b670d`, `d28fa845`, `d76cc245`, `e970e58c`, `f159a7b6`. +- **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.