From 34d0b850c81169b394be2d521d08958077200512 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 15 Dec 2025 13:08:47 +1100 Subject: [PATCH 01/14] 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 fd489c06c71f2..6583af826c18a 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2626,6 +2626,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 6dc5cedc601eb980f2a41ac9a36a41c4f9ce7257 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 17 Dec 2025 13:41:32 +1100 Subject: [PATCH 02/14] 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 95cd611089639cb05a689c5417d24a592ee10150 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 17 Dec 2025 13:46:05 +1100 Subject: [PATCH 03/14] 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 4f06d6752e56e7d98973a47aad27a545cb779399 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 23 Dec 2025 12:29:08 +1100 Subject: [PATCH 04/14] 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 281836695e4b24038f5a3ad254630cbf79042f92 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 23 Dec 2025 12:40:51 +1100 Subject: [PATCH 05/14] 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 f1541d74fb8c5da990ecc755f0ce81df82e0548c Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 23 Dec 2025 13:25:40 +1100 Subject: [PATCH 06/14] 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 173a10174b745054cbe3e6bef0a21d626b6ecb27 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 7 Jan 2026 10:28:28 +1100 Subject: [PATCH 07/14] 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 cf06337b67b5d448a7c3b7d08af536ea396d7a43 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 8 Jan 2026 00:09:03 +1100 Subject: [PATCH 08/14] 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 0383b98f3715c05e0c3731e7ed64d2d2ba194534 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 12 Jan 2026 18:37:47 +1100 Subject: [PATCH 09/14] 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 5a0e25b03542eb0618f2df5e579ef516640fbd53 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 16 Jan 2026 12:12:30 +1100 Subject: [PATCH 10/14] 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 3885f81adb18b035487bd8cd70973189a88bef03 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 16 Jan 2026 12:17:29 +1100 Subject: [PATCH 11/14] 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 dd04b3bb3b6f0f3f6e43300cdd5a3f0e3945a90d Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 16 Jan 2026 13:02:04 +1100 Subject: [PATCH 12/14] 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 6252a817deff64fd69d698e7fbbcd8011fb8ee75 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 16 Jan 2026 14:59:02 +1100 Subject: [PATCH 13/14] 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 0ca2322e4874ab4ec6e16c7ee8b2b18f5b0c970d Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 16 Jan 2026 18:13:03 +1100 Subject: [PATCH 14/14] 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();