diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php
index 523536cf56e1e..756e0500418f4 100644
--- a/src/wp-includes/block-supports/block-visibility.php
+++ b/src/wp-includes/block-supports/block-visibility.php
@@ -7,9 +7,10 @@
*/
/**
- * 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 viewport visibility.
* @access private
*
* @param string $block_content Rendered block content.
@@ -23,10 +24,126 @@ 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 ) ) {
+ $viewport_config = $block_visibility['viewport'] ?? null;
+
+ if ( ! is_array( $viewport_config ) || empty( $viewport_config ) ) {
+ return $block_content;
+ }
+ /*
+ * 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.
+ *
+ * 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.
+ */
+ $viewport_sizes = array(
+ array(
+ 'name' => 'Mobile',
+ 'slug' => 'mobile',
+ 'size' => '480px',
+ ),
+ array(
+ 'name' => 'Tablet',
+ 'slug' => 'tablet',
+ 'size' => '782px',
+ ),
+ array(
+ 'name' => 'Desktop',
+ 'slug' => 'desktop',
+ /*
+ * 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`.
+ * 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.
+ */
+ ),
+ );
+
+ /*
+ * 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.
+ */
+ $viewport_media_queries = array();
+ $previous_size = null;
+ foreach ( $viewport_sizes as $index => $viewport_size ) {
+ // First item: width <= size.
+ if ( 0 === $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[ $viewport_size['slug'] ] = "@media (width > $previous_size)";
+ } else {
+ // Middle items: previous size < width <= size.
+ $viewport_media_queries[ $viewport_size['slug'] ] = "@media ({$previous_size} < width <= {$viewport_size['size']})";
+ }
+
+ $previous_size = $viewport_size['size'] ?? null;
+ }
+
+ $hidden_on = array();
+
+ // Collect which viewport the block is hidden on (only known viewport sizes).
+ 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;
+ }
+ }
+
+ // If no viewport sizes have visibility set to false, return unchanged.
+ if ( empty( $hidden_on ) ) {
+ return $block_content;
+ }
+
+ // Maintain consistent order of viewport sizes for class name generation.
+ sort( $hidden_on );
+
+ $css_rules = array();
+ $class_names = array();
+
+ 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-' . $hidden_viewport_size;
+ $class_names[] = $visibility_class;
+ $css_rules[] = array(
+ 'selector' => '.' . $visibility_class,
+ 'declarations' => array(
+ 'display' => 'none !important',
+ ),
+ 'rules_group' => $viewport_media_queries[ $hidden_viewport_size ],
+ );
+ }
+
+ 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();
+ }
+ }
+ }
+
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..dd116472ba1f4 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,319 @@ 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, 'Block content should remain unchanged when no visibility attribute is present.' );
+ }
+
+ /*
+ * @ticket 64414
+ */
+ public function test_block_visibility_support_generated_css_with_mobile_viewport_size() {
+ $this->register_visibility_block_with_support(
+ 'test/viewport-mobile',
+ array( 'visibility' => true )
+ );
+
+ $block = array(
+ 'blockName' => 'test/viewport-mobile',
+ 'attrs' => array(
+ 'metadata' => array(
+ 'blockVisibility' => array(
+ 'viewport' => 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.' );
+
+ $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
+
+ $this->assertSame(
+ '@media (width <= 480px){.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_tablet_viewport_size() {
+ $this->register_visibility_block_with_support(
+ 'test/viewport-tablet',
+ array( 'visibility' => true )
+ );
+
+ $block = array(
+ 'blockName' => 'test/viewport-tablet',
+ 'attrs' => array(
+ 'metadata' => array(
+ 'blockVisibility' => array(
+ 'viewport' => 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 = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
+
+ $this->assertSame(
+ '@media (480px < width <= 782px){.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(
+ 'viewport' => array(
+ 'desktop' => false,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ $block_content = 'Test content
';
+ $result = wp_render_block_visibility_support( $block_content, $block );
+
+ $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', array( 'prettify' => false ) );
+
+ $this->assertSame(
+ '@media (width > 782px){.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_two_viewport_sizes() {
+ $this->register_visibility_block_with_support(
+ 'test/viewport-two',
+ array( 'visibility' => true )
+ );
+
+ $block = array(
+ 'blockName' => 'test/viewport-two',
+ 'attrs' => array(
+ 'metadata' => array(
+ 'blockVisibility' => array(
+ 'viewport' => array(
+ 'mobile' => false,
+ 'desktop' => false,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ $block_content = 'Test content
';
+ $result = wp_render_block_visibility_support( $block_content, $block );
+
+ $this->assertStringContainsString(
+ 'class="wp-block-hidden-desktop wp-block-hidden-mobile"',
+ $result,
+ 'Block should have both visibility classes in the class attribute'
+ );
+
+ $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
+
+ $this->assertSame(
+ '@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 desktop and mobile visibility rules'
+ );
+ }
+
+ /*
+ * @ticket 64414
+ */
+ 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 )
+ );
+
+ $block = array(
+ 'blockName' => 'test/viewport-all-visible',
+ 'attrs' => array(
+ 'metadata' => array(
+ 'blockVisibility' => array(
+ 'viewport' => 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_all_viewport_sizes_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(
+ 'viewport' => array(
+ 'mobile' => false,
+ 'tablet' => false,
+ 'desktop' => false,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ $block_content = 'Test content
';
+ $result = wp_render_block_visibility_support( $block_content, $block );
+
+ $this->assertSame( 'Test content
', $result, 'Block content should have the visibility classes for all viewport sizes in the class attribute.' );
+ }
+
+ /*
+ * @ticket 64414
+ */
+ public function test_block_visibility_support_generated_css_with_empty_object() {
+ $this->register_visibility_block_with_support(
+ 'test/viewport-empty',
+ array( 'visibility' => true )
+ );
+
+ $block = array(
+ 'blockName' => 'test/viewport-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 blockVisibility is an empty array.' );
+ }
+
+ /*
+ * @ticket 64414
+ */
+ public function test_block_visibility_support_generated_css_with_unknown_viewport_sizes_ignored() {
+ $this->register_visibility_block_with_support(
+ 'test/viewport-unknown-viewport-sizes',
+ array( 'visibility' => true )
+ );
+
+ $block = array(
+ 'blockName' => 'test/viewport-unknown-viewport-sizes',
+ 'attrs' => array(
+ 'metadata' => array(
+ 'blockVisibility' => array(
+ 'viewport' => array(
+ 'mobile' => false,
+ 'unknownBreak' => false,
+ 'largeScreen' => false,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ $block_content = 'Test content
';
+ $result = wp_render_block_visibility_support( $block_content, $block );
+
+ $this->assertStringContainsString(
+ 'class="wp-block-hidden-mobile"',
+ $result,
+ 'Block should have the visibility class for the mobile breakpoint in the class attribute'
+ );
+ }
+
+ /*
+ * @ticket 64414
+ */
+ public function test_block_visibility_support_generated_css_with_empty_content() {
+ $this->register_visibility_block_with_support(
+ 'test/viewport-empty-content',
+ array( 'visibility' => true )
+ );
+
+ $block = array(
+ 'blockName' => 'test/viewport-empty-content',
+ 'attrs' => array(
+ 'metadata' => array(
+ 'blockVisibility' => array(
+ 'viewport' => 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',
+ ),
);
}