From 0fcc09f939230e12550c000cee1cb0f8d7ffc980 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 15 Dec 2025 13:08:47 +1100 Subject: [PATCH 1/8] Enhancement: Add support for `display` property in CSS filtering and implement block visibility based on breakpoints. This update introduces the `display` property to the `safecss_filter_attr` function, enhancing CSS filtering capabilities. Additionally, it implements breakpoint visibility support in block rendering, allowing blocks to be hidden or shown based on defined breakpoints. Corresponding tests have been added to ensure functionality. See #64414. --- .../block-supports/block-visibility.php | 113 +++++++- src/wp-includes/kses.php | 2 + .../tests/block-supports/block-visibility.php | 242 +++++++++++++++++- tests/phpunit/tests/kses.php | 34 +++ 4 files changed, 388 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index 523536cf56e1e..d8c0dbae839a4 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -10,6 +10,7 @@ * Render nothing if the block is hidden. * * @since 6.9.0 + * @since 7.0.0 Added support for breakpoint visibility. * @access private * * @param string $block_content Rendered block content. @@ -23,10 +24,120 @@ function wp_render_block_visibility_support( $block_content, $block ) { return $block_content; } - if ( isset( $block['attrs']['metadata']['blockVisibility'] ) && false === $block['attrs']['metadata']['blockVisibility'] ) { + $block_visibility = $block['attrs']['metadata']['blockVisibility'] ?? null; + + if ( false === $block_visibility ) { return ''; } + if ( is_array( $block_visibility ) && ! empty( $block_visibility ) ) { + /* + * Breakpoints definitions are in several places in WordPress packages. + * The following are taken from: https://github.com/WordPress/gutenberg/blob/trunk/packages/base-styles/_breakpoints.scss + * The array is in a future, potential JSON format, and will be centralized + * as the feature is developed. + */ + $breakpoints = array( + 'mobile' => array( + 'max' => '599px', + ), + 'tablet' => array( + 'min' => '600px', + 'max' => '959px', + ), + 'desktop' => array( + 'min' => '960px', + ), + ); + + /* + * Build media queries from breakpoint definitions. + * Could be absorbed into the style engine, + * as well as classname building, and declaration of the display property, if required. + */ + $breakpoint_queries = array(); + foreach ( $breakpoints as $name => $values ) { + $query_parts = array(); + if ( isset( $values['min'] ) ) { + $query_parts[] = '(min-width: ' . $values['min'] . ')'; + } + if ( isset( $values['max'] ) ) { + $query_parts[] = '(max-width: ' . $values['max'] . ')'; + } + if ( ! empty( $query_parts ) ) { + $breakpoint_queries[ $name ] = '@media ' . implode( ' and ', $query_parts ); + } + } + + $hidden_on = array(); + + // Collect which breakpoints the block is hidden on (only known breakpoints). + foreach ( $block_visibility as $breakpoint => $is_visible ) { + if ( false === $is_visible && isset( $breakpoint_queries[ $breakpoint ] ) ) { + $hidden_on[] = $breakpoint; + } + } + + // If no breakpoints have visibility set to false, return unchanged. + if ( empty( $hidden_on ) ) { + return $block_content; + } + + // If the block is hidden on all breakpoints, return empty string. + if ( count( $hidden_on ) === count( $breakpoint_queries ) ) { + return ''; + } + + // Generate a unique class name based on which breakpoints are hidden. + sort( $hidden_on ); + + // Sanitize breakpoint names for use in HTML class attribute. + $sanitized_hidden_on = array_map( 'sanitize_html_class', $hidden_on ); + $sanitized_hidden_on = array_filter( $sanitized_hidden_on ); + + // If all breakpoint names were invalid after sanitization, return unchanged. + if ( empty( $sanitized_hidden_on ) ) { + return $block_content; + } + + $visibility_class = 'wp-block-hidden-' . implode( '-', $sanitized_hidden_on ); + + // Generate CSS rules for each hidden breakpoint. + $css_rules = array(); + + foreach ( $hidden_on as $breakpoint ) { + if ( isset( $breakpoint_queries[ $breakpoint ] ) ) { + $css_rules[] = array( + 'selector' => '.' . $visibility_class, + 'declarations' => array( + 'display' => 'none !important', + ), + 'rules_group' => $breakpoint_queries[ $breakpoint ], + ); + } + } + + // Use the style engine to enqueue the CSS. + if ( ! empty( $css_rules ) ) { + wp_style_engine_get_stylesheet_from_css_rules( + $css_rules, + array( + 'context' => 'block-supports', + 'prettify' => false, + ) + ); + + // Add the visibility class to the block content. + if ( ! empty( $block_content ) ) { + $processor = new WP_HTML_Tag_Processor( $block_content ); + if ( $processor->next_tag() ) { + $processor->add_class( $visibility_class ); + $block_content = $processor->get_updated_html(); + } + } + } + } + return $block_content; } diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 1d77491c299f1..fc0db1891656e 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2708,6 +2708,8 @@ function safecss_filter_attr( $css, $deprecated = '' ) { 'column-span', 'column-width', + 'display', + 'color', 'filter', 'font', diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index b73e8a1ede09e..ea36120d79de0 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -61,7 +61,7 @@ private function register_visibility_block_with_support( $block_name, $supports * @ticket 64061 */ public function test_block_visibility_support_hides_block_when_visibility_false() { - $block_type = $this->register_visibility_block_with_support( + $this->register_visibility_block_with_support( 'test/visibility-block', array( 'visibility' => true ) ); @@ -88,7 +88,7 @@ public function test_block_visibility_support_hides_block_when_visibility_false( * @ticket 64061 */ public function test_block_visibility_support_shows_block_when_support_not_opted_in() { - $block_type = $this->register_visibility_block_with_support( + $this->register_visibility_block_with_support( 'test/visibility-block', array( 'visibility' => false ) ); @@ -107,4 +107,242 @@ public function test_block_visibility_support_shows_block_when_support_not_opted $this->assertSame( $block_content, $result, 'Block content should remain unchanged when blockVisibility support is not opted in.' ); } + + /* + * @ticket 64414 + */ + public function test_block_visibility_support_no_visibility_attribute() { + $this->register_visibility_block_with_support( + 'test/block-visibility-none', + array( 'visibility' => true ) + ); + + $block = array( + 'blockName' => 'test/block-visibility-none', + 'attrs' => array(), + ); + + $block_content = '
Test content
'; + $result = wp_render_block_visibility_support( $block_content, $block ); + + $this->assertSame( $block_content, $result ); + } + + /* + * @ticket 64414 + */ + public function test_block_visibility_support_generated_css_with_display_none() { + $this->register_visibility_block_with_support( + 'test/css-generation', + array( 'visibility' => true ) + ); + + $block = array( + 'blockName' => 'test/css-generation', + 'attrs' => array( + 'metadata' => array( + 'blockVisibility' => array( + 'mobile' => false, + ), + ), + ), + ); + + $block_content = '
Test content
'; + wp_render_block_visibility_support( $block_content, $block ); + + $stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports' ); + + $this->assertStringContainsString( 'display:none!important', str_replace( ' ', '', $stylesheet ), 'display:none!important should be in the CSS' ); + $this->assertStringContainsString( '.wp-block-hidden-mobile', $stylesheet, 'Stylesheet should contain the visibility class' ); + $this->assertStringContainsString( '@media', $stylesheet, 'Stylesheet should contain media query' ); + } + + /* + * @ticket 64414 + */ + public function test_block_visibility_support_generated_css_with_mobile_breakpoint() { + $this->register_visibility_block_with_support( + 'test/responsive-mobile', + array( 'visibility' => true ) + ); + + $block = array( + 'blockName' => 'test/responsive-mobile', + 'attrs' => array( + 'metadata' => array( + 'blockVisibility' => array( + 'mobile' => false, + ), + ), + ), + ); + + $block_content = '
Test content
'; + $result = wp_render_block_visibility_support( $block_content, $block ); + + $this->assertStringContainsString( 'wp-block-hidden-mobile', $result, 'Block should have the visibility class for the mobile breakpoint.' ); + } + + /* + * @ticket 64414 + */ + public function test_block_visibility_support_generated_css_with_multiple_breakpoints() { + $this->register_visibility_block_with_support( + 'test/responsive-multiple', + array( 'visibility' => true ) + ); + + $block = array( + 'blockName' => 'test/responsive-multiple', + 'attrs' => array( + 'metadata' => array( + 'blockVisibility' => array( + 'mobile' => false, + 'desktop' => false, + ), + ), + ), + ); + + $block_content = '
Test content
'; + $result = wp_render_block_visibility_support( $block_content, $block ); + + $this->assertStringContainsString( 'wp-block-hidden-desktop-mobile', $result, 'Block should have the visibility class for both breakpoints (sorted alphabetically).' ); + } + + /* + * @ticket 64414 + */ + public function test_block_visibility_support_generated_css_with_tablet_breakpoint() { + $this->register_visibility_block_with_support( + 'test/responsive-tablet', + array( 'visibility' => true ) + ); + + $block = array( + 'blockName' => 'test/responsive-tablet', + 'attrs' => array( + 'metadata' => array( + 'blockVisibility' => array( + 'tablet' => false, + ), + ), + ), + ); + + $block_content = '
Test content
'; + $result = wp_render_block_visibility_support( $block_content, $block ); + + $this->assertStringContainsString( 'existing-class', $result, 'Block should have the existing class.' ); + $this->assertStringContainsString( 'wp-block-hidden-tablet', $result, 'Block should have the visibility class for the tablet breakpoint.' ); + } + + /* + * @ticket 64414 + */ + public function test_block_visibility_support_generated_css_with_all_breakpoints_visible() { + $this->register_visibility_block_with_support( + 'test/responsive-all-visible', + array( 'visibility' => true ) + ); + + $block = array( + 'blockName' => 'test/responsive-all-visible', + 'attrs' => array( + 'metadata' => array( + 'blockVisibility' => array( + 'mobile' => true, + 'tablet' => true, + 'desktop' => true, + ), + ), + ), + ); + + $block_content = '
Test content
'; + $result = wp_render_block_visibility_support( $block_content, $block ); + + $this->assertSame( $block_content, $result, 'Block content should remain unchanged when all breakpoints are visible.' ); + } + + /* + * @ticket 64414 + */ + public function test_block_visibility_support_generated_css_with_empty_object() { + $this->register_visibility_block_with_support( + 'test/responsive-empty', + array( 'visibility' => true ) + ); + + $block = array( + 'blockName' => 'test/responsive-empty', + 'attrs' => array( + 'metadata' => array( + 'blockVisibility' => array(), + ), + ), + ); + + $block_content = '
Test content
'; + $result = wp_render_block_visibility_support( $block_content, $block ); + + $this->assertSame( $block_content, $result, 'Block content should remain unchanged when there is no visibility object.' ); + } + + /* + * @ticket 64414 + */ + public function test_block_visibility_support_generated_css_with_unknown_breakpoints_ignored() { + $this->register_visibility_block_with_support( + 'test/responsive-unknown-breakpoints', + array( 'visibility' => true ) + ); + + $block = array( + 'blockName' => 'test/responsive-unknown-breakpoints', + 'attrs' => array( + 'metadata' => array( + 'blockVisibility' => array( + 'mobile' => false, + 'unknownBreak' => false, + 'largeScreen' => false, + ), + ), + ), + ); + + $block_content = '
Test content
'; + $result = wp_render_block_visibility_support( $block_content, $block ); + + $this->assertStringContainsString( 'wp-block-hidden-mobile', $result, 'Block should have the visibility class for the mobile breakpoint.' ); + $this->assertStringNotContainsString( 'unknownBreak', $result, 'Unknown breakpoints should not appear in the class name.' ); + $this->assertStringNotContainsString( 'largeScreen', $result, 'Large screen breakpoints should not appear in the class name.' ); + } + + /* + * @ticket 64414 + */ + public function test_block_visibility_support_generated_css_with_empty_content() { + $this->register_visibility_block_with_support( + 'test/empty-content', + array( 'visibility' => true ) + ); + + $block = array( + 'blockName' => 'test/empty-content', + 'attrs' => array( + 'metadata' => array( + 'blockVisibility' => array( + 'mobile' => false, + ), + ), + ), + ); + + $block_content = ''; + $result = wp_render_block_visibility_support( $block_content, $block ); + + $this->assertSame( '', $result, 'Block content should be empty when there is no content.' ); + } } diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php index 3384a6f137e81..5c8e0974fb4aa 100644 --- a/tests/phpunit/tests/kses.php +++ b/tests/phpunit/tests/kses.php @@ -999,6 +999,7 @@ public function test_wp_kses_attr_no_attributes_allowed_with_false() { * @ticket 56122 * @ticket 58551 * @ticket 60132 + * @ticket 64414 * * @dataProvider data_safecss_filter_attr * @@ -1435,6 +1436,39 @@ public function data_safecss_filter_attr() { 'css' => 'opacity: 10', 'expected' => 'opacity: 10', ), + // `display` introduced in 7.0.0. + array( + 'css' => 'display: none', + 'expected' => 'display: none', + ), + array( + 'css' => 'display: block', + 'expected' => 'display: block', + ), + array( + 'css' => 'display: inline', + 'expected' => 'display: inline', + ), + array( + 'css' => 'display: inline-block', + 'expected' => 'display: inline-block', + ), + array( + 'css' => 'display: inline-flex', + 'expected' => 'display: inline-flex', + ), + array( + 'css' => 'display: inline-grid', + 'expected' => 'display: inline-grid', + ), + array( + 'css' => 'display: table', + 'expected' => 'display: table', + ), + array( + 'css' => 'display: flex', + 'expected' => 'display: flex', + ), ); } From c6a56ddf0db7b65add7e4140ed39e59a719d10e9 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 17 Dec 2025 13:41:32 +1100 Subject: [PATCH 2/8] removing unnecessary sanitization and improving class name generation --- .../block-supports/block-visibility.php | 37 ++++++------------- .../tests/block-supports/block-visibility.php | 30 --------------- 2 files changed, 11 insertions(+), 56 deletions(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index d8c0dbae839a4..51494fab51090 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -83,41 +83,27 @@ function wp_render_block_visibility_support( $block_content, $block ) { return $block_content; } - // If the block is hidden on all breakpoints, return empty string. + // If the block is hidden on all breakpoints, do not render the block. if ( count( $hidden_on ) === count( $breakpoint_queries ) ) { return ''; } - // Generate a unique class name based on which breakpoints are hidden. + // Maintain consistent order of breakpoints for class name generation. sort( $hidden_on ); - // Sanitize breakpoint names for use in HTML class attribute. - $sanitized_hidden_on = array_map( 'sanitize_html_class', $hidden_on ); - $sanitized_hidden_on = array_filter( $sanitized_hidden_on ); - - // If all breakpoint names were invalid after sanitization, return unchanged. - if ( empty( $sanitized_hidden_on ) ) { - return $block_content; - } - - $visibility_class = 'wp-block-hidden-' . implode( '-', $sanitized_hidden_on ); - - // Generate CSS rules for each hidden breakpoint. - $css_rules = array(); + $visibility_class = 'wp-block-hidden-' . implode( '-', $hidden_on ); + $css_rules = array(); foreach ( $hidden_on as $breakpoint ) { - if ( isset( $breakpoint_queries[ $breakpoint ] ) ) { - $css_rules[] = array( - 'selector' => '.' . $visibility_class, - 'declarations' => array( - 'display' => 'none !important', - ), - 'rules_group' => $breakpoint_queries[ $breakpoint ], - ); - } + $css_rules[] = array( + 'selector' => '.' . $visibility_class, + 'declarations' => array( + 'display' => 'none !important', + ), + 'rules_group' => $breakpoint_queries[ $breakpoint ], + ); } - // Use the style engine to enqueue the CSS. if ( ! empty( $css_rules ) ) { wp_style_engine_get_stylesheet_from_css_rules( $css_rules, @@ -127,7 +113,6 @@ function wp_render_block_visibility_support( $block_content, $block ) { ) ); - // Add the visibility class to the block content. if ( ! empty( $block_content ) ) { $processor = new WP_HTML_Tag_Processor( $block_content ); if ( $processor->next_tag() ) { diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index ea36120d79de0..838e800c782bc 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -128,36 +128,6 @@ public function test_block_visibility_support_no_visibility_attribute() { $this->assertSame( $block_content, $result ); } - /* - * @ticket 64414 - */ - public function test_block_visibility_support_generated_css_with_display_none() { - $this->register_visibility_block_with_support( - 'test/css-generation', - array( 'visibility' => true ) - ); - - $block = array( - 'blockName' => 'test/css-generation', - 'attrs' => array( - 'metadata' => array( - 'blockVisibility' => array( - 'mobile' => false, - ), - ), - ), - ); - - $block_content = '
Test content
'; - wp_render_block_visibility_support( $block_content, $block ); - - $stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports' ); - - $this->assertStringContainsString( 'display:none!important', str_replace( ' ', '', $stylesheet ), 'display:none!important should be in the CSS' ); - $this->assertStringContainsString( '.wp-block-hidden-mobile', $stylesheet, 'Stylesheet should contain the visibility class' ); - $this->assertStringContainsString( '@media', $stylesheet, 'Stylesheet should contain media query' ); - } - /* * @ticket 64414 */ From 1e1770c87bc8a251c22f745d5da5ffc01327d617 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 17 Dec 2025 13:46:05 +1100 Subject: [PATCH 3/8] This commit introduces a new test to verify that block content is empty when all visibility breakpoints (mobile, tablet, desktop) are set to false. --- .../tests/block-supports/block-visibility.php | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index 838e800c782bc..2bd54b2fc7364 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -236,6 +236,34 @@ public function test_block_visibility_support_generated_css_with_all_breakpoints $this->assertSame( $block_content, $result, 'Block content should remain unchanged when all breakpoints are visible.' ); } + /* + * @ticket 64414 + */ + public function test_block_visibility_support_generated_css_with_all_breakpoints_hidden() { + $this->register_visibility_block_with_support( + 'test/viewport-all-hidden', + array( 'visibility' => true ) + ); + + $block = array( + 'blockName' => 'test/viewport-all-hidden', + 'attrs' => array( + 'metadata' => array( + 'blockVisibility' => array( + 'mobile' => false, + 'tablet' => false, + 'desktop' => false, + ), + ), + ), + ); + + $block_content = '
Test content
'; + $result = wp_render_block_visibility_support( $block_content, $block ); + + $this->assertSame( '', $result, 'Block content should be empty when all breakpoints are hidden.' ); + } + /* * @ticket 64414 */ @@ -257,7 +285,7 @@ public function test_block_visibility_support_generated_css_with_empty_object() $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertSame( $block_content, $result, 'Block content should remain unchanged when there is no visibility object.' ); + $this->assertSame( $block_content, $result, 'Block content should remain unchanged when blockVisibility is an empty array.' ); } /* From 5cef77cf209042c5df43ce1210519c5ba4871f6f Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 23 Dec 2025 12:29:08 +1100 Subject: [PATCH 4/8] Sync with https://github.com/WordPress/gutenberg/pull/73994/ --- .../block-supports/block-visibility.php | 72 ++++++++--- .../tests/block-supports/block-visibility.php | 115 ++++++++++++++---- 2 files changed, 141 insertions(+), 46 deletions(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index 51494fab51090..e39213723e061 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -7,7 +7,7 @@ */ /** - * Render nothing if the block is hidden. + * Render nothing if the block is hidden, or add viewport visibility styles. * * @since 6.9.0 * @since 7.0.0 Added support for breakpoint visibility. @@ -36,17 +36,26 @@ function wp_render_block_visibility_support( $block_content, $block ) { * The following are taken from: https://github.com/WordPress/gutenberg/blob/trunk/packages/base-styles/_breakpoints.scss * The array is in a future, potential JSON format, and will be centralized * as the feature is developed. + * + * Breakpoints as array items are defined sequentially. The first item's size is the max value. + * Each subsequent item's min is calc(previous size + 1px), and its size is the max. + * The last item's min is previous size plus 1px, and it has no max. */ $breakpoints = array( - 'mobile' => array( - 'max' => '599px', + array( + 'name' => 'Mobile', + 'slug' => 'mobile', + 'size' => '599px', ), - 'tablet' => array( - 'min' => '600px', - 'max' => '959px', + array( + 'name' => 'Tablet', + 'slug' => 'tablet', + 'size' => '959px', ), - 'desktop' => array( - 'min' => '960px', + array( + 'name' => 'Desktop', + 'slug' => 'desktop', + 'size' => '960px', ), ); @@ -56,17 +65,29 @@ function wp_render_block_visibility_support( $block_content, $block ) { * as well as classname building, and declaration of the display property, if required. */ $breakpoint_queries = array(); - foreach ( $breakpoints as $name => $values ) { + $previous_size = null; + foreach ( $breakpoints as $index => $breakpoint ) { + $slug = $breakpoint['slug']; + $size = $breakpoint['size']; $query_parts = array(); - if ( isset( $values['min'] ) ) { - $query_parts[] = '(min-width: ' . $values['min'] . ')'; - } - if ( isset( $values['max'] ) ) { - $query_parts[] = '(max-width: ' . $values['max'] . ')'; + + // First item: max = size. + if ( 0 === $index ) { + $query_parts[] = '(max-width: ' . $size . ')'; + } elseif ( count( $breakpoints ) - 1 === $index ) { + // Last item: min = calc(previous size + 1px), no max. + $query_parts[] = '(min-width: calc(' . $previous_size . ' + 1px))'; + } else { + // Middle items: min = calc(previous size + 1px), max = size. + $query_parts[] = '(min-width: calc(' . $previous_size . ' + 1px))'; + $query_parts[] = '(max-width: ' . $size . ')'; } + if ( ! empty( $query_parts ) ) { - $breakpoint_queries[ $name ] = '@media ' . implode( ' and ', $query_parts ); + $breakpoint_queries[ $slug ] = '@media ' . implode( ' and ', $query_parts ); } + + $previous_size = $size; } $hidden_on = array(); @@ -83,7 +104,12 @@ function wp_render_block_visibility_support( $block_content, $block ) { return $block_content; } - // If the block is hidden on all breakpoints, do not render the block. + /* + * If the block is hidden on all breakpoints, + * do not render the block. If these values ever become user-defined, + * we might need to output the CSS regardless of the breakpoint count. + * For example, if there is one breakpoint defined and it's hidden. + */ if ( count( $hidden_on ) === count( $breakpoint_queries ) ) { return ''; } @@ -91,11 +117,17 @@ function wp_render_block_visibility_support( $block_content, $block ) { // Maintain consistent order of breakpoints for class name generation. sort( $hidden_on ); - $visibility_class = 'wp-block-hidden-' . implode( '-', $hidden_on ); - $css_rules = array(); + $css_rules = array(); + $class_names = array(); foreach ( $hidden_on as $breakpoint ) { - $css_rules[] = array( + /* + * If these values ever become user-defined, + * they should be sanitized and kebab-cased. + */ + $visibility_class = 'wp-block-hidden-' . $breakpoint; + $class_names[] = $visibility_class; + $css_rules[] = array( 'selector' => '.' . $visibility_class, 'declarations' => array( 'display' => 'none !important', @@ -116,7 +148,7 @@ function wp_render_block_visibility_support( $block_content, $block ) { if ( ! empty( $block_content ) ) { $processor = new WP_HTML_Tag_Processor( $block_content ); if ( $processor->next_tag() ) { - $processor->add_class( $visibility_class ); + $processor->add_class( implode( ' ', $class_names ) ); $block_content = $processor->get_updated_html(); } } diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index 2bd54b2fc7364..9c9fe63a95def 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -125,7 +125,7 @@ public function test_block_visibility_support_no_visibility_attribute() { $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertSame( $block_content, $result ); + $this->assertSame( $block_content, $result, 'Block content should remain unchanged when no visibility attribute is present.' ); } /* @@ -133,12 +133,12 @@ public function test_block_visibility_support_no_visibility_attribute() { */ public function test_block_visibility_support_generated_css_with_mobile_breakpoint() { $this->register_visibility_block_with_support( - 'test/responsive-mobile', + 'test/viewport-mobile', array( 'visibility' => true ) ); $block = array( - 'blockName' => 'test/responsive-mobile', + 'blockName' => 'test/viewport-mobile', 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( @@ -152,23 +152,64 @@ public function test_block_visibility_support_generated_css_with_mobile_breakpoi $result = wp_render_block_visibility_support( $block_content, $block ); $this->assertStringContainsString( 'wp-block-hidden-mobile', $result, 'Block should have the visibility class for the mobile breakpoint.' ); + + $actual_stylesheet = gutenberg_style_engine_get_stylesheet_from_context( 'block-supports' ); + + $this->assertSame( + '@media (max-width: 599px){.wp-block-hidden-mobile{display:none !important;}}', + $actual_stylesheet, + 'CSS should contain mobile visibility rule' + ); } /* * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_multiple_breakpoints() { + public function test_block_visibility_support_generated_css_with_tablet_breakpoint() { $this->register_visibility_block_with_support( - 'test/responsive-multiple', + 'test/viewport-tablet', array( 'visibility' => true ) ); $block = array( - 'blockName' => 'test/responsive-multiple', + 'blockName' => 'test/viewport-tablet', + 'attrs' => array( + 'metadata' => array( + 'blockVisibility' => array( + 'tablet' => false, + ), + ), + ), + ); + + $block_content = '
Test content
'; + $result = wp_render_block_visibility_support( $block_content, $block ); + + $this->assertStringContainsString( 'class="existing-class wp-block-hidden-tablet"', $result, 'Block should have the existing class and the visibility class for the tablet breakpoint in the class attribute.' ); + + $actual_stylesheet = gutenberg_style_engine_get_stylesheet_from_context( 'block-supports' ); + + $this->assertSame( + '@media (min-width: calc(599px + 1px)) and (max-width: 959px){.wp-block-hidden-tablet{display:none !important;}}', + $actual_stylesheet, + 'CSS should contain tablet visibility rule' + ); + } + + /* + * @ticket 64414 + */ + public function test_block_visibility_support_generated_css_with_desktop_breakpoint() { + $this->register_visibility_block_with_support( + 'test/viewport-desktop', + array( 'visibility' => true ) + ); + + $block = array( + 'blockName' => 'test/viewport-desktop', 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( - 'mobile' => false, 'desktop' => false, ), ), @@ -178,34 +219,54 @@ public function test_block_visibility_support_generated_css_with_multiple_breakp $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertStringContainsString( 'wp-block-hidden-desktop-mobile', $result, 'Block should have the visibility class for both breakpoints (sorted alphabetically).' ); + $this->assertStringContainsString( 'class="wp-block-hidden-desktop"', $result, 'Block should have the visibility class for the desktop breakpoint in the class attribute.' ); + + $actual_stylesheet = gutenberg_style_engine_get_stylesheet_from_context( 'block-supports' ); + + $this->assertSame( + '@media (min-width: calc(959px + 1px)){.wp-block-hidden-desktop{display:none !important;}}', + $actual_stylesheet, + 'CSS should contain desktop visibility rule' + ); } /* * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_tablet_breakpoint() { + public function test_block_visibility_support_generated_css_with_multiple_breakpoints() { $this->register_visibility_block_with_support( - 'test/responsive-tablet', + 'test/viewport-multiple', array( 'visibility' => true ) ); $block = array( - 'blockName' => 'test/responsive-tablet', + 'blockName' => 'test/viewport-multiple', 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( - 'tablet' => false, + 'mobile' => false, + 'desktop' => false, ), ), ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertStringContainsString( 'existing-class', $result, 'Block should have the existing class.' ); - $this->assertStringContainsString( 'wp-block-hidden-tablet', $result, 'Block should have the visibility class for the tablet breakpoint.' ); + $this->assertStringContainsString( + 'class="wp-block-hidden-desktop wp-block-hidden-mobile"', + $result, + 'Block should have both visibility classes in the class attribute' + ); + + $actual_stylesheet = gutenberg_style_engine_get_stylesheet_from_context( 'block-supports' ); + + $this->assertSame( + '@media (min-width: calc(959px + 1px)){.wp-block-hidden-desktop{display:none !important;}}@media (max-width: 599px){.wp-block-hidden-mobile{display:none !important;}}', + $actual_stylesheet, + 'CSS should contain both visibility rules' + ); } /* @@ -213,12 +274,12 @@ public function test_block_visibility_support_generated_css_with_tablet_breakpoi */ public function test_block_visibility_support_generated_css_with_all_breakpoints_visible() { $this->register_visibility_block_with_support( - 'test/responsive-all-visible', + 'test/viewport-all-visible', array( 'visibility' => true ) ); $block = array( - 'blockName' => 'test/responsive-all-visible', + 'blockName' => 'test/viewport-all-visible', 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( @@ -269,12 +330,12 @@ public function test_block_visibility_support_generated_css_with_all_breakpoints */ public function test_block_visibility_support_generated_css_with_empty_object() { $this->register_visibility_block_with_support( - 'test/responsive-empty', + 'test/viewport-empty', array( 'visibility' => true ) ); $block = array( - 'blockName' => 'test/responsive-empty', + 'blockName' => 'test/viewport-empty', 'attrs' => array( 'metadata' => array( 'blockVisibility' => array(), @@ -293,12 +354,12 @@ public function test_block_visibility_support_generated_css_with_empty_object() */ public function test_block_visibility_support_generated_css_with_unknown_breakpoints_ignored() { $this->register_visibility_block_with_support( - 'test/responsive-unknown-breakpoints', + 'test/viewport-unknown-breakpoints', array( 'visibility' => true ) ); $block = array( - 'blockName' => 'test/responsive-unknown-breakpoints', + 'blockName' => 'test/viewport-unknown-breakpoints', 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( @@ -313,9 +374,11 @@ public function test_block_visibility_support_generated_css_with_unknown_breakpo $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertStringContainsString( 'wp-block-hidden-mobile', $result, 'Block should have the visibility class for the mobile breakpoint.' ); - $this->assertStringNotContainsString( 'unknownBreak', $result, 'Unknown breakpoints should not appear in the class name.' ); - $this->assertStringNotContainsString( 'largeScreen', $result, 'Large screen breakpoints should not appear in the class name.' ); + $this->assertStringContainsString( + 'class="wp-block-hidden-mobile"', + $result, + 'Block should have the visibility class for the mobile breakpoint in the class attribute' + ); } /* @@ -323,12 +386,12 @@ public function test_block_visibility_support_generated_css_with_unknown_breakpo */ public function test_block_visibility_support_generated_css_with_empty_content() { $this->register_visibility_block_with_support( - 'test/empty-content', + 'test/viewport-empty-content', array( 'visibility' => true ) ); $block = array( - 'blockName' => 'test/empty-content', + 'blockName' => 'test/viewport-empty-content', 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( From 3645a8844a11a90e26791061da804dd04cb3be78 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 23 Dec 2025 12:40:51 +1100 Subject: [PATCH 5/8] whoops --- tests/phpunit/tests/block-supports/block-visibility.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index 9c9fe63a95def..63d03bcb51a89 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -153,7 +153,7 @@ public function test_block_visibility_support_generated_css_with_mobile_breakpoi $this->assertStringContainsString( 'wp-block-hidden-mobile', $result, 'Block should have the visibility class for the mobile breakpoint.' ); - $actual_stylesheet = gutenberg_style_engine_get_stylesheet_from_context( 'block-supports' ); + $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports' ); $this->assertSame( '@media (max-width: 599px){.wp-block-hidden-mobile{display:none !important;}}', @@ -187,7 +187,7 @@ public function test_block_visibility_support_generated_css_with_tablet_breakpoi $this->assertStringContainsString( 'class="existing-class wp-block-hidden-tablet"', $result, 'Block should have the existing class and the visibility class for the tablet breakpoint in the class attribute.' ); - $actual_stylesheet = gutenberg_style_engine_get_stylesheet_from_context( 'block-supports' ); + $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports' ); $this->assertSame( '@media (min-width: calc(599px + 1px)) and (max-width: 959px){.wp-block-hidden-tablet{display:none !important;}}', @@ -221,7 +221,7 @@ public function test_block_visibility_support_generated_css_with_desktop_breakpo $this->assertStringContainsString( 'class="wp-block-hidden-desktop"', $result, 'Block should have the visibility class for the desktop breakpoint in the class attribute.' ); - $actual_stylesheet = gutenberg_style_engine_get_stylesheet_from_context( 'block-supports' ); + $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports' ); $this->assertSame( '@media (min-width: calc(959px + 1px)){.wp-block-hidden-desktop{display:none !important;}}', @@ -260,7 +260,7 @@ public function test_block_visibility_support_generated_css_with_multiple_breakp 'Block should have both visibility classes in the class attribute' ); - $actual_stylesheet = gutenberg_style_engine_get_stylesheet_from_context( 'block-supports' ); + $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports' ); $this->assertSame( '@media (min-width: calc(959px + 1px)){.wp-block-hidden-desktop{display:none !important;}}@media (max-width: 599px){.wp-block-hidden-mobile{display:none !important;}}', From f29105c01fcaf0e6904d14a157206e99b8a9b75d Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 23 Dec 2025 13:25:40 +1100 Subject: [PATCH 6/8] Update block visibility tests to disable CSS prettification in stylesheet retrieval. This change ensures that the generated styles for mobile, tablet, and desktop breakpoints are returned without additional formatting, allowing for accurate assertions in the tests. --- tests/phpunit/tests/block-supports/block-visibility.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index 63d03bcb51a89..3920749ed1291 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -153,7 +153,7 @@ public function test_block_visibility_support_generated_css_with_mobile_breakpoi $this->assertStringContainsString( 'wp-block-hidden-mobile', $result, 'Block should have the visibility class for the mobile breakpoint.' ); - $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports' ); + $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); $this->assertSame( '@media (max-width: 599px){.wp-block-hidden-mobile{display:none !important;}}', @@ -187,7 +187,7 @@ public function test_block_visibility_support_generated_css_with_tablet_breakpoi $this->assertStringContainsString( 'class="existing-class wp-block-hidden-tablet"', $result, 'Block should have the existing class and the visibility class for the tablet breakpoint in the class attribute.' ); - $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports' ); + $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); $this->assertSame( '@media (min-width: calc(599px + 1px)) and (max-width: 959px){.wp-block-hidden-tablet{display:none !important;}}', @@ -221,7 +221,7 @@ public function test_block_visibility_support_generated_css_with_desktop_breakpo $this->assertStringContainsString( 'class="wp-block-hidden-desktop"', $result, 'Block should have the visibility class for the desktop breakpoint in the class attribute.' ); - $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports' ); + $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); $this->assertSame( '@media (min-width: calc(959px + 1px)){.wp-block-hidden-desktop{display:none !important;}}', @@ -260,7 +260,7 @@ public function test_block_visibility_support_generated_css_with_multiple_breakp 'Block should have both visibility classes in the class attribute' ); - $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports' ); + $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); $this->assertSame( '@media (min-width: calc(959px + 1px)){.wp-block-hidden-desktop{display:none !important;}}@media (max-width: 599px){.wp-block-hidden-mobile{display:none !important;}}', From 2835818b1c6695a4fc8c1882b0e06a9f2809836f Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 7 Jan 2026 10:28:28 +1100 Subject: [PATCH 7/8] Adjusted max-width values from 599px to 479px to match breakpoints.scss values --- src/wp-includes/block-supports/block-visibility.php | 2 +- tests/phpunit/tests/block-supports/block-visibility.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index e39213723e061..c24828cfacb12 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -45,7 +45,7 @@ function wp_render_block_visibility_support( $block_content, $block ) { array( 'name' => 'Mobile', 'slug' => 'mobile', - 'size' => '599px', + 'size' => '479px', ), array( 'name' => 'Tablet', diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index 3920749ed1291..5076d90f75e97 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -156,7 +156,7 @@ public function test_block_visibility_support_generated_css_with_mobile_breakpoi $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); $this->assertSame( - '@media (max-width: 599px){.wp-block-hidden-mobile{display:none !important;}}', + '@media (max-width: 479px){.wp-block-hidden-mobile{display:none !important;}}', $actual_stylesheet, 'CSS should contain mobile visibility rule' ); @@ -190,7 +190,7 @@ public function test_block_visibility_support_generated_css_with_tablet_breakpoi $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); $this->assertSame( - '@media (min-width: calc(599px + 1px)) and (max-width: 959px){.wp-block-hidden-tablet{display:none !important;}}', + '@media (min-width: calc(479px + 1px)) and (max-width: 959px){.wp-block-hidden-tablet{display:none !important;}}', $actual_stylesheet, 'CSS should contain tablet visibility rule' ); @@ -263,7 +263,7 @@ public function test_block_visibility_support_generated_css_with_multiple_breakp $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); $this->assertSame( - '@media (min-width: calc(959px + 1px)){.wp-block-hidden-desktop{display:none !important;}}@media (max-width: 599px){.wp-block-hidden-mobile{display:none !important;}}', + '@media (min-width: calc(959px + 1px)){.wp-block-hidden-desktop{display:none !important;}}@media (max-width: 479px){.wp-block-hidden-mobile{display:none !important;}}', $actual_stylesheet, 'CSS should contain both visibility rules' ); From e0e4e07fbcea4d95819c1a87758b3343862c4e5c Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 8 Jan 2026 00:09:03 +1100 Subject: [PATCH 8/8] Update block visibility breakpoints for mobile and tablet sizes to align with design specifications. Adjust corresponding unit tests to reflect the new breakpoint values. --- src/wp-includes/block-supports/block-visibility.php | 4 ++-- tests/phpunit/tests/block-supports/block-visibility.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index c24828cfacb12..10105460c798a 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -45,12 +45,12 @@ function wp_render_block_visibility_support( $block_content, $block ) { array( 'name' => 'Mobile', 'slug' => 'mobile', - 'size' => '479px', + 'size' => '480px', ), array( 'name' => 'Tablet', 'slug' => 'tablet', - 'size' => '959px', + 'size' => '782px', ), array( 'name' => 'Desktop', diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index 5076d90f75e97..bb1af38f530bc 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -156,7 +156,7 @@ public function test_block_visibility_support_generated_css_with_mobile_breakpoi $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); $this->assertSame( - '@media (max-width: 479px){.wp-block-hidden-mobile{display:none !important;}}', + '@media (max-width: 480px){.wp-block-hidden-mobile{display:none !important;}}', $actual_stylesheet, 'CSS should contain mobile visibility rule' ); @@ -190,7 +190,7 @@ public function test_block_visibility_support_generated_css_with_tablet_breakpoi $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); $this->assertSame( - '@media (min-width: calc(479px + 1px)) and (max-width: 959px){.wp-block-hidden-tablet{display:none !important;}}', + '@media (min-width: calc(480px + 1px)) and (max-width: 782px){.wp-block-hidden-tablet{display:none !important;}}', $actual_stylesheet, 'CSS should contain tablet visibility rule' ); @@ -224,7 +224,7 @@ public function test_block_visibility_support_generated_css_with_desktop_breakpo $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); $this->assertSame( - '@media (min-width: calc(959px + 1px)){.wp-block-hidden-desktop{display:none !important;}}', + '@media (min-width: calc(782px + 1px)){.wp-block-hidden-desktop{display:none !important;}}', $actual_stylesheet, 'CSS should contain desktop visibility rule' ); @@ -263,7 +263,7 @@ public function test_block_visibility_support_generated_css_with_multiple_breakp $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); $this->assertSame( - '@media (min-width: calc(959px + 1px)){.wp-block-hidden-desktop{display:none !important;}}@media (max-width: 479px){.wp-block-hidden-mobile{display:none !important;}}', + '@media (min-width: calc(782px + 1px)){.wp-block-hidden-desktop{display:none !important;}}@media (max-width: 480px){.wp-block-hidden-mobile{display:none !important;}}', $actual_stylesheet, 'CSS should contain both visibility rules' );