Skip to content

Commit 9de97f5

Browse files
committed
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.
1 parent eda8d9d commit 9de97f5

File tree

4 files changed

+388
-3
lines changed

4 files changed

+388
-3
lines changed

src/wp-includes/block-supports/block-visibility.php

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* Render nothing if the block is hidden.
1111
*
1212
* @since 6.9.0
13+
* @since 7.0.0 Added support for breakpoint visibility.
1314
* @access private
1415
*
1516
* @param string $block_content Rendered block content.
@@ -23,10 +24,120 @@ function wp_render_block_visibility_support( $block_content, $block ) {
2324
return $block_content;
2425
}
2526

26-
if ( isset( $block['attrs']['metadata']['blockVisibility'] ) && false === $block['attrs']['metadata']['blockVisibility'] ) {
27+
$block_visibility = $block['attrs']['metadata']['blockVisibility'] ?? null;
28+
29+
if ( false === $block_visibility ) {
2730
return '';
2831
}
2932

33+
if ( is_array( $block_visibility ) && ! empty( $block_visibility ) ) {
34+
/*
35+
* Breakpoints definitions are in several places in WordPress packages.
36+
* The following are taken from: https://github.com/WordPress/gutenberg/blob/trunk/packages/base-styles/_breakpoints.scss
37+
* The array is in a future, potential JSON format, and will be centralized
38+
* as the feature is developed.
39+
*/
40+
$breakpoints = array(
41+
'mobile' => array(
42+
'max' => '599px',
43+
),
44+
'tablet' => array(
45+
'min' => '600px',
46+
'max' => '959px',
47+
),
48+
'desktop' => array(
49+
'min' => '960px',
50+
),
51+
);
52+
53+
/*
54+
* Build media queries from breakpoint definitions.
55+
* Could be absorbed into the style engine,
56+
* as well as classname building, and declaration of the display property, if required.
57+
*/
58+
$breakpoint_queries = array();
59+
foreach ( $breakpoints as $name => $values ) {
60+
$query_parts = array();
61+
if ( isset( $values['min'] ) ) {
62+
$query_parts[] = '(min-width: ' . $values['min'] . ')';
63+
}
64+
if ( isset( $values['max'] ) ) {
65+
$query_parts[] = '(max-width: ' . $values['max'] . ')';
66+
}
67+
if ( ! empty( $query_parts ) ) {
68+
$breakpoint_queries[ $name ] = '@media ' . implode( ' and ', $query_parts );
69+
}
70+
}
71+
72+
$hidden_on = array();
73+
74+
// Collect which breakpoints the block is hidden on (only known breakpoints).
75+
foreach ( $block_visibility as $breakpoint => $is_visible ) {
76+
if ( false === $is_visible && isset( $breakpoint_queries[ $breakpoint ] ) ) {
77+
$hidden_on[] = $breakpoint;
78+
}
79+
}
80+
81+
// If no breakpoints have visibility set to false, return unchanged.
82+
if ( empty( $hidden_on ) ) {
83+
return $block_content;
84+
}
85+
86+
// If the block is hidden on all breakpoints, return empty string.
87+
if ( count( $hidden_on ) === count( $breakpoint_queries ) ) {
88+
return '';
89+
}
90+
91+
// Generate a unique class name based on which breakpoints are hidden.
92+
sort( $hidden_on );
93+
94+
// Sanitize breakpoint names for use in HTML class attribute.
95+
$sanitized_hidden_on = array_map( 'sanitize_html_class', $hidden_on );
96+
$sanitized_hidden_on = array_filter( $sanitized_hidden_on );
97+
98+
// If all breakpoint names were invalid after sanitization, return unchanged.
99+
if ( empty( $sanitized_hidden_on ) ) {
100+
return $block_content;
101+
}
102+
103+
$visibility_class = 'wp-block-hidden-' . implode( '-', $sanitized_hidden_on );
104+
105+
// Generate CSS rules for each hidden breakpoint.
106+
$css_rules = array();
107+
108+
foreach ( $hidden_on as $breakpoint ) {
109+
if ( isset( $breakpoint_queries[ $breakpoint ] ) ) {
110+
$css_rules[] = array(
111+
'selector' => '.' . $visibility_class,
112+
'declarations' => array(
113+
'display' => 'none !important',
114+
),
115+
'rules_group' => $breakpoint_queries[ $breakpoint ],
116+
);
117+
}
118+
}
119+
120+
// Use the style engine to enqueue the CSS.
121+
if ( ! empty( $css_rules ) ) {
122+
wp_style_engine_get_stylesheet_from_css_rules(
123+
$css_rules,
124+
array(
125+
'context' => 'block-supports',
126+
'prettify' => false,
127+
)
128+
);
129+
130+
// Add the visibility class to the block content.
131+
if ( ! empty( $block_content ) ) {
132+
$processor = new WP_HTML_Tag_Processor( $block_content );
133+
if ( $processor->next_tag() ) {
134+
$processor->add_class( $visibility_class );
135+
$block_content = $processor->get_updated_html();
136+
}
137+
}
138+
}
139+
}
140+
30141
return $block_content;
31142
}
32143

