From 88e25f5ff05e2c80163ab8f7e273789825cedf41 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Thu, 22 Oct 2026 10:10:57 +0300 Subject: [PATCH 1/3] Editor: Add the rotate block support. Backports the rotate block support from Gutenberg: block types support rotation unless they opt out with `supports.rotate: false`. The angle is stored in `style.rotate`, with viewport overrides in `style['@tablet'].rotate` and `style['@mobile'].rotate`, and rendered with the CSS `rotate` property under a generated class in the block supports stylesheet. Adds `rotate` to the safe CSS properties so the style engine keeps the declaration. Synced from https://github.com/WordPress/gutenberg/pull/84505. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/block-supports/rotate.php | 200 +++++++++ src/wp-includes/kses.php | 2 + src/wp-settings.php | 1 + tests/phpunit/tests/block-supports/rotate.php | 395 ++++++++++++++++++ 4 files changed, 598 insertions(+) create mode 100644 src/wp-includes/block-supports/rotate.php create mode 100644 tests/phpunit/tests/block-supports/rotate.php diff --git a/src/wp-includes/block-supports/rotate.php b/src/wp-includes/block-supports/rotate.php new file mode 100644 index 0000000000000..b4ded87cf233a --- /dev/null +++ b/src/wp-includes/block-supports/rotate.php @@ -0,0 +1,200 @@ +attributes ) { + $block_type->attributes = array(); + } + + // Check for existing style attribute definition e.g. from block.json. + if ( ! array_key_exists( 'style', $block_type->attributes ) ) { + $block_type->attributes['style'] = array( + 'type' => 'object', + ); + } +} + +/** + * Reads a stored rotation, which is a number of degrees. + * + * Block attributes are untrusted, so anything that is not numeric is ignored. + * The angle is rounded to two decimals and wrapped into the (-180, 180] range. + * + * @since 7.2.0 + * @access private + * + * @param mixed $value Stored rotation. + * @return float|null The rotation in degrees, or null when the value is not a rotation. + */ +function wp_get_rotate_value( $value ) { + // PHP 7 rejects numeric strings with trailing whitespace, PHP 8 accepts them. + if ( is_string( $value ) ) { + $value = trim( $value ); + } + if ( ! is_numeric( $value ) ) { + return null; + } + + $angle = (float) $value; + if ( ! is_finite( $angle ) ) { + return null; + } + + $angle = fmod( round( $angle, 2 ), 360 ); + if ( $angle <= -180 ) { + $angle += 360; + } elseif ( $angle > 180 ) { + $angle -= 360; + } + + // Avoid returning -0. + return 0.0 === $angle ? 0.0 : $angle; +} + +/** + * Builds the CSS rules that rotate a block, without their selector. + * + * The `rotate` property is used rather than `transform` so that it combines with any + * transforms a theme or block applies. In a viewport override, 0 undoes the default + * rotation, so it is output as `none`. + * + * @since 7.2.0 + * @access private + * + * @param array $style Block style attribute. + * @param array $responsive_media_queries Media queries keyed by viewport state, e.g. `@mobile`. + * @return array[] CSS rules with `declarations` and, for viewport overrides, `rules_group`. + */ +function wp_get_rotate_style_rules( $style, $responsive_media_queries ) { + $rules = array(); + $rotate = wp_get_rotate_value( $style['rotate'] ?? null ); + + if ( $rotate ) { + $rules[] = array( + 'declarations' => array( 'rotate' => $rotate . 'deg' ), + ); + } + + foreach ( $responsive_media_queries as $breakpoint => $media_query ) { + if ( ! isset( $style[ $breakpoint ] ) || ! is_array( $style[ $breakpoint ] ) ) { + continue; + } + + $viewport_rotate = wp_get_rotate_value( $style[ $breakpoint ]['rotate'] ?? null ); + if ( null === $viewport_rotate ) { + continue; + } + + $rules[] = array( + 'rules_group' => $media_query, + 'declarations' => array( 'rotate' => $viewport_rotate ? $viewport_rotate . 'deg' : 'none' ), + ); + } + + return $rules; +} + +/** + * Rotates a block on the front end. + * + * The rules are added to the block supports stylesheet under a class generated from + * them, the default rotation first, so that viewport overrides win. + * + * @since 7.2.0 + * @access private + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function wp_render_rotate_support( $block_content, $block ) { + $style = $block['attrs']['style'] ?? null; + if ( empty( $block_content ) || empty( $block['blockName'] ) || ! is_array( $style ) ) { + return $block_content; + } + + $has_viewport_rotate = false; + foreach ( $style as $key => $value ) { + if ( is_string( $key ) && str_starts_with( $key, '@' ) && is_array( $value ) && array_key_exists( 'rotate', $value ) ) { + $has_viewport_rotate = true; + break; + } + } + + if ( ! array_key_exists( 'rotate', $style ) && ! $has_viewport_rotate ) { + return $block_content; + } + + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + if ( ! $block_type || ! block_has_support( $block_type, 'rotate', true ) ) { + return $block_content; + } + + // A block's own `style` attribute that is not an object doesn't hold a rotation. + $style_attribute_type = $block_type->attributes['style']['type'] ?? 'object'; + if ( 'object' !== $style_attribute_type ) { + return $block_content; + } + + // Only resolve the global settings when there is a viewport override to place. + $responsive_media_queries = $has_viewport_rotate + ? WP_Theme_JSON::get_viewport_media_queries( wp_get_global_settings( array( 'viewport' ) ) ) + : array(); + $rules = wp_get_rotate_style_rules( $style, $responsive_media_queries ); + if ( empty( $rules ) ) { + return $block_content; + } + + $processor = new WP_HTML_Tag_Processor( $block_content ); + if ( ! $processor->next_tag() ) { + return $block_content; + } + + $class_name = wp_unique_id_from_values( $rules, 'wp-rotate-' ); + foreach ( $rules as $index => $rule ) { + $rules[ $index ]['selector'] = ".$class_name"; + } + + wp_style_engine_get_stylesheet_from_css_rules( + $rules, + array( + 'context' => 'block-supports', + 'prettify' => false, + ) + ); + + $processor->add_class( $class_name ); + + return $processor->get_updated_html(); +} + +// Register the block support. +WP_Block_Supports::get_instance()->register( + 'rotate', + array( + 'register_attribute' => 'wp_register_rotate_support', + ) +); + +add_filter( 'render_block', 'wp_render_rotate_support', 10, 2 ); diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index c68b71fcc1527..67658ec17f280 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -3693,6 +3693,7 @@ function kses_init() { * and URLs in the SVG element reference properties. * @since 7.2.0 Added support for CSS anchor positioning properties and `text-shadow`. * Added support for `background-clip`. + * Added support for `rotate`. * * @param string $css A string of CSS rules, decoded from an HTML `style` attribute. * @param string $deprecated Not used. @@ -3880,6 +3881,7 @@ function safecss_filter_attr( $css, $deprecated = '' ) { 'box-shadow', 'aspect-ratio', 'container-type', + 'rotate', 'fill', 'fill-opacity', diff --git a/src/wp-settings.php b/src/wp-settings.php index 8bd37b43d635e..c5fe017fc348f 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -461,6 +461,7 @@ require ABSPATH . WPINC . '/block-supports/anchor.php'; require ABSPATH . WPINC . '/block-supports/block-visibility.php'; require ABSPATH . WPINC . '/block-supports/custom-css.php'; +require ABSPATH . WPINC . '/block-supports/rotate.php'; require ABSPATH . WPINC . '/block-supports/states.php'; require ABSPATH . WPINC . '/style-engine.php'; require ABSPATH . WPINC . '/style-engine/class-wp-style-engine.php'; diff --git a/tests/phpunit/tests/block-supports/rotate.php b/tests/phpunit/tests/block-supports/rotate.php new file mode 100644 index 0000000000000..986981b833858 --- /dev/null +++ b/tests/phpunit/tests/block-supports/rotate.php @@ -0,0 +1,395 @@ +test_block_name = null; + WP_Style_Engine_CSS_Rules_Store::remove_all_stores(); + } + + public function tear_down() { + if ( $this->test_block_name ) { + unregister_block_type( $this->test_block_name ); + } + $this->test_block_name = null; + WP_Style_Engine_CSS_Rules_Store::remove_all_stores(); + parent::tear_down(); + } + + /** + * Registers a block for the tests. + * + * @param array $supports Block supports. + * @param array $attributes Block attributes. + * @return WP_Block_Type The registered block type. + */ + private function register_test_block( $supports = array(), $attributes = array() ) { + $this->test_block_name = 'test/rotate-block'; + register_block_type( + $this->test_block_name, + array( + 'api_version' => 3, + 'attributes' => $attributes, + 'supports' => $supports, + ) + ); + + return WP_Block_Type_Registry::get_instance()->get_registered( $this->test_block_name ); + } + + /** + * Renders the test block with a style attribute. + * + * @param array $style Block style attribute. + * @param string $block_content Rendered block content. + * @return string Filtered block content. + */ + private function render_test_block( $style, $block_content = '
Content
' ) { + return wp_render_rotate_support( + $block_content, + array( + 'blockName' => 'test/rotate-block', + 'attrs' => array( 'style' => $style ), + ) + ); + } + + /** + * Returns the class the rotate support added to rendered block content. + * + * @param string $block_content Rendered block content. + * @return string|null The class, or null when there is none. + */ + private function get_rotate_class( $block_content ) { + return preg_match( '/wp-rotate-[a-f0-9]+/', $block_content, $matches ) ? $matches[0] : null; + } + + /** + * Returns the block supports stylesheet. + * + * @return string The stylesheet. + */ + private function get_block_supports_stylesheet() { + return wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); + } + + /** + * Tests that every block supports rotation unless it opts out. + * + * @covers ::wp_register_rotate_support + */ + public function test_registers_style_attribute_unless_block_opts_out() { + $block_type = $this->register_test_block(); + wp_register_rotate_support( $block_type ); + $this->assertSame( array( 'type' => 'object' ), $block_type->attributes['style'] ?? null, 'A block supports rotation by default.' ); + unregister_block_type( $this->test_block_name ); + + $block_type = $this->register_test_block( array( 'rotate' => false ) ); + wp_register_rotate_support( $block_type ); + $this->assertArrayNotHasKey( 'style', $block_type->attributes, 'A block that opts out of rotation does not get a style attribute from it.' ); + } + + /** + * Tests that a block's own style attribute definition is kept. + * + * @covers ::wp_register_rotate_support + */ + public function test_keeps_existing_style_attribute() { + $style_attribute = array( + 'type' => 'object', + 'default' => array( 'color' => array( 'text' => 'red' ) ), + ); + $block_type = $this->register_test_block( array(), array( 'style' => $style_attribute ) ); + wp_register_rotate_support( $block_type ); + + $this->assertSame( $style_attribute, $block_type->attributes['style'] ); + } + + /** + * Tests that stored rotations are read as angles in (-180, 180]. + * + * @covers ::wp_get_rotate_value + * + * @dataProvider data_get_rotate_value + * + * @param mixed $value Stored rotation. + * @param float|null $expected Expected rotation. + */ + public function test_get_rotate_value( $value, $expected ) { + $this->assertSame( $expected, wp_get_rotate_value( $value ) ); + } + + /** + * Data provider for test_get_rotate_value(). + * + * @return array[] + */ + public function data_get_rotate_value() { + return array( + 'integer' => array( 15, 15.0 ), + 'negative float' => array( -12.5, -12.5 ), + 'numeric string' => array( '30', 30.0 ), + 'zero' => array( 0, 0.0 ), + 'half turn' => array( 180, 180.0 ), + 'minus half turn' => array( -180, 180.0 ), + 'more than a half turn' => array( 270, -90.0 ), + 'more than a turn' => array( 405, 45.0 ), + 'whole negative turn' => array( -360, 0.0 ), + 'rounded to two decimals' => array( 12.3456, 12.35 ), + 'negative half rounded away from zero' => array( -12.345, -12.35 ), + 'half rounded away from zero' => array( 1.005, 1.01 ), + 'numeric string with spaces' => array( ' 15 ', 15.0 ), + 'numeric string with exponent' => array( '1e2', 100.0 ), + 'hexadecimal string' => array( '0x10', null ), // phpcs:ignore PHPCompatibility.Miscellaneous.ValidIntegers.HexNumericStringFound -- Checks that hexadecimal strings are rejected. + 'binary string' => array( '0b11', null ), + 'rounded onto a half turn' => array( -179.999, 180.0 ), + 'string with a unit' => array( '15deg', null ), + 'string with a css injection' => array( '15deg; color: red', null ), + 'empty string' => array( '', null ), + 'null' => array( null, null ), + 'boolean' => array( true, null ), + 'array' => array( array( 15 ), null ), + 'infinite' => array( INF, null ), + 'not a number' => array( NAN, null ), + ); + } + + /** + * Tests that a rotated block gets a class with a rotate rule. + * + * @covers ::wp_render_rotate_support + */ + public function test_renders_default_rotation() { + $this->register_test_block(); + + $actual = $this->render_test_block( array( 'rotate' => 15 ) ); + $class_name = $this->get_rotate_class( $actual ); + + $this->assertNotNull( $class_name, 'The block should get a rotate class.' ); + $this->assertSame( + '
Content
', + $actual + ); + $this->assertSame( ".$class_name{rotate:15deg;}", $this->get_block_supports_stylesheet() ); + } + + /** + * Tests that the rotation is wrapped and rounded before it is output. + * + * @covers ::wp_render_rotate_support + */ + public function test_renders_normalized_rotation() { + $this->register_test_block(); + + $class_name = $this->get_rotate_class( $this->render_test_block( array( 'rotate' => '-270.004' ) ) ); + + $this->assertSame( ".$class_name{rotate:90deg;}", $this->get_block_supports_stylesheet() ); + } + + /** + * Tests that blocks without a rotation are left alone. + * + * @covers ::wp_render_rotate_support + * + * @dataProvider data_does_not_render_without_rotation + * + * @param array $style Block style attribute. + */ + public function test_does_not_render_without_rotation( $style ) { + $this->register_test_block(); + $block_content = '
Content
'; + + $this->assertSame( $block_content, $this->render_test_block( $style, $block_content ) ); + $this->assertSame( '', $this->get_block_supports_stylesheet() ); + } + + /** + * Data provider for test_does_not_render_without_rotation(). + * + * @return array[] + */ + public function data_does_not_render_without_rotation() { + return array( + 'no rotation' => array( array( 'color' => array( 'text' => 'red' ) ) ), + 'zero rotation' => array( array( 'rotate' => 0 ) ), + 'rotation that is not a number' => array( array( 'rotate' => '15deg; color: red' ) ), + 'viewport rotation that is not a number' => array( array( '@mobile' => array( 'rotate' => 'none' ) ) ), + 'malformed viewport style' => array( array( '@mobile' => 'rotate' ) ), + ); + } + + /** + * Tests that viewport overrides are output in media queries after the default rotation, + * and that 0 undoes the default rotation. + * + * @covers ::wp_render_rotate_support + */ + public function test_renders_viewport_overrides() { + $this->register_test_block(); + + $class_name = $this->get_rotate_class( + $this->render_test_block( + array( + 'rotate' => 15, + '@mobile' => array( 'rotate' => 0 ), + '@tablet' => array( 'rotate' => -20 ), + ) + ) + ); + + $this->assertNotNull( $class_name, 'The block should get a rotate class.' ); + $this->assertSame( + ".$class_name{rotate:15deg;}" . + "@media (width <= 480px){.$class_name{rotate:none;}}" . + "@media (480px < width <= 782px){.$class_name{rotate:-20deg;}}", + $this->get_block_supports_stylesheet() + ); + } + + /** + * Tests that a block can be rotated on a viewport only. + * + * @covers ::wp_render_rotate_support + */ + public function test_renders_viewport_rotation_without_default_rotation() { + $this->register_test_block(); + + $class_name = $this->get_rotate_class( + $this->render_test_block( + array( + '@mobile' => array( 'rotate' => 45 ), + ) + ) + ); + + $this->assertNotNull( $class_name, 'The block should get a rotate class.' ); + $this->assertSame( + "@media (width <= 480px){.$class_name{rotate:45deg;}}", + $this->get_block_supports_stylesheet() + ); + } + + /** + * Tests that blocks with the same rotation share a class and a rule. + * + * @covers ::wp_render_rotate_support + */ + public function test_blocks_with_the_same_rotation_share_a_class() { + $this->register_test_block(); + + $first = $this->get_rotate_class( $this->render_test_block( array( 'rotate' => 15 ) ) ); + $second = $this->get_rotate_class( $this->render_test_block( array( 'rotate' => 15 ) ) ); + $third = $this->get_rotate_class( $this->render_test_block( array( 'rotate' => 30 ) ) ); + + $this->assertSame( $first, $second, 'Blocks with the same rotation should share a class.' ); + $this->assertNotSame( $first, $third, 'Blocks with different rotations should have different classes.' ); + $this->assertSame( ".$first{rotate:15deg;}.$third{rotate:30deg;}", $this->get_block_supports_stylesheet() ); + } + + /** + * Tests that a block whose own style attribute is not an object is not rotated. + * + * @covers ::wp_render_rotate_support + */ + public function test_does_not_render_when_style_attribute_is_not_an_object() { + $this->register_test_block( array(), array( 'style' => array( 'type' => 'string' ) ) ); + $block_content = '
Content
'; + + $this->assertSame( $block_content, $this->render_test_block( array( 'rotate' => 15 ), $block_content ) ); + $this->assertSame( '', $this->get_block_supports_stylesheet() ); + } + + /** + * Tests that viewport overrides use the theme's viewport breakpoints. + * + * @covers ::wp_render_rotate_support + */ + public function test_renders_viewport_overrides_with_theme_breakpoints() { + $this->register_test_block(); + + $filter = static function ( $theme_json ) { + return $theme_json->update_with( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'viewport' => array( + 'mobile' => '640px', + 'tablet' => '960px', + ), + ), + ) + ); + }; + + add_filter( 'wp_theme_json_data_theme', $filter ); + WP_Theme_JSON_Resolver::clean_cached_data(); + + try { + $class_name = $this->get_rotate_class( + $this->render_test_block( + array( + '@mobile' => array( 'rotate' => 10 ), + '@tablet' => array( 'rotate' => 0 ), + ) + ) + ); + + $this->assertSame( + "@media (width <= 640px){.$class_name{rotate:10deg;}}" . + "@media (640px < width <= 960px){.$class_name{rotate:none;}}", + $this->get_block_supports_stylesheet() + ); + } finally { + remove_filter( 'wp_theme_json_data_theme', $filter ); + WP_Theme_JSON_Resolver::clean_cached_data(); + } + } + + /** + * Tests that blocks that opt out of rotation are not rotated. + * + * @covers ::wp_render_rotate_support + */ + public function test_does_not_render_when_block_opts_out() { + $this->register_test_block( array( 'rotate' => false ) ); + $block_content = '
Content
'; + + $this->assertSame( $block_content, $this->render_test_block( array( 'rotate' => 15 ), $block_content ) ); + $this->assertSame( '', $this->get_block_supports_stylesheet() ); + } + + /** + * Tests that block content without a tag to rotate is left alone. + * + * @covers ::wp_render_rotate_support + */ + public function test_does_not_render_without_a_tag() { + $this->register_test_block(); + + $this->assertSame( 'Plain text', $this->render_test_block( array( 'rotate' => 15 ), 'Plain text' ) ); + $this->assertSame( '', $this->get_block_supports_stylesheet() ); + } + + /** + * Tests that the rotate property is allowed in inline styles. + * + * @covers ::safecss_filter_attr + */ + public function test_rotate_is_a_safe_css_property() { + $this->assertSame( 'rotate: 15deg', safecss_filter_attr( 'rotate: 15deg' ) ); + $this->assertSame( 'rotate: none', safecss_filter_attr( 'rotate: none' ) ); + } +} From ce269163fce3268be2382b680573487c8721a382 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Fri, 30 Oct 2026 15:48:15 +0200 Subject: [PATCH 2/3] Editor: Make the rotation block support opt-in. Renames the support to `rotation` and makes it opt-in with `supports.rotation: true`, matching Gutenberg, where the support is no longer behind an experiment. Renames the file to `block-supports/rotation.php`. Co-Authored-By: Claude Opus 5.5 --- .../{rotate.php => rotation.php} | 10 ++++---- src/wp-settings.php | 2 +- .../{rotate.php => rotation.php} | 24 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) rename src/wp-includes/block-supports/{rotate.php => rotation.php} (95%) rename tests/phpunit/tests/block-supports/{rotate.php => rotation.php} (93%) diff --git a/src/wp-includes/block-supports/rotate.php b/src/wp-includes/block-supports/rotation.php similarity index 95% rename from src/wp-includes/block-supports/rotate.php rename to src/wp-includes/block-supports/rotation.php index b4ded87cf233a..30bdd428e40e8 100644 --- a/src/wp-includes/block-supports/rotate.php +++ b/src/wp-includes/block-supports/rotation.php @@ -1,8 +1,8 @@ get_registered( $block['blockName'] ); - if ( ! $block_type || ! block_has_support( $block_type, 'rotate', true ) ) { + if ( ! $block_type || ! block_has_support( $block_type, 'rotation', false ) ) { return $block_content; } @@ -191,7 +191,7 @@ function wp_render_rotate_support( $block_content, $block ) { // Register the block support. WP_Block_Supports::get_instance()->register( - 'rotate', + 'rotation', array( 'register_attribute' => 'wp_register_rotate_support', ) diff --git a/src/wp-settings.php b/src/wp-settings.php index c5fe017fc348f..273cc95403342 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -461,7 +461,7 @@ require ABSPATH . WPINC . '/block-supports/anchor.php'; require ABSPATH . WPINC . '/block-supports/block-visibility.php'; require ABSPATH . WPINC . '/block-supports/custom-css.php'; -require ABSPATH . WPINC . '/block-supports/rotate.php'; +require ABSPATH . WPINC . '/block-supports/rotation.php'; require ABSPATH . WPINC . '/block-supports/states.php'; require ABSPATH . WPINC . '/style-engine.php'; require ABSPATH . WPINC . '/style-engine/class-wp-style-engine.php'; diff --git a/tests/phpunit/tests/block-supports/rotate.php b/tests/phpunit/tests/block-supports/rotation.php similarity index 93% rename from tests/phpunit/tests/block-supports/rotate.php rename to tests/phpunit/tests/block-supports/rotation.php index 986981b833858..d4dd0a04493f2 100644 --- a/tests/phpunit/tests/block-supports/rotate.php +++ b/tests/phpunit/tests/block-supports/rotation.php @@ -1,6 +1,6 @@ true ), $attributes = array() ) { $this->test_block_name = 'test/rotate-block'; register_block_type( $this->test_block_name, @@ -87,19 +87,19 @@ private function get_block_supports_stylesheet() { } /** - * Tests that every block supports rotation unless it opts out. + * Tests that a block supports rotation only when it opts in. * * @covers ::wp_register_rotate_support */ - public function test_registers_style_attribute_unless_block_opts_out() { - $block_type = $this->register_test_block(); + public function test_registers_style_attribute_only_when_block_opts_in() { + $block_type = $this->register_test_block( array( 'rotation' => true ) ); wp_register_rotate_support( $block_type ); - $this->assertSame( array( 'type' => 'object' ), $block_type->attributes['style'] ?? null, 'A block supports rotation by default.' ); + $this->assertSame( array( 'type' => 'object' ), $block_type->attributes['style'] ?? null, 'A block that opts in supports rotation.' ); unregister_block_type( $this->test_block_name ); - $block_type = $this->register_test_block( array( 'rotate' => false ) ); + $block_type = $this->register_test_block( array() ); wp_register_rotate_support( $block_type ); - $this->assertArrayNotHasKey( 'style', $block_type->attributes, 'A block that opts out of rotation does not get a style attribute from it.' ); + $this->assertArrayNotHasKey( 'style', $block_type->attributes, 'A block that does not opt in does not get a style attribute from rotation.' ); } /** @@ -112,7 +112,7 @@ public function test_keeps_existing_style_attribute() { 'type' => 'object', 'default' => array( 'color' => array( 'text' => 'red' ) ), ); - $block_type = $this->register_test_block( array(), array( 'style' => $style_attribute ) ); + $block_type = $this->register_test_block( array( 'rotation' => true ), array( 'style' => $style_attribute ) ); wp_register_rotate_support( $block_type ); $this->assertSame( $style_attribute, $block_type->attributes['style'] ); @@ -305,7 +305,7 @@ public function test_blocks_with_the_same_rotation_share_a_class() { * @covers ::wp_render_rotate_support */ public function test_does_not_render_when_style_attribute_is_not_an_object() { - $this->register_test_block( array(), array( 'style' => array( 'type' => 'string' ) ) ); + $this->register_test_block( array( 'rotation' => true ), array( 'style' => array( 'type' => 'string' ) ) ); $block_content = '
Content
'; $this->assertSame( $block_content, $this->render_test_block( array( 'rotate' => 15 ), $block_content ) ); @@ -364,7 +364,7 @@ public function test_renders_viewport_overrides_with_theme_breakpoints() { * @covers ::wp_render_rotate_support */ public function test_does_not_render_when_block_opts_out() { - $this->register_test_block( array( 'rotate' => false ) ); + $this->register_test_block( array() ); $block_content = '
Content
'; $this->assertSame( $block_content, $this->render_test_block( array( 'rotate' => 15 ), $block_content ) ); From 2138e7a7baadc8d7d28ca315645e3f317a9a4181 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Mon, 2 Nov 2026 17:44:12 +0200 Subject: [PATCH 3/3] Editor: Target 7.3 for the rotation block support. Core is past the 7.2 feature freeze. Also simplifies two docblocks, matching Gutenberg. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/block-supports/rotation.php | 17 ++++++++--------- src/wp-includes/kses.php | 2 +- tests/phpunit/tests/block-supports/rotation.php | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/block-supports/rotation.php b/src/wp-includes/block-supports/rotation.php index 30bdd428e40e8..06248b1831893 100644 --- a/src/wp-includes/block-supports/rotation.php +++ b/src/wp-includes/block-supports/rotation.php @@ -7,13 +7,13 @@ * overrides in `style['@tablet'].rotate` and `style['@mobile'].rotate`. * * @package WordPress - * @since 7.2.0 + * @since 7.3.0 */ /** * Registers the style block attribute for block types that support rotation. * - * @since 7.2.0 + * @since 7.3.0 * @access private * * @param WP_Block_Type $block_type Block Type. @@ -41,7 +41,7 @@ function wp_register_rotate_support( $block_type ) { * Block attributes are untrusted, so anything that is not numeric is ignored. * The angle is rounded to two decimals and wrapped into the (-180, 180] range. * - * @since 7.2.0 + * @since 7.3.0 * @access private * * @param mixed $value Stored rotation. @@ -75,11 +75,10 @@ function wp_get_rotate_value( $value ) { /** * Builds the CSS rules that rotate a block, without their selector. * - * The `rotate` property is used rather than `transform` so that it combines with any - * transforms a theme or block applies. In a viewport override, 0 undoes the default - * rotation, so it is output as `none`. + * The `rotate` property is used because it maps most directly to the editor feature. + * In a viewport override, 0 undoes the default rotation, so it is output as `none`. * - * @since 7.2.0 + * @since 7.3.0 * @access private * * @param array $style Block style attribute. @@ -119,9 +118,9 @@ function wp_get_rotate_style_rules( $style, $responsive_media_queries ) { * Rotates a block on the front end. * * The rules are added to the block supports stylesheet under a class generated from - * them, the default rotation first, so that viewport overrides win. + * them. * - * @since 7.2.0 + * @since 7.3.0 * @access private * * @param string $block_content Rendered block content. diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index f5b8325553f3a..9aadf7fc16dac 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -3721,7 +3721,7 @@ function kses_init() { * and URLs in the SVG element reference properties. * @since 7.2.0 Added support for CSS anchor positioning properties and `text-shadow`. * Added support for `background-clip`. - * Added support for `rotate`. + * @since 7.3.0 Added support for `rotate`. * * @param string $css A string of CSS rules, decoded from an HTML `style` attribute. * @param string $deprecated Not used. diff --git a/tests/phpunit/tests/block-supports/rotation.php b/tests/phpunit/tests/block-supports/rotation.php index d4dd0a04493f2..65ecd88abe7c1 100644 --- a/tests/phpunit/tests/block-supports/rotation.php +++ b/tests/phpunit/tests/block-supports/rotation.php @@ -4,7 +4,7 @@ * * @package WordPress * @subpackage Block Supports - * @since 7.2.0 + * @since 7.3.0 * * @group block-supports */