From ab5fb904455bdcc2e1f5ce62a5f8d5ce3a53696a Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 15 Dec 2025 13:08:47 +1100 Subject: [PATCH 01/15] 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 c71453177ddc2..ed2f96503ac27 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2631,6 +2631,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 304bdaf582ea01e51aaa758248e4694978d9e557 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 17 Dec 2025 13:41:32 +1100 Subject: [PATCH 02/15] 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 ec481d8e33ec118de44bd462f941b0100e1be240 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 17 Dec 2025 13:46:05 +1100 Subject: [PATCH 03/15] 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 7c7023ef9a2a37aab4d2738e55c41376e5a79545 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 23 Dec 2025 12:29:08 +1100 Subject: [PATCH 04/15] 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 ceeaf71ce7265cd0dd587fb17d54da6aeede7066 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 23 Dec 2025 12:40:51 +1100 Subject: [PATCH 05/15] 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 936f28777f00604d8cad79efc625032bc749e5c3 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 23 Dec 2025 13:25:40 +1100 Subject: [PATCH 06/15] 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 eda4aaa32916274afb3b17715b7943aab6225565 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 7 Jan 2026 10:28:28 +1100 Subject: [PATCH 07/15] 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 cf6e561cb6cc11569534cef00ef14545f0c97c26 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 8 Jan 2026 00:09:03 +1100 Subject: [PATCH 08/15] 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' ); From 8cc2c924411ee7ef3aed910613b4757f75238dfa Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 12 Jan 2026 18:37:47 +1100 Subject: [PATCH 09/15] Refactor block visibility media queries to use CSS range syntax for improved clarity. Update unit tests to reflect changes in breakpoint definitions for mobile, tablet, and desktop visibility rules. --- .../block-supports/block-visibility.php | 54 ++++++++----------- .../tests/block-supports/block-visibility.php | 10 ++-- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index 10105460c798a..49d45c0f04a6a 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -38,8 +38,8 @@ function wp_render_block_visibility_support( $block_content, $block ) { * 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. + * Each subsequent item starts after the previous size (using > operator), and its size is the max. + * The last item starts after the previous size (using > operator), and it has no max. */ $breakpoints = array( array( @@ -60,31 +60,25 @@ function wp_render_block_visibility_support( $block_content, $block ) { ); /* - * Build media queries from breakpoint definitions. + * Build media queries from breakpoint definitions using the CSS range syntax. * Could be absorbed into the style engine, * as well as classname building, and declaration of the display property, if required. */ $breakpoint_queries = array(); $previous_size = null; foreach ( $breakpoints as $index => $breakpoint ) { - $slug = $breakpoint['slug']; - $size = $breakpoint['size']; - $query_parts = array(); + $slug = $breakpoint['slug']; + $size = $breakpoint['size']; - // First item: max = size. + // First item: width <= size. if ( 0 === $index ) { - $query_parts[] = '(max-width: ' . $size . ')'; + $breakpoint_queries[ $slug ] = "@media (width <= $size)"; } elseif ( count( $breakpoints ) - 1 === $index ) { - // Last item: min = calc(previous size + 1px), no max. - $query_parts[] = '(min-width: calc(' . $previous_size . ' + 1px))'; + // Last item: width > previous size. + $breakpoint_queries[ $slug ] = "@media (width > $previous_size)"; } 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[ $slug ] = '@media ' . implode( ' and ', $query_parts ); + // Middle items: previous size < width <= size. + $breakpoint_queries[ $slug ] = "@media ($previous_size < width <= $size)"; } $previous_size = $size; @@ -136,21 +130,19 @@ function wp_render_block_visibility_support( $block_content, $block ) { ); } - if ( ! empty( $css_rules ) ) { - wp_style_engine_get_stylesheet_from_css_rules( - $css_rules, - array( - 'context' => 'block-supports', - 'prettify' => false, - ) - ); + wp_style_engine_get_stylesheet_from_css_rules( + $css_rules, + array( + 'context' => 'block-supports', + 'prettify' => false, + ) + ); - if ( ! empty( $block_content ) ) { - $processor = new WP_HTML_Tag_Processor( $block_content ); - if ( $processor->next_tag() ) { - $processor->add_class( implode( ' ', $class_names ) ); - $block_content = $processor->get_updated_html(); - } + if ( ! empty( $block_content ) ) { + $processor = new WP_HTML_Tag_Processor( $block_content ); + if ( $processor->next_tag() ) { + $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 bb1af38f530bc..27526269feade 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: 480px){.wp-block-hidden-mobile{display:none !important;}}', + '@media (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(480px + 1px)) and (max-width: 782px){.wp-block-hidden-tablet{display:none !important;}}', + '@media (480px < 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(782px + 1px)){.wp-block-hidden-desktop{display:none !important;}}', + '@media (width > 782px){.wp-block-hidden-desktop{display:none !important;}}', $actual_stylesheet, 'CSS should contain desktop visibility rule' ); @@ -263,9 +263,9 @@ 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(782px + 1px)){.wp-block-hidden-desktop{display:none !important;}}@media (max-width: 480px){.wp-block-hidden-mobile{display:none !important;}}', + '@media (width > 782px){.wp-block-hidden-desktop{display:none !important;}}@media (width <= 480px){.wp-block-hidden-mobile{display:none !important;}}', $actual_stylesheet, - 'CSS should contain both visibility rules' + 'CSS should contain desktop and mobile visibility rules' ); } From 941c4f4843e9b2aa441900464d497b0f482300dc Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 16 Jan 2026 12:12:30 +1100 Subject: [PATCH 10/15] Sync https://github.com/WordPress/gutenberg/pull/74602 --- .../block-supports/block-visibility.php | 19 +++--- .../tests/block-supports/block-visibility.php | 58 ++++++++++++------- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index 49d45c0f04a6a..0d9557c895c33 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -31,6 +31,11 @@ function wp_render_block_visibility_support( $block_content, $block ) { } if ( is_array( $block_visibility ) && ! empty( $block_visibility ) ) { + $viewport_config = $block_visibility['viewport'] ?? null; + + if ( ! is_array( $viewport_config ) || empty( $viewport_config ) ) { + return $block_content; + } /* * 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 @@ -86,29 +91,29 @@ function wp_render_block_visibility_support( $block_content, $block ) { $hidden_on = array(); - // Collect which breakpoints the block is hidden on (only known breakpoints). - foreach ( $block_visibility as $breakpoint => $is_visible ) { + // Collect which viewport the block is hidden on (only known viewport sizes). + foreach ( $viewport_config 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 no viewport sizes have visibility set to false, return unchanged. if ( empty( $hidden_on ) ) { return $block_content; } /* - * If the block is hidden on all breakpoints, + * If the block is hidden on all viewport sizes, * 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. + * we might need to output the CSS regardless of the viewport size count. + * For example, if there is one viewport size defined and it's hidden. */ if ( count( $hidden_on ) === count( $breakpoint_queries ) ) { return ''; } - // Maintain consistent order of breakpoints for class name generation. + // Maintain consistent order of viewport sizes for class name generation. sort( $hidden_on ); $css_rules = array(); diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index 27526269feade..6a17bcc06ddb5 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -131,7 +131,7 @@ public function test_block_visibility_support_no_visibility_attribute() { /* * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_mobile_breakpoint() { + public function test_block_visibility_support_generated_css_with_mobile_viewport_size() { $this->register_visibility_block_with_support( 'test/viewport-mobile', array( 'visibility' => true ) @@ -142,7 +142,9 @@ public function test_block_visibility_support_generated_css_with_mobile_breakpoi 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( - 'mobile' => false, + 'viewport' => array( + 'mobile' => false, + ), ), ), ), @@ -165,7 +167,7 @@ public function test_block_visibility_support_generated_css_with_mobile_breakpoi /* * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_tablet_breakpoint() { + public function test_block_visibility_support_generated_css_with_tablet_viewport_size() { $this->register_visibility_block_with_support( 'test/viewport-tablet', array( 'visibility' => true ) @@ -176,7 +178,9 @@ public function test_block_visibility_support_generated_css_with_tablet_breakpoi 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( - 'tablet' => false, + 'viewport' => array( + 'tablet' => false, + ), ), ), ), @@ -233,7 +237,7 @@ public function test_block_visibility_support_generated_css_with_desktop_breakpo /* * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_multiple_breakpoints() { + public function test_block_visibility_support_generated_css_with_multiple_viewport_sizes() { $this->register_visibility_block_with_support( 'test/viewport-multiple', array( 'visibility' => true ) @@ -244,8 +248,10 @@ public function test_block_visibility_support_generated_css_with_multiple_breakp 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( - 'mobile' => false, - 'desktop' => false, + 'viewport' => array( + 'mobile' => false, + 'desktop' => false, + ), ), ), ), @@ -272,7 +278,7 @@ public function test_block_visibility_support_generated_css_with_multiple_breakp /* * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_all_breakpoints_visible() { + public function test_block_visibility_support_generated_css_with_all_viewport_sizes_visible() { $this->register_visibility_block_with_support( 'test/viewport-all-visible', array( 'visibility' => true ) @@ -283,9 +289,11 @@ public function test_block_visibility_support_generated_css_with_all_breakpoints 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( - 'mobile' => true, - 'tablet' => true, - 'desktop' => true, + 'viewport' => array( + 'mobile' => true, + 'tablet' => true, + 'desktop' => true, + ), ), ), ), @@ -300,7 +308,7 @@ public function test_block_visibility_support_generated_css_with_all_breakpoints /* * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_all_breakpoints_hidden() { + public function test_block_visibility_support_generated_css_with_all_viewport_sizes_hidden() { $this->register_visibility_block_with_support( 'test/viewport-all-hidden', array( 'visibility' => true ) @@ -311,9 +319,11 @@ public function test_block_visibility_support_generated_css_with_all_breakpoints 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( - 'mobile' => false, - 'tablet' => false, - 'desktop' => false, + 'viewport' => array( + 'mobile' => false, + 'tablet' => false, + 'desktop' => false, + ), ), ), ), @@ -352,20 +362,22 @@ public function test_block_visibility_support_generated_css_with_empty_object() /* * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_unknown_breakpoints_ignored() { + public function test_block_visibility_support_generated_css_with_unknown_viewport_sizes_ignored() { $this->register_visibility_block_with_support( - 'test/viewport-unknown-breakpoints', + 'test/viewport-unknown-viewport-sizes', array( 'visibility' => true ) ); $block = array( - 'blockName' => 'test/viewport-unknown-breakpoints', + 'blockName' => 'test/viewport-unknown-viewport-sizes', 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( - 'mobile' => false, - 'unknownBreak' => false, - 'largeScreen' => false, + 'viewport' => array( + 'mobile' => false, + 'unknownBreak' => false, + 'largeScreen' => false, + ), ), ), ), @@ -395,7 +407,9 @@ public function test_block_visibility_support_generated_css_with_empty_content() 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( - 'mobile' => false, + 'viewport' => array( + 'mobile' => false, + ), ), ), ), From e00e6ad99f62930425957704e31718ab3ad72093 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 16 Jan 2026 12:17:29 +1100 Subject: [PATCH 11/15] Use "viewport" instead of breakpoint to refer to metadata keys and values. --- .../block-supports/block-visibility.php | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index 0d9557c895c33..8312edf96a6a1 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -10,7 +10,7 @@ * 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. + * @since 7.0.0 Added support for viewport visibility. * @access private * * @param string $block_content Rendered block content. @@ -37,16 +37,16 @@ function wp_render_block_visibility_support( $block_content, $block ) { return $block_content; } /* - * Breakpoints definitions are in several places in WordPress packages. + * Viewport size 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 as array items are defined sequentially. The first item's size is the max value. + * Viewport sizes as array items are defined sequentially. The first item's size is the max value. * Each subsequent item starts after the previous size (using > operator), and its size is the max. * The last item starts after the previous size (using > operator), and it has no max. */ - $breakpoints = array( + $viewport_sizes = array( array( 'name' => 'Mobile', 'slug' => 'mobile', @@ -65,25 +65,25 @@ function wp_render_block_visibility_support( $block_content, $block ) { ); /* - * Build media queries from breakpoint definitions using the CSS range syntax. + * Build media queries from viewport size definitions using the CSS range syntax. * Could be absorbed into the style engine, * as well as classname building, and declaration of the display property, if required. */ - $breakpoint_queries = array(); - $previous_size = null; - foreach ( $breakpoints as $index => $breakpoint ) { - $slug = $breakpoint['slug']; - $size = $breakpoint['size']; + $viewport_media_queries = array(); + $previous_size = null; + foreach ( $viewport_sizes as $index => $viewport_size ) { + $slug = $viewport_size['slug']; + $size = $viewport_size['size']; // First item: width <= size. if ( 0 === $index ) { - $breakpoint_queries[ $slug ] = "@media (width <= $size)"; - } elseif ( count( $breakpoints ) - 1 === $index ) { + $viewport_media_queries[ $slug ] = "@media (width <= $size)"; + } elseif ( count( $viewport_sizes ) - 1 === $index ) { // Last item: width > previous size. - $breakpoint_queries[ $slug ] = "@media (width > $previous_size)"; + $viewport_media_queries[ $slug ] = "@media (width > $previous_size)"; } else { // Middle items: previous size < width <= size. - $breakpoint_queries[ $slug ] = "@media ($previous_size < width <= $size)"; + $viewport_media_queries[ $slug ] = "@media ($previous_size < width <= $size)"; } $previous_size = $size; @@ -92,9 +92,9 @@ function wp_render_block_visibility_support( $block_content, $block ) { $hidden_on = array(); // Collect which viewport the block is hidden on (only known viewport sizes). - foreach ( $viewport_config as $breakpoint => $is_visible ) { - if ( false === $is_visible && isset( $breakpoint_queries[ $breakpoint ] ) ) { - $hidden_on[] = $breakpoint; + foreach ( $viewport_config as $viewport_config_size => $is_visible ) { + if ( false === $is_visible && isset( $viewport_media_queries[ $viewport_config_size ] ) ) { + $hidden_on[] = $viewport_config_size; } } @@ -109,7 +109,7 @@ function wp_render_block_visibility_support( $block_content, $block ) { * we might need to output the CSS regardless of the viewport size count. * For example, if there is one viewport size defined and it's hidden. */ - if ( count( $hidden_on ) === count( $breakpoint_queries ) ) { + if ( count( $hidden_on ) === count( $viewport_media_queries ) ) { return ''; } @@ -119,19 +119,19 @@ function wp_render_block_visibility_support( $block_content, $block ) { $css_rules = array(); $class_names = array(); - foreach ( $hidden_on as $breakpoint ) { + foreach ( $hidden_on as $hidden_viewport_size ) { /* * If these values ever become user-defined, * they should be sanitized and kebab-cased. */ - $visibility_class = 'wp-block-hidden-' . $breakpoint; + $visibility_class = 'wp-block-hidden-' . $hidden_viewport_size; $class_names[] = $visibility_class; $css_rules[] = array( 'selector' => '.' . $visibility_class, 'declarations' => array( 'display' => 'none !important', ), - 'rules_group' => $breakpoint_queries[ $breakpoint ], + 'rules_group' => $viewport_media_queries[ $hidden_viewport_size ], ); } From 5931a77c9ff79dc592726fc8e64317536b350f72 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 16 Jan 2026 13:02:04 +1100 Subject: [PATCH 12/15] Fix broken test (wrong fixture) --- tests/phpunit/tests/block-supports/block-visibility.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index 6a17bcc06ddb5..f65376e6fe65d 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -214,7 +214,9 @@ public function test_block_visibility_support_generated_css_with_desktop_breakpo 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( - 'desktop' => false, + 'viewport' => array( + 'desktop' => false, + ), ), ), ), From 77e744378e3c05099911c6129f60afeea582924f Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 16 Jan 2026 14:59:02 +1100 Subject: [PATCH 13/15] Sync with https://github.com/WordPress/gutenberg/pull/74679 --- .../block-supports/block-visibility.php | 15 +++++---------- .../tests/block-supports/block-visibility.php | 8 ++++---- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index 8312edf96a6a1..df4b27e71ecd9 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -60,6 +60,11 @@ function wp_render_block_visibility_support( $block_content, $block ) { array( 'name' => 'Desktop', 'slug' => 'desktop', + /* + * Note: the last item in the $viewport_sizes array does not technically require a size, + * as the last item's media query is calculated using `width > previous size`. + * It's included for consistency and as a record of the "official" breakpoint size. + */ 'size' => '960px', ), ); @@ -103,16 +108,6 @@ function wp_render_block_visibility_support( $block_content, $block ) { return $block_content; } - /* - * If the block is hidden on all viewport sizes, - * do not render the block. If these values ever become user-defined, - * we might need to output the CSS regardless of the viewport size count. - * For example, if there is one viewport size defined and it's hidden. - */ - if ( count( $hidden_on ) === count( $viewport_media_queries ) ) { - return ''; - } - // Maintain consistent order of viewport sizes for class name generation. sort( $hidden_on ); diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index f65376e6fe65d..dd116472ba1f4 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -239,14 +239,14 @@ public function test_block_visibility_support_generated_css_with_desktop_breakpo /* * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_multiple_viewport_sizes() { + public function test_block_visibility_support_generated_css_with_two_viewport_sizes() { $this->register_visibility_block_with_support( - 'test/viewport-multiple', + 'test/viewport-two', array( 'visibility' => true ) ); $block = array( - 'blockName' => 'test/viewport-multiple', + 'blockName' => 'test/viewport-two', 'attrs' => array( 'metadata' => array( 'blockVisibility' => array( @@ -334,7 +334,7 @@ public function test_block_visibility_support_generated_css_with_all_viewport_si $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.' ); + $this->assertSame( '
Test content
', $result, 'Block content should have the visibility classes for all viewport sizes in the class attribute.' ); } /* From 6e4660d92565609dedba55c8ae83f513bbb09a8c Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 16 Jan 2026 18:13:03 +1100 Subject: [PATCH 14/15] Refine comments and logic in block visibility support for clarity. Update media query generation to ensure accurate handling of viewport sizes. --- .../block-supports/block-visibility.php | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index df4b27e71ecd9..756e0500418f4 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -61,11 +61,11 @@ function wp_render_block_visibility_support( $block_content, $block ) { 'name' => 'Desktop', 'slug' => 'desktop', /* - * Note: the last item in the $viewport_sizes array does not technically require a size, + * Note: the last item in the $viewport_sizes array does not technically require a 'size' key, * as the last item's media query is calculated using `width > previous size`. - * It's included for consistency and as a record of the "official" breakpoint size. + * The last item is present for validating the attribute values, and in order to indicate + * that this is the final viewport size, and to calculate the previous media query accordingly. */ - 'size' => '960px', ), ); @@ -77,21 +77,18 @@ function wp_render_block_visibility_support( $block_content, $block ) { $viewport_media_queries = array(); $previous_size = null; foreach ( $viewport_sizes as $index => $viewport_size ) { - $slug = $viewport_size['slug']; - $size = $viewport_size['size']; - // First item: width <= size. if ( 0 === $index ) { - $viewport_media_queries[ $slug ] = "@media (width <= $size)"; - } elseif ( count( $viewport_sizes ) - 1 === $index ) { + $viewport_media_queries[ $viewport_size['slug'] ] = "@media (width <= {$viewport_size['size']})"; + } elseif ( count( $viewport_sizes ) - 1 === $index && $previous_size ) { // Last item: width > previous size. - $viewport_media_queries[ $slug ] = "@media (width > $previous_size)"; + $viewport_media_queries[ $viewport_size['slug'] ] = "@media (width > $previous_size)"; } else { // Middle items: previous size < width <= size. - $viewport_media_queries[ $slug ] = "@media ($previous_size < width <= $size)"; + $viewport_media_queries[ $viewport_size['slug'] ] = "@media ({$previous_size} < width <= {$viewport_size['size']})"; } - $previous_size = $size; + $previous_size = $viewport_size['size'] ?? null; } $hidden_on = array(); From a485ead0f57781586175826d22ed2872d84209df Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Fri, 30 Jan 2026 13:17:38 +1100 Subject: [PATCH 15/15] add grid display to kses test --- tests/phpunit/tests/kses.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php index 5c8e0974fb4aa..dc01b5dfe2979 100644 --- a/tests/phpunit/tests/kses.php +++ b/tests/phpunit/tests/kses.php @@ -1469,6 +1469,10 @@ public function data_safecss_filter_attr() { 'css' => 'display: flex', 'expected' => 'display: flex', ), + array( + 'css' => 'display: grid', + 'expected' => 'display: grid', + ), ); }