diff --git a/backport-changelog/6.6/7097.md b/backport-changelog/6.6/7097.md new file mode 100644 index 00000000000000..e674d5ea76ba6f --- /dev/null +++ b/backport-changelog/6.6/7097.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7097 + +* https://github.com/WordPress/gutenberg/pull/63980 diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 65a5e5fe4b9578..cfe0c4d55ceed7 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -1443,9 +1443,16 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' protected function process_blocks_custom_css( $css, $selector ) { $processed_css = ''; + if ( empty( $css ) ) { + return $processed_css; + } + // Split CSS nested rules. $parts = explode( '&', $css ); foreach ( $parts as $part ) { + if ( empty( $part ) ) { + continue; + } $is_root_css = ( ! str_contains( $part, '{' ) ); if ( $is_root_css ) { // If the part doesn't contain braces, it applies to the root level. @@ -1458,11 +1465,24 @@ protected function process_blocks_custom_css( $css, $selector ) { } $nested_selector = $part[0]; $css_value = $part[1]; - $part_selector = str_starts_with( $nested_selector, ' ' ) + + /* + * Handle pseudo elements such as ::before, ::after etc. Regex will also + * capture any leading combinator such as >, +, or ~, as well as spaces. + * This allows pseudo elements as descendants e.g. `.parent ::before`. + */ + $matches = array(); + $has_pseudo_element = preg_match( '/([>+~\s]*::[a-zA-Z-]+)/', $nested_selector, $matches ); + $pseudo_part = $has_pseudo_element ? $matches[1] : ''; + $nested_selector = $has_pseudo_element ? str_replace( $pseudo_part, '', $nested_selector ) : $nested_selector; + + // Finalize selector and re-append pseudo element if required. + $part_selector = str_starts_with( $nested_selector, ' ' ) ? static::scope_selector( $selector, $nested_selector ) : static::append_to_selector( $selector, $nested_selector ); - $final_selector = ":root :where($part_selector)"; - $processed_css .= $final_selector . '{' . trim( $css_value ) . '}'; + $final_selector = ":root :where($part_selector)$pseudo_part"; + + $processed_css .= $final_selector . '{' . trim( $css_value ) . '}'; } } return $processed_css; diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index 525a8a1d53d072..3f7fb5462e377c 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -1031,11 +1031,19 @@ describe( 'global styles renderer', () => { } ); describe( 'processCSSNesting', () => { + it( 'should return empty string when supplied css is empty', () => { + expect( processCSSNesting( '', '.foo' ) ).toEqual( '' ); + } ); it( 'should return processed CSS without any nested selectors', () => { expect( processCSSNesting( 'color: red; margin: auto;', '.foo' ) ).toEqual( ':root :where(.foo){color: red; margin: auto;}' ); } ); + it( 'should return processed CSS when there are no root selectors', () => { + expect( + processCSSNesting( '&::before{color: red;}', '.foo' ) + ).toEqual( ':root :where(.foo)::before{color: red;}' ); + } ); it( 'should return processed CSS with nested selectors', () => { expect( processCSSNesting( @@ -1049,21 +1057,21 @@ describe( 'global styles renderer', () => { it( 'should return processed CSS with pseudo elements', () => { expect( processCSSNesting( - 'color: red; margin: auto; &::before{color: blue;} & ::before{color: green;} &.one::before{color: yellow;} & .two::before{color: purple;}', + 'color: red; margin: auto; &::before{color: blue;} & ::before{color: green;} &.one::before{color: yellow;} & .two::before{color: purple;} & > ::before{color: darkseagreen;}', '.foo' ) ).toEqual( - ':root :where(.foo){color: red; margin: auto;}:root :where(.foo::before){color: blue;}:root :where(.foo ::before){color: green;}:root :where(.foo.one::before){color: yellow;}:root :where(.foo .two::before){color: purple;}' + ':root :where(.foo){color: red; margin: auto;}:root :where(.foo)::before{color: blue;}:root :where(.foo) ::before{color: green;}:root :where(.foo.one)::before{color: yellow;}:root :where(.foo .two)::before{color: purple;}:root :where(.foo) > ::before{color: darkseagreen;}' ); } ); it( 'should return processed CSS with multiple root selectors', () => { expect( processCSSNesting( - 'color: red; margin: auto; &.one{color: blue;} & .two{color: green;} &::before{color: yellow;} & ::before{color: purple;} &.three::before{color: orange;} & .four::before{color: skyblue;}', + 'color: red; margin: auto; &.one{color: blue;} & .two{color: green;} &::before{color: yellow;} & ::before{color: purple;} &.three::before{color: orange;} & .four::before{color: skyblue;} & > ::before{color: darkseagreen;}', '.foo, .bar' ) ).toEqual( - ':root :where(.foo, .bar){color: red; margin: auto;}:root :where(.foo.one, .bar.one){color: blue;}:root :where(.foo .two, .bar .two){color: green;}:root :where(.foo::before, .bar::before){color: yellow;}:root :where(.foo ::before, .bar ::before){color: purple;}:root :where(.foo.three::before, .bar.three::before){color: orange;}:root :where(.foo .four::before, .bar .four::before){color: skyblue;}' + ':root :where(.foo, .bar){color: red; margin: auto;}:root :where(.foo.one, .bar.one){color: blue;}:root :where(.foo .two, .bar .two){color: green;}:root :where(.foo, .bar)::before{color: yellow;}:root :where(.foo, .bar) ::before{color: purple;}:root :where(.foo.three, .bar.three)::before{color: orange;}:root :where(.foo .four, .bar .four)::before{color: skyblue;}:root :where(.foo, .bar) > ::before{color: darkseagreen;}' ); } ); } ); 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 63d6a2fb22dd7a..69adf221900a47 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 @@ -1327,9 +1327,17 @@ function updateConfigWithSeparator( config ) { export function processCSSNesting( css, blockSelector ) { let processedCSS = ''; + if ( ! css || css.trim() === '' ) { + return processedCSS; + } + // Split CSS nested rules. const parts = css.split( '&' ); parts.forEach( ( part ) => { + if ( ! part || part.trim() === '' ) { + return; + } + const isRootCss = ! part.includes( '{' ); if ( isRootCss ) { // If the part doesn't contain braces, it applies to the root level. @@ -1342,11 +1350,32 @@ export function processCSSNesting( css, blockSelector ) { } const [ nestedSelector, cssValue ] = splittedPart; - const combinedSelector = nestedSelector.startsWith( ' ' ) - ? scopeSelector( blockSelector, nestedSelector ) - : appendToSelector( blockSelector, nestedSelector ); - processedCSS += `:root :where(${ combinedSelector }){${ cssValue.trim() }}`; + // Handle pseudo elements such as ::before, ::after, etc. Regex will also + // capture any leading combinator such as >, +, or ~, as well as spaces. + // This allows pseudo elements as descendants e.g. `.parent ::before`. + const matches = nestedSelector.match( /([>+~\s]*::[a-zA-Z-]+)/ ); + const pseudoPart = matches ? matches[ 1 ] : ''; + const withoutPseudoElement = matches + ? nestedSelector.replace( pseudoPart, '' ).trim() + : nestedSelector.trim(); + + let combinedSelector; + if ( withoutPseudoElement === '' ) { + // Only contained a pseudo element to use the block selector to form + // the final `:root :where()` selector. + combinedSelector = blockSelector; + } else { + // If the nested selector is a descendant of the block scope it with the + // block selector. Otherwise append it to the block selector. + combinedSelector = nestedSelector.startsWith( ' ' ) + ? scopeSelector( blockSelector, withoutPseudoElement ) + : appendToSelector( blockSelector, withoutPseudoElement ); + } + + // Build final rule, re-adding any pseudo element outside the `:where()` + // to maintain valid CSS selector. + processedCSS += `:root :where(${ combinedSelector })${ pseudoPart }{${ cssValue.trim() }}`; } } ); return processedCSS; diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 2f76ec7a5d4739..36b8218785f216 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -5004,6 +5004,13 @@ public function test_process_blocks_custom_css( $input, $expected ) { public function data_process_blocks_custom_css() { return array( // Simple CSS without any nested selectors. + 'empty css' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => '', + ), + 'expected' => '', + ), 'no nested selectors' => array( 'input' => array( 'selector' => '.foo', @@ -5011,6 +5018,13 @@ public function data_process_blocks_custom_css() { ), 'expected' => ':root :where(.foo){color: red; margin: auto;}', ), + 'no root styles' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => '&::before{color: red;}', + ), + 'expected' => ':root :where(.foo)::before{color: red;}', + ), // CSS with nested selectors. 'with nested selector' => array( 'input' => array( @@ -5025,7 +5039,7 @@ public function data_process_blocks_custom_css() { 'selector' => '.foo', 'css' => 'color: red; margin: auto; &::before{color: blue;} & ::before{color: green;} &.one::before{color: yellow;} & .two::before{color: purple;}', ), - 'expected' => ':root :where(.foo){color: red; margin: auto;}:root :where(.foo::before){color: blue;}:root :where(.foo ::before){color: green;}:root :where(.foo.one::before){color: yellow;}:root :where(.foo .two::before){color: purple;}', + 'expected' => ':root :where(.foo){color: red; margin: auto;}:root :where(.foo)::before{color: blue;}:root :where(.foo) ::before{color: green;}:root :where(.foo.one)::before{color: yellow;}:root :where(.foo .two)::before{color: purple;}', ), // CSS with multiple root selectors. 'with multiple root selectors' => array( @@ -5033,7 +5047,7 @@ public function data_process_blocks_custom_css() { 'selector' => '.foo, .bar', 'css' => 'color: red; margin: auto; &.one{color: blue;} & .two{color: green;} &::before{color: yellow;} & ::before{color: purple;} &.three::before{color: orange;} & .four::before{color: skyblue;}', ), - 'expected' => ':root :where(.foo, .bar){color: red; margin: auto;}:root :where(.foo.one, .bar.one){color: blue;}:root :where(.foo .two, .bar .two){color: green;}:root :where(.foo::before, .bar::before){color: yellow;}:root :where(.foo ::before, .bar ::before){color: purple;}:root :where(.foo.three::before, .bar.three::before){color: orange;}:root :where(.foo .four::before, .bar .four::before){color: skyblue;}', + 'expected' => ':root :where(.foo, .bar){color: red; margin: auto;}:root :where(.foo.one, .bar.one){color: blue;}:root :where(.foo .two, .bar .two){color: green;}:root :where(.foo, .bar)::before{color: yellow;}:root :where(.foo, .bar) ::before{color: purple;}:root :where(.foo.three, .bar.three)::before{color: orange;}:root :where(.foo .four, .bar .four)::before{color: skyblue;}', ), ); }