diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 97b9c58b13df29..b23bb678fb725b 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -1138,9 +1138,27 @@ protected function process_blocks_custom_css( $css, $selector ) { // Split CSS nested rules. $parts = explode( '&', $css ); foreach ( $parts as $part ) { - $processed_css .= ( ! str_contains( $part, '{' ) ) - ? trim( $selector ) . '{' . trim( $part ) . '}' // If the part doesn't contain braces, it applies to the root level. - : trim( $selector . $part ); // Prepend the selector, which effectively replaces the "&" character. + $is_root_css = ( ! str_contains( $part, '{' ) ); + if ( $is_root_css ) { + // If the part doesn't contain braces, it applies to the root level. + $processed_css .= trim( $selector ) . '{' . trim( $part ) . '}'; + } else { + // If the part contains braces, it's a nested CSS rule. + $part = explode( '{', str_replace( '}', '', $part ) ); + if ( count( $part ) !== 2 ) { + continue; + } + $nested_selector = $part[0]; + $css_value = $part[1]; + $root_selectors = explode( ',', $selector ); + $combined_selectors = array_map( + static function( $root_selector ) use ( $nested_selector ) { + return $root_selector . $nested_selector; + }, + $root_selectors + ); + $processed_css .= implode( ',', $combined_selectors ) . '{' . trim( $css_value ) . '}'; + } } return $processed_css; } diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 1db7e46c6eb775..492b55f6262ba3 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -1127,9 +1127,26 @@ const processCSSNesting = ( css, blockSelector ) => { // Split CSS nested rules. const parts = css.split( '&' ); parts.forEach( ( part ) => { - processedCSS += ! part.includes( '{' ) - ? blockSelector + '{' + part + '}' // If the part doesn't contain braces, it applies to the root level. - : blockSelector + part; // Prepend the selector, which effectively replaces the "&" character. + const isRootCss = ! part.includes( '{' ); + if ( isRootCss ) { + // If the part doesn't contain braces, it applies to the root level. + processedCSS += `${ blockSelector }{${ part }}`; + } else { + // If the part contains braces, it's a nested CSS rule. + const splittedPart = part.replace( '}', '' ).split( '{' ); + if ( splittedPart.length !== 2 ) { + return; + } + + const [ nestedSelector, cssValue ] = splittedPart; + const rootSelectors = blockSelector.split( ',' ); + const combinedSelectors = rootSelectors.map( + ( rootSelector ) => rootSelector + nestedSelector + ); + processedCSS += `${ combinedSelectors.join( + ', ' + ) }{${ cssValue.trim() }}`; + } } ); return processedCSS; }; diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 11ec35e0925bfe..c5881b100538d5 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -2033,6 +2033,14 @@ public function data_process_blocks_custom_css() { ), 'expected' => '.foo{color: red; margin: auto;}.foo .bar{color: blue;}.foo::before{color: green;}', ), + // CSS with multiple root selectors. + 'with multiple root selectors' => array( + 'input' => array( + 'selector' => '.foo, .bar', + 'css' => 'color: red; margin: auto; & .baz{color: blue;} &::before{color: green;}', + ), + 'expected' => '.foo, .bar{color: red; margin: auto;}.foo .baz, .bar .baz{color: blue;}.foo::before, .bar::before{color: green;}', + ), ); }