From d3cc2fad009813659b69a008844f0d5711e52082 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 10 Sep 2026 20:50:48 -0400 Subject: [PATCH 1/5] fix(navigation): preserve separate responsive menus --- .../Style/GeneratedBlockStyleProjector.php | 4 +- .../Support/NavigationToggleSuppressor.php | 38 +++++++++++++++++++ php-transformer/tests/contract/run.php | 23 +++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/php-transformer/src/HtmlToBlocks/Style/GeneratedBlockStyleProjector.php b/php-transformer/src/HtmlToBlocks/Style/GeneratedBlockStyleProjector.php index d0a3d5a57..34219a5cc 100644 --- a/php-transformer/src/HtmlToBlocks/Style/GeneratedBlockStyleProjector.php +++ b/php-transformer/src/HtmlToBlocks/Style/GeneratedBlockStyleProjector.php @@ -44,7 +44,9 @@ public function applyDeclaredBlockSupport( } } } - if ( 'core/navigation' === $name && is_array($fallback['spacing'] ?? null) ) { + if ( 'core/navigation' === $name + && ! in_array('blocks-engine-list-navigation', $classes, true) + && is_array($fallback['spacing'] ?? null) ) { $declarations = $this->styleResolver->styleAttributeMapper()->serialize(array( 'spacing' => $fallback['spacing'] ))['style']; foreach ( $classes as $class ) { if ( '' !== $declarations && 'blocks-engine-list-navigation' !== $class && ! str_starts_with($class, 'blocks-engine-') ) { diff --git a/php-transformer/src/HtmlToBlocks/Support/NavigationToggleSuppressor.php b/php-transformer/src/HtmlToBlocks/Support/NavigationToggleSuppressor.php index 85751af1b..28fffb2a4 100644 --- a/php-transformer/src/HtmlToBlocks/Support/NavigationToggleSuppressor.php +++ b/php-transformer/src/HtmlToBlocks/Support/NavigationToggleSuppressor.php @@ -766,15 +766,25 @@ private function hasEquivalentSourceNavigationVariant(DOMElement $navigation): b } $navigationRoot = $this->navigationLandmarkAncestor($navigation) ?? $navigation; + // A captured details disclosure owns its summary and panel navigation. + // It remains an independent mobile surface, rather than evidence that a + // sibling desktop navigation should be replaced by Core's overlay toggle. + if ( $this->isInsideCapturedDisclosure($navigationRoot) ) { + return false; + } $signature = $this->sourceNavigationSignature($navigationRoot); if ( '' === $signature ) { return false; } + if ( $this->hasCapturedDisclosureNavigation($document, $signature) ) { + return false; + } foreach ( $document->getElementsByTagName('nav') as $candidate ) { if ( ! $candidate instanceof DOMElement || $candidate->isSameNode($navigationRoot) || $this->isProjectedNavigationSuppressed($candidate) + || $this->isInsideCapturedDisclosure($candidate) || SourceDom::elementContains($navigationRoot, $candidate) || SourceDom::elementContains($candidate, $navigationRoot) ) { @@ -790,6 +800,34 @@ private function hasEquivalentSourceNavigationVariant(DOMElement $navigation): b return false; } + private function hasCapturedDisclosureNavigation(DOMDocument $document, string $signature): bool + { + foreach ( $document->getElementsByTagName('details') as $disclosure ) { + if ( ! $disclosure instanceof DOMElement || ! $this->isCapturedDialogControl($disclosure) ) { + continue; + } + + foreach ( $disclosure->getElementsByTagName('nav') as $candidate ) { + if ( $candidate instanceof DOMElement && $signature === $this->sourceNavigationSignature($candidate) ) { + return true; + } + } + } + + return false; + } + + private function isInsideCapturedDisclosure(DOMElement $element): bool + { + for ( $node = $element; $node instanceof DOMElement; $node = $node->parentNode ) { + if ( 'details' === strtolower($node->tagName) && $this->isCapturedDialogControl($node) ) { + return true; + } + } + + return false; + } + private function hasMobileNavigationSignal(DOMElement $element): bool { for ( $node = $element; $node instanceof DOMElement && 'body' !== strtolower($node->tagName); $node = $node->parentNode ) { diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php index 26f8b40e0..31bfe33f3 100644 --- a/php-transformer/tests/contract/run.php +++ b/php-transformer/tests/contract/run.php @@ -1018,6 +1018,29 @@ public function recognize(DOMElement $element, PatternContext $context): ?Patter $capturedMobileMenuDisclosureMarkup = (string) ($capturedMobileMenuDisclosure['serialized_blocks'] ?? ''); $assert(str_contains($capturedMobileMenuDisclosureMarkup, '"overlayMenu":"never"') && ! str_contains($capturedMobileMenuDisclosureMarkup, 'blocks-engine-native-responsive-navigation'), 'navigation inside a captured native disclosure does not create a nested mobile overlay', $capturedMobileMenuDisclosureMarkup); +// Separate desktop and mobile source surfaces are not an overlay pair when the +// mobile menu keeps its own native details interaction. In particular, the +// desktop menu must retain its authored display at desktop widths instead of +// receiving the global native-responsive display:flex bridge. +$separateResponsiveMenuArtifact = ( new ArtifactCompiler() )->compile( + array( + 'entry' => 'index.html', + 'files' => array( + 'index.html' => '
' + . '
', + ), + ) +)->toArray(); +$separateResponsiveMenuMarkup = (string) ($separateResponsiveMenuArtifact['serialized_blocks'] ?? ''); +$separateResponsiveMenuCss = implode("\n", array_map(static fn (array $asset): string => 'css' === ($asset['kind'] ?? '') ? (string) ($asset['content'] ?? '') : '', $separateResponsiveMenuArtifact['assets'] ?? array())); +$assert(! str_contains($separateResponsiveMenuMarkup, 'blocks-engine-native-responsive-navigation') && 2 === substr_count($separateResponsiveMenuMarkup, '"overlayMenu":"never"'), 'a separately preserved disclosure menu does not promote its desktop companion to native responsive navigation', $separateResponsiveMenuMarkup); +$assert(str_contains($separateResponsiveMenuMarkup, '