diff --git a/src/wp-includes/block-supports/position.php b/src/wp-includes/block-supports/position.php new file mode 100644 index 0000000000000..8dd5f662e38cf --- /dev/null +++ b/src/wp-includes/block-supports/position.php @@ -0,0 +1,151 @@ +attributes ) { + $block_type->attributes = array(); + } + + if ( $has_position_support && ! array_key_exists( 'style', $block_type->attributes ) ) { + $block_type->attributes['style'] = array( + 'type' => 'object', + ); + } +} + +/** + * Renders position styles to the block wrapper. + * + * @since 6.2.0 + * @access private + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function wp_render_position_support( $block_content, $block ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $has_position_support = block_has_support( $block_type, array( 'position' ), false ); + + if ( + ! $has_position_support || + empty( $block['attrs']['style']['position'] ) + ) { + return $block_content; + } + + $global_settings = wp_get_global_settings(); + $theme_has_sticky_support = _wp_array_get( $global_settings, array( 'position', 'sticky' ), false ); + $theme_has_fixed_support = _wp_array_get( $global_settings, array( 'position', 'fixed' ), false ); + + // Only allow output for position types that the theme supports. + $allowed_position_types = array(); + if ( true === $theme_has_sticky_support ) { + $allowed_position_types[] = 'sticky'; + } + if ( true === $theme_has_fixed_support ) { + $allowed_position_types[] = 'fixed'; + } + + $style_attribute = _wp_array_get( $block, array( 'attrs', 'style' ), null ); + $class_name = wp_unique_id( 'wp-container-' ); + $selector = ".$class_name"; + $position_styles = array(); + $position_type = _wp_array_get( $style_attribute, array( 'position', 'type' ), '' ); + $wrapper_classes = array(); + + if ( + in_array( $position_type, $allowed_position_types, true ) + ) { + $wrapper_classes[] = $class_name; + $wrapper_classes[] = 'is-position-' . $position_type; + $sides = array( 'top', 'right', 'bottom', 'left' ); + + foreach ( $sides as $side ) { + $side_value = _wp_array_get( $style_attribute, array( 'position', $side ) ); + if ( null !== $side_value ) { + /* + * For fixed or sticky top positions, + * ensure the value includes an offset for the logged in admin bar. + */ + if ( + 'top' === $side && + ( 'fixed' === $position_type || 'sticky' === $position_type ) + ) { + // Ensure 0 values can be used in `calc()` calculations. + if ( '0' === $side_value || 0 === $side_value ) { + $side_value = '0px'; + } + + // Ensure current side value also factors in the height of the logged in admin bar. + $side_value = "calc($side_value + var(--wp-admin--admin-bar--position-offset, 0px))"; + } + + $position_styles[] = + array( + 'selector' => $selector, + 'declarations' => array( + $side => $side_value, + ), + ); + } + } + + $position_styles[] = + array( + 'selector' => $selector, + 'declarations' => array( + 'position' => $position_type, + 'z-index' => '10', + ), + ); + } + + if ( ! empty( $position_styles ) ) { + /* + * Add to the style engine store to enqueue and render position styles. + */ + wp_style_engine_get_stylesheet_from_css_rules( + $position_styles, + array( + 'context' => 'block-supports', + 'prettify' => false, + ) + ); + + // Inject class name to block container markup. + $content = new WP_HTML_Tag_Processor( $block_content ); + $content->next_tag(); + foreach ( $wrapper_classes as $class ) { + $content->add_class( $class ); + } + return (string) $content; + } + + return $block_content; +} + +// Register the block support. +WP_Block_Supports::get_instance()->register( + 'position', + array( + 'register_attribute' => 'wp_register_position_support', + ) +); +add_filter( 'render_block', 'wp_render_position_support', 10, 2 ); diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 2908fb94d7e69..43c9ffa7d0511 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -304,7 +304,8 @@ class WP_Theme_JSON { * and `typography`, and renamed others according to the new schema. * @since 6.0.0 Added `color.defaultDuotone`. * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`. - * @since 6.2.0 Added `dimensions.minHeight`, 'shadow.presets', and 'shadow.defaultPresets'. + * @since 6.2.0 Added `dimensions.minHeight`, 'shadow.presets', 'shadow.defaultPresets', + * `position.fixed` and `position.sticky`. * @var array */ const VALID_SETTINGS = array( @@ -339,6 +340,10 @@ class WP_Theme_JSON { 'definitions' => null, 'wideSize' => null, ), + 'position' => array( + 'fixed' => null, + 'sticky' => null, + ), 'spacing' => array( 'customSpacingSize' => null, 'spacingSizes' => null, @@ -513,7 +518,7 @@ public static function get_element_class_name( $element ) { * Options that settings.appearanceTools enables. * * @since 6.0.0 - * @since 6.2.0 Added `dimensions.minHeight`. + * @since 6.2.0 Added `dimensions.minHeight` and `position.sticky`. * @var array */ const APPEARANCE_TOOLS_OPT_INS = array( @@ -523,6 +528,7 @@ public static function get_element_class_name( $element ) { array( 'border', 'width' ), array( 'color', 'link' ), array( 'dimensions', 'minHeight' ), + array( 'position', 'sticky' ), array( 'spacing', 'blockGap' ), array( 'spacing', 'margin' ), array( 'spacing', 'padding' ), diff --git a/src/wp-settings.php b/src/wp-settings.php index 6e4a89a332296..994e79ad5a427 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -335,6 +335,7 @@ require ABSPATH . WPINC . '/block-supports/elements.php'; require ABSPATH . WPINC . '/block-supports/generated-classname.php'; require ABSPATH . WPINC . '/block-supports/layout.php'; +require ABSPATH . WPINC . '/block-supports/position.php'; require ABSPATH . WPINC . '/block-supports/spacing.php'; require ABSPATH . WPINC . '/block-supports/typography.php'; require ABSPATH . WPINC . '/style-engine.php'; diff --git a/tests/phpunit/tests/block-supports/wpRenderPositionSupport.php b/tests/phpunit/tests/block-supports/wpRenderPositionSupport.php new file mode 100644 index 0000000000000..4aa1418fe5eb2 --- /dev/null +++ b/tests/phpunit/tests/block-supports/wpRenderPositionSupport.php @@ -0,0 +1,186 @@ +test_block_name = null; + $this->theme_root = realpath( DIR_TESTDATA . '/themedir1' ); + $this->orig_theme_dir = $GLOBALS['wp_theme_directories']; + + // /themes is necessary as theme.php functions assume /themes is the root if there is only one root. + $GLOBALS['wp_theme_directories'] = array( WP_CONTENT_DIR . '/themes', $this->theme_root ); + + add_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + + // Clear caches. + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + WP_Style_Engine_CSS_Rules_Store::remove_all_stores(); + } + + public function tear_down() { + $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir; + + // Clear up the filters to modify the theme root. + remove_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + remove_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + remove_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + WP_Style_Engine_CSS_Rules_Store::remove_all_stores(); + unregister_block_type( $this->test_block_name ); + $this->test_block_name = null; + parent::tear_down(); + } + + public function filter_set_theme_root() { + return $this->theme_root; + } + + /** + * Tests that position block support works as expected. + * + * @ticket 57618 + * + * @covers ::wp_render_position_support + * + * @dataProvider data_position_block_support + * + * @param string $theme_name The theme to switch to. + * @param string $block_name The test block name to register. + * @param mixed $position_settings The position block support settings. + * @param mixed $position_style The position styles within the block attributes. + * @param string $expected_wrapper Expected markup for the block wrapper. + * @param string $expected_styles Expected styles enqueued by the style engine. + */ + public function test_position_block_support( $theme_name, $block_name, $position_settings, $position_style, $expected_wrapper, $expected_styles ) { + switch_theme( $theme_name ); + $this->test_block_name = $block_name; + + register_block_type( + $this->test_block_name, + array( + 'api_version' => 2, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'position' => $position_settings, + ), + ) + ); + + $block = array( + 'blockName' => 'test/position-rules-are-output', + 'attrs' => array( + 'style' => array( + 'position' => $position_style, + ), + ), + ); + + $actual = wp_render_position_support( '
Content
', $block ); + + $this->assertMatchesRegularExpression( + $expected_wrapper, + $actual, + 'Position block wrapper markup should be correct' + ); + + $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( + 'block-supports', + array( + 'prettify' => false, + ) + ); + + $this->assertMatchesRegularExpression( + $expected_styles, + $actual_stylesheet, + 'Position style rules output should be correct' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_position_block_support() { + return array( + 'sticky position style is applied' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/position-rules-are-output', + 'position_settings' => true, + 'position_style' => array( + 'type' => 'sticky', + 'top' => '0px', + ), + 'expected_wrapper' => '/^
Content<\/div>$/', + 'expected_styles' => '/^.wp-container-\d+' . preg_quote( '{top:calc(0px + var(--wp-admin--admin-bar--position-offset, 0px));position:sticky;z-index:10;}' ) . '$/', + ), + 'sticky position style is not applied if theme does not support it' => array( + 'theme_name' => 'default', + 'block_name' => 'test/position-rules-without-theme-support', + 'position_settings' => true, + 'position_style' => array( + 'type' => 'sticky', + 'top' => '0px', + ), + 'expected_wrapper' => '/^
Content<\/div>$/', + 'expected_styles' => '/^$/', + ), + 'sticky position style is not applied if block does not support it' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/position-rules-without-block-support', + 'position_settings' => false, + 'position_style' => array( + 'type' => 'sticky', + 'top' => '0px', + ), + 'expected_wrapper' => '/^
Content<\/div>$/', + 'expected_styles' => '/^$/', + ), + 'sticky position style is not applied if type is not valid' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/position-rules-with-valid-type', + 'position_settings' => true, + 'position_style' => array( + 'type' => 'illegal-type', + 'top' => '0px', + ), + 'expected_wrapper' => '/^
Content<\/div>$/', + 'expected_styles' => '/^$/', + ), + ); + } +} diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index efa709ef756dc..6b915bf218a80 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -274,6 +274,9 @@ public function test_get_settings_appearance_true_opts_in() { 'dimensions' => array( 'minHeight' => true, ), + 'position' => array( + 'sticky' => true, + ), 'spacing' => array( 'blockGap' => false, 'margin' => true, @@ -301,6 +304,9 @@ public function test_get_settings_appearance_true_opts_in() { 'dimensions' => array( 'minHeight' => true, ), + 'position' => array( + 'sticky' => true, + ), 'spacing' => array( 'blockGap' => false, 'margin' => true,