src/wp-includes/kses.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2708,6 +2708,8 @@ function safecss_filter_attr( $css, $deprecated = '' ) {
27082708
'column-span',
27092709
'column-width',
27102710

2711+
'display',
2712+
27112713
'color',
27122714
'filter',
27132715
'font',

tests/phpunit/tests/block-supports/block-visibility.php

Lines changed: 240 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ private function register_visibility_block_with_support( $block_name, $supports
6161
* @ticket 64061
6262
*/
6363
public function test_block_visibility_support_hides_block_when_visibility_false() {
64-
$block_type = $this->register_visibility_block_with_support(
64+
$this->register_visibility_block_with_support(
6565
'test/visibility-block',
6666
array( 'visibility' => true )
6767
);
@@ -88,7 +88,7 @@ public function test_block_visibility_support_hides_block_when_visibility_false(
8888
* @ticket 64061
8989
*/
9090
public function test_block_visibility_support_shows_block_when_support_not_opted_in() {
91-
$block_type = $this->register_visibility_block_with_support(
91+
$this->register_visibility_block_with_support(
9292
'test/visibility-block',
9393
array( 'visibility' => false )
9494
);
@@ -107,4 +107,242 @@ public function test_block_visibility_support_shows_block_when_support_not_opted
107107

108108
$this->assertSame( $block_content, $result, 'Block content should remain unchanged when blockVisibility support is not opted in.' );
109109
}
110+
111+
/*
112+
* @ticket 64414
113+
*/
114+
public function test_block_visibility_support_no_visibility_attribute() {
115+
$this->register_visibility_block_with_support(
116+
'test/block-visibility-none',
117+
array( 'visibility' => true )
118+
);
119+
120+
$block = array(
121+
'blockName' => 'test/block-visibility-none',
122+
'attrs' => array(),
123+
);
124+
125+
$block_content = '<div>Test content</div>';
126+
$result = wp_render_block_visibility_support( $block_content, $block );
127+
128+
$this->assertSame( $block_content, $result );
129+
}
130+
131+
/*
132+
* @ticket 64414
133+
*/
134+
public function test_block_visibility_support_generated_css_with_display_none() {
135+
$this->register_visibility_block_with_support(
136+
'test/css-generation',
137+
array( 'visibility' => true )
138+
);
139+
140+
$block = array(
141+
'blockName' => 'test/css-generation',
142+
'attrs' => array(
143+
'metadata' => array(
144+
'blockVisibility' => array(
145+
'mobile' => false,
146+
),
147+
),
148+
),
149+
);
150+
151+
$block_content = '<div>Test content</div>';
152+
wp_render_block_visibility_support( $block_content, $block );
153+
154+
$stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports' );
155+
156+
$this->assertStringContainsString( 'display:none!important', str_replace( ' ', '', $stylesheet ), 'display:none!important should be in the CSS' );
157+
$this->assertStringContainsString( '.wp-block-hidden-mobile', $stylesheet, 'Stylesheet should contain the visibility class' );
158+
$this->assertStringContainsString( '@media', $stylesheet, 'Stylesheet should contain media query' );
159+
}
160+
161+
/*
162+
* @ticket 64414
163+
*/
164+
public function test_block_visibility_support_generated_css_with_mobile_breakpoint() {
165+
$this->register_visibility_block_with_support(
166+
'test/responsive-mobile',
167+
array( 'visibility' => true )
168+
);
169+
170+
$block = array(
171+
'blockName' => 'test/responsive-mobile',
172+
'attrs' => array(
173+
'metadata' => array(
174+
'blockVisibility' => array(
175+
'mobile' => false,
176+
),
177+
),
178+
),
179+
);
180+
181+
$block_content = '<div>Test content</div>';
182+
$result = wp_render_block_visibility_support( $block_content, $block );
183+
184+
$this->assertStringContainsString( 'wp-block-hidden-mobile', $result, 'Block should have the visibility class for the mobile breakpoint.' );
185+
}
186+
187+
/*
188+
* @ticket 64414
189+
*/
190+
public function test_block_visibility_support_generated_css_with_multiple_breakpoints() {
191+
$this->register_visibility_block_with_support(
192+
'test/responsive-multiple',
193+
array( 'visibility' => true )
194+
);
195+
196+
$block = array(
197+
'blockName' => 'test/responsive-multiple',
198+
'attrs' => array(
199+
'metadata' => array(
200+
'blockVisibility' => array(
201+
'mobile' => false,
202+
'desktop' => false,
203+
),
204+
),
205+
),
206+
);
207+
208+
$block_content = '<div>Test content</div>';
209+
$result = wp_render_block_visibility_support( $block_content, $block );
210+
211+
$this->assertStringContainsString( 'wp-block-hidden-desktop-mobile', $result, 'Block should have the visibility class for both breakpoints (sorted alphabetically).' );
212+
}
213+
214+
/*
215+
* @ticket 64414
216+
*/
217+
public function test_block_visibility_support_generated_css_with_tablet_breakpoint() {
218+
$this->register_visibility_block_with_support(
219+
'test/responsive-tablet',
220+
array( 'visibility' => true )
221+
);
222+
223+
$block = array(
224+
'blockName' => 'test/responsive-tablet',
225+
'attrs' => array(
226+
'metadata' => array(
227+
'blockVisibility' => array(
228+
'tablet' => false,
229+
),
230+
),
231+
),
232+
);
233+
234+
$block_content = '<div class="existing-class">Test content</div>';
235+
$result = wp_render_block_visibility_support( $block_content, $block );
236+
237+
$this->assertStringContainsString( 'existing-class', $result, 'Block should have the existing class.' );
238+
$this->assertStringContainsString( 'wp-block-hidden-tablet', $result, 'Block should have the visibility class for the tablet breakpoint.' );
239+
}
240+
241+
/*
242+
* @ticket 64414
243+
*/
244+
public function test_block_visibility_support_generated_css_with_all_breakpoints_visible() {
245+
$this->register_visibility_block_with_support(
246+
'test/responsive-all-visible',
247+
array( 'visibility' => true )
248+
);
249+
250+
$block = array(
251+
'blockName' => 'test/responsive-all-visible',
252+
'attrs' => array(
253+
'metadata' => array(
254+
'blockVisibility' => array(
255+
'mobile' => true,
256+
'tablet' => true,
257+
'desktop' => true,
258+
),
259+
),
260+
),
261+
);
262+
263+
$block_content = '<div>Test content</div>';
264+
$result = wp_render_block_visibility_support( $block_content, $block );
265+
266+
$this->assertSame( $block_content, $result, 'Block content should remain unchanged when all breakpoints are visible.' );
267+
}
268+
269+
/*
270+
* @ticket 64414
271+
*/
272+
public function test_block_visibility_support_generated_css_with_empty_object() {
273+
$this->register_visibility_block_with_support(
274+
'test/responsive-empty',
275+
array( 'visibility' => true )
276+
);
277+
278+
$block = array(
279+
'blockName' => 'test/responsive-empty',
280+
'attrs' => array(
281+
'metadata' => array(
282+
'blockVisibility' => array(),
283+
),
284+
),
285+
);
286+
287+
$block_content = '<div>Test content</div>';
288+
$result = wp_render_block_visibility_support( $block_content, $block );
289+
290+
$this->assertSame( $block_content, $result, 'Block content should remain unchanged when there is no visibility object.' );
291+
}
292+
293+
/*
294+
* @ticket 64414
295+
*/
296+
public function test_block_visibility_support_generated_css_with_unknown_breakpoints_ignored() {
297+
$this->register_visibility_block_with_support(
298+
'test/responsive-unknown-breakpoints',
299+
array( 'visibility' => true )
300+
);
301+
302+
$block = array(
303+
'blockName' => 'test/responsive-unknown-breakpoints',
304+
'attrs' => array(
305+
'metadata' => array(
306+
'blockVisibility' => array(
307+
'mobile' => false,
308+
'unknownBreak' => false,
309+
'largeScreen' => false,
310+
),
311+
),
312+
),
313+
);
314+
315+
$block_content = '<div>Test content</div>';
316+
$result = wp_render_block_visibility_support( $block_content, $block );
317+
318+
$this->assertStringContainsString( 'wp-block-hidden-mobile', $result, 'Block should have the visibility class for the mobile breakpoint.' );
319+
$this->assertStringNotContainsString( 'unknownBreak', $result, 'Unknown breakpoints should not appear in the class name.' );
320+
$this->assertStringNotContainsString( 'largeScreen', $result, 'Large screen breakpoints should not appear in the class name.' );
321+
}
322+
323+
/*
324+
* @ticket 64414
325+
*/
326+
public function test_block_visibility_support_generated_css_with_empty_content() {
327+
$this->register_visibility_block_with_support(
328+
'test/empty-content',
329+
array( 'visibility' => true )
330+
);
331+
332+
$block = array(
333+
'blockName' => 'test/empty-content',
334+
'attrs' => array(
335+
'metadata' => array(
336+
'blockVisibility' => array(
337+
'mobile' => false,
338+
),
339+
),
340+
),
341+
);
342+
343+
$block_content = '';
344+
$result = wp_render_block_visibility_support( $block_content, $block );
345+
346+
$this->assertSame( '', $result, 'Block content should be empty when there is no content.' );
347+
}
110348
}

0 commit comments

Comments
 (0)