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', + ), ); }