Make WordPress Core

Changeset 62478


Ignore:
Timestamp:
06/09/2026 04:25:45 AM (2 months ago)
Author:
isabel_brison
Message:

Editor: add support for layout responsive styles.

Adds support for setting responsive layout, block spacing and child layout styles.

Props isabel_brison, ramonopoly, audrasjb, desrosj, sabernhardt.
Fixes #65164.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/block-supports/layout.php

    r61655 r62478  
    3636
    3737        return null;
     38}
     39
     40/**
     41 * Returns the child-layout-only subset of a layout object.
     42 *
     43 * @since 7.1.0
     44 *
     45 * @param mixed $layout Layout object.
     46 * @return array Child layout values, or an empty array.
     47 */
     48function wp_get_layout_child_values( $layout ) {
     49        if ( ! is_array( $layout ) ) {
     50                return array();
     51        }
     52
     53        return array_intersect_key(
     54                $layout,
     55                array_flip( array( 'selfStretch', 'flexSize', 'columnStart', 'columnSpan', 'rowStart', 'rowSpan' ) )
     56        );
     57}
     58
     59/**
     60 * Returns the container-layout subset of a layout object.
     61 *
     62 * @since 7.1.0
     63 *
     64 * @param mixed $layout Layout object.
     65 * @return array Container layout values, or an empty array.
     66 */
     67function wp_get_layout_container_values( $layout ) {
     68        if ( ! is_array( $layout ) ) {
     69                return array();
     70        }
     71
     72        return array_diff_key(
     73                $layout,
     74                array_flip( array( 'selfStretch', 'flexSize', 'columnStart', 'columnSpan', 'rowStart', 'rowSpan' ) )
     75        );
     76}
     77
     78/**
     79 * Sanitizes a block gap value before layout style generation.
     80 *
     81 * @since 7.1.0
     82 *
     83 * @param string|array|null $gap_value Block gap value.
     84 * @return string|array|null Sanitized block gap value.
     85 */
     86function wp_sanitize_block_gap_value( $gap_value ) {
     87        if ( is_array( $gap_value ) ) {
     88                foreach ( $gap_value as $key => $value ) {
     89                        $gap_value[ $key ] = $value && preg_match( '%[\\\(&=}]|/\*%', $value ) ? null : $value;
     90                }
     91
     92                return $gap_value;
     93        }
     94
     95        return $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
     96}
     97
     98/**
     99 * Returns child layout styles for a block affected by its parent's layout.
     100 *
     101 * @since 7.1.0
     102 *
     103 * @param string     $selector           CSS selector.
     104 * @param array      $child_layout       Child layout values.
     105 * @param array      $parent_layout      Parent layout values.
     106 * @param array|null $viewport_overrides Optional. Child viewport layout overrides to emit.
     107 * @return array Child layout style rules.
     108 */
     109function wp_get_child_layout_style_rules( $selector, $child_layout, $parent_layout = array(), $viewport_overrides = null ) {
     110        $base_child_layout              = is_array( $child_layout ) ? $child_layout : array();
     111        $viewport_overrides             = is_array( $viewport_overrides ) ? $viewport_overrides : null;
     112        $child_layout                   = null === $viewport_overrides ? $base_child_layout : array_replace( $base_child_layout, $viewport_overrides );
     113        $child_layout_declarations      = array();
     114        $child_layout_styles            = array();
     115        $has_viewport_property_override = static function ( $property ) use ( $viewport_overrides ) {
     116                return array_key_exists( $property, $viewport_overrides );
     117        };
     118
     119        $self_stretch = $child_layout['selfStretch'] ?? null;
     120
     121        if ( null === $viewport_overrides || $has_viewport_property_override( 'selfStretch' ) || $has_viewport_property_override( 'flexSize' ) ) {
     122                if ( 'fixed' === $self_stretch && isset( $child_layout['flexSize'] ) ) {
     123                        $child_layout_declarations['flex-basis'] = $child_layout['flexSize'];
     124                        $child_layout_declarations['box-sizing'] = 'border-box';
     125                } elseif ( 'fill' === $self_stretch ) {
     126                        $child_layout_declarations['flex-grow'] = '1';
     127                }
     128        }
     129
     130        $column_start = $child_layout['columnStart'] ?? null;
     131        $column_span  = $child_layout['columnSpan'] ?? null;
     132        if ( null === $viewport_overrides || $has_viewport_property_override( 'columnStart' ) || $has_viewport_property_override( 'columnSpan' ) ) {
     133                if ( $column_start && $column_span ) {
     134                        $child_layout_declarations['grid-column'] = "$column_start / span $column_span";
     135                } elseif ( $column_start ) {
     136                        $child_layout_declarations['grid-column'] = "$column_start";
     137                } elseif ( $column_span ) {
     138                        $child_layout_declarations['grid-column'] = "span $column_span";
     139                }
     140        }
     141
     142        $row_start = $child_layout['rowStart'] ?? null;
     143        $row_span  = $child_layout['rowSpan'] ?? null;
     144        if ( null === $viewport_overrides || $has_viewport_property_override( 'rowStart' ) || $has_viewport_property_override( 'rowSpan' ) ) {
     145                if ( $row_start && $row_span ) {
     146                        $child_layout_declarations['grid-row'] = "$row_start / span $row_span";
     147                } elseif ( $row_start ) {
     148                        $child_layout_declarations['grid-row'] = "$row_start";
     149                } elseif ( $row_span ) {
     150                        $child_layout_declarations['grid-row'] = "span $row_span";
     151                }
     152        }
     153
     154        if ( ! empty( $child_layout_declarations ) ) {
     155                $child_layout_styles[] = array(
     156                        'selector'     => $selector,
     157                        'declarations' => $child_layout_declarations,
     158                );
     159        }
     160
     161        $minimum_column_width = $parent_layout['minimumColumnWidth'] ?? null;
     162        $column_count         = $parent_layout['columnCount'] ?? null;
     163
     164        /*
     165         * If columnSpan or columnStart is set, and the parent grid is responsive, i.e. if it has a minimumColumnWidth set,
     166         * the columnSpan should be removed once the grid is smaller than the span, and columnStart should be removed
     167         * once the grid has less columns than the start.
     168         * If there's a minimumColumnWidth, the grid is responsive. But if the minimumColumnWidth value wasn't changed, it won't be set.
     169         * In that case, if columnCount doesn't exist, we can assume that the grid is responsive.
     170         */
     171        if ( null === $viewport_overrides && ( $column_span || $column_start ) && ( $minimum_column_width || ! $column_count ) ) {
     172                $column_span_number  = floatval( $column_span );
     173                $column_start_number = floatval( $column_start );
     174                $parent_column_width = $minimum_column_width ? $minimum_column_width : '12rem';
     175                $parent_column_value = floatval( $parent_column_width );
     176                $parent_column_unit  = explode( $parent_column_value, $parent_column_width );
     177
     178                $num_cols_to_break_at = 2;
     179                if ( $column_span_number && $column_start_number ) {
     180                        $num_cols_to_break_at = $column_start_number + $column_span_number - 1;
     181                } elseif ( $column_span_number ) {
     182                        $num_cols_to_break_at = $column_span_number;
     183                } else {
     184                        $num_cols_to_break_at = $column_start_number;
     185                }
     186
     187                /*
     188                 * If there is no unit, the width has somehow been mangled so we reset both unit and value
     189                 * to defaults.
     190                 * Additionally, the unit should be one of px, rem or em, so that also needs to be checked.
     191                 */
     192                if ( count( $parent_column_unit ) <= 1 ) {
     193                        $parent_column_unit  = 'rem';
     194                        $parent_column_value = 12;
     195                } else {
     196                        $parent_column_unit = $parent_column_unit[1];
     197
     198                        if ( ! in_array( $parent_column_unit, array( 'px', 'rem', 'em' ), true ) ) {
     199                                $parent_column_unit = 'rem';
     200                        }
     201                }
     202
     203                /*
     204                 * A default gap value is used for this computation because custom gap values may not be
     205                 * viable to use in the computation of the container query value.
     206                 */
     207                $default_gap_value             = 'px' === $parent_column_unit ? 24 : 1.5;
     208                $container_query_value         = $num_cols_to_break_at * $parent_column_value + ( $num_cols_to_break_at - 1 ) * $default_gap_value;
     209                $minimum_container_query_value = $parent_column_value * 2 + $default_gap_value - 1;
     210                $container_query_value         = max( $container_query_value, $minimum_container_query_value ) . $parent_column_unit;
     211                // If a span is set we want to preserve it as long as possible, otherwise we just reset the value.
     212                $grid_column_value = $column_span && $column_span > 1 ? '1/-1' : 'auto';
     213
     214                $child_layout_styles[] = array(
     215                        'rules_group'  => "@container (max-width: $container_query_value )",
     216                        'selector'     => $selector,
     217                        'declarations' => array(
     218                                'grid-column' => $grid_column_value,
     219                                'grid-row'    => 'auto',
     220                        ),
     221                );
     222        }
     223
     224        return $child_layout_styles;
    38225}
    39226
     
    257444 * @since 6.6.0 Removed duplicated selector from layout styles.
    258445 *              Enabled negative margins for alignfull children of blocks with custom padding.
     446 * @since 7.1.0 Added options array with options to process responsive styles.
    259447 * @access private
    260448 *
     
    267455 * @param string|array         $fallback_gap_value            Optional. The block gap value to apply. If it's an array expected properties are "top" and/or "left". Default '0.5em'.
    268456 * @param array|null           $block_spacing                 Optional. Custom spacing set on the block. Default null.
     457 * @param array                $options                       {
     458 *     Optional. Extra options for internal callers. Default empty array.
     459 *
     460 *     @type array       $viewport_overrides     An array of layout property overrides for the sake of style generation,
     461 *                                               keyed by property name.
     462 *     @type string|null $rules_group            Optional group name for the rules. Default null.
     463 *     @type bool        $has_block_gap_override Whether the block gap has been overridden. Default false.
     464 * }
    269465 * @return string CSS styles on success. Else, empty string.
    270466 */
    271 function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false, $gap_value = null, $should_skip_gap_serialization = false, $fallback_gap_value = '0.5em', $block_spacing = null ) {
    272         $layout_type   = $layout['type'] ?? 'default';
    273         $layout_styles = array();
     467function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false, $gap_value = null, $should_skip_gap_serialization = false, $fallback_gap_value = '0.5em', $block_spacing = null, $options = array() ) {
     468        $base_layout                    = is_array( $layout ) ? $layout : array();
     469        $viewport_overrides             = $options['viewport_overrides'] ?? null;
     470        $layout_for_styles              = null === $viewport_overrides ? $base_layout : array_replace( $base_layout, $viewport_overrides );
     471        $layout_type                    = $base_layout['type'] ?? 'default';
     472        $rules_group                    = $options['rules_group'] ?? null;
     473        $has_block_gap_override         = ! empty( $options['has_block_gap_override'] );
     474        $should_output_block_gap        = null === $viewport_overrides || $has_block_gap_override;
     475        $has_viewport_property_override = static function ( $property ) use ( $viewport_overrides ) {
     476                return array_key_exists( $property, $viewport_overrides );
     477        };
     478        $layout_styles                  = array();
    274479
    275480        if ( 'default' === $layout_type ) {
    276                 if ( $has_block_gap_support ) {
     481                if ( $has_block_gap_support && $should_output_block_gap ) {
    277482                        if ( is_array( $gap_value ) ) {
    278483                                $gap_value = $gap_value['top'] ?? null;
     
    306511                }
    307512        } elseif ( 'constrained' === $layout_type ) {
    308                 $content_size    = $layout['contentSize'] ?? '';
    309                 $wide_size       = $layout['wideSize'] ?? '';
    310                 $justify_content = $layout['justifyContent'] ?? 'center';
     513                $content_size    = $layout_for_styles['contentSize'] ?? '';
     514                $wide_size       = $layout_for_styles['wideSize'] ?? '';
     515                $justify_content = $layout_for_styles['justifyContent'] ?? 'center';
    311516
    312517                $all_max_width_value  = $content_size ? $content_size : $wide_size;
     
    320525                $margin_right = 'right' === $justify_content ? '0 !important' : 'auto !important';
    321526
    322                 if ( $content_size || $wide_size ) {
     527                $has_justify_content_override    = null !== $viewport_overrides && $has_viewport_property_override( 'justifyContent' );
     528                $should_output_constrained_sizes = null === $viewport_overrides || $has_viewport_property_override( 'contentSize' ) || $has_viewport_property_override( 'wideSize' );
     529                if ( $should_output_constrained_sizes && ( $content_size || $wide_size ) ) {
     530                        $content_size_declarations = array(
     531                                'max-width' => $all_max_width_value,
     532                        );
     533
     534                        if ( null === $viewport_overrides || $has_justify_content_override ) {
     535                                $content_size_declarations['margin-left']  = $margin_left;
     536                                $content_size_declarations['margin-right'] = $margin_right;
     537                        }
     538
    323539                        array_push(
    324540                                $layout_styles,
    325541                                array(
    326542                                        'selector'     => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
    327                                         'declarations' => array(
    328                                                 'max-width'    => $all_max_width_value,
    329                                                 'margin-left'  => $margin_left,
    330                                                 'margin-right' => $margin_right,
    331                                         ),
     543                                        'declarations' => $content_size_declarations,
    332544                                ),
    333545                                array(
     
    342554                }
    343555
    344                 if ( isset( $block_spacing ) ) {
     556                if ( null === $viewport_overrides && isset( $block_spacing ) ) {
    345557                        $block_spacing_values = wp_style_engine_get_styles(
    346558                                array(
     
    377589                }
    378590
    379                 if ( 'left' === $justify_content ) {
     591                if ( $has_justify_content_override && ! $should_output_constrained_sizes ) {
    380592                        $layout_styles[] = array(
    381593                                'selector'     => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
    382                                 'declarations' => array( 'margin-left' => '0 !important' ),
     594                                'declarations' => array(
     595                                        'margin-left'  => $margin_left,
     596                                        'margin-right' => $margin_right,
     597                                ),
    383598                        );
    384                 }
    385 
    386                 if ( 'right' === $justify_content ) {
    387                         $layout_styles[] = array(
    388                                 'selector'     => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
    389                                 'declarations' => array( 'margin-right' => '0 !important' ),
    390                         );
    391                 }
    392 
    393                 if ( $has_block_gap_support ) {
     599                } elseif ( null === $viewport_overrides ) {
     600                        if ( 'left' === $justify_content ) {
     601                                $layout_styles[] = array(
     602                                        'selector'     => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
     603                                        'declarations' => array( 'margin-left' => '0 !important' ),
     604                                );
     605                        }
     606
     607                        if ( 'right' === $justify_content ) {
     608                                $layout_styles[] = array(
     609                                        'selector'     => "$selector > :where(:not(.alignleft):not(.alignright):not(.alignfull))",
     610                                        'declarations' => array( 'margin-right' => '0 !important' ),
     611                                );
     612                        }
     613                }
     614
     615                if ( $has_block_gap_support && $should_output_block_gap ) {
    394616                        if ( is_array( $gap_value ) ) {
    395617                                $gap_value = $gap_value['top'] ?? null;
     
    423645                }
    424646        } elseif ( 'flex' === $layout_type ) {
    425                 $layout_orientation = $layout['orientation'] ?? 'horizontal';
     647                $layout_orientation = $layout_for_styles['orientation'] ?? 'horizontal';
    426648
    427649                $justify_content_options = array(
     
    445667                }
    446668
    447                 if ( ! empty( $layout['flexWrap'] ) && 'nowrap' === $layout['flexWrap'] ) {
     669                $should_output_flex_wrap          = null === $viewport_overrides || $has_viewport_property_override( 'flexWrap' );
     670                $should_output_flex_orientation   = null === $viewport_overrides || $has_viewport_property_override( 'orientation' );
     671                $should_output_flex_justification = null === $viewport_overrides || $has_viewport_property_override( 'justifyContent' ) || $has_viewport_property_override( 'orientation' );
     672                $should_output_flex_alignment     = null === $viewport_overrides || $has_viewport_property_override( 'verticalAlignment' ) || $has_viewport_property_override( 'orientation' );
     673
     674                if ( $should_output_flex_wrap && ! empty( $layout_for_styles['flexWrap'] ) && 'nowrap' === $layout_for_styles['flexWrap'] ) {
    448675                        $layout_styles[] = array(
    449676                                'selector'     => $selector,
     
    452679                }
    453680
    454                 if ( $has_block_gap_support && isset( $gap_value ) ) {
     681                if ( $has_block_gap_support && $should_output_block_gap && isset( $gap_value ) ) {
    455682                        $combined_gap_value = '';
    456683                        $gap_sides          = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
     
    490717                         * by custom css.
    491718                         */
    492                         if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) {
     719                        if ( $should_output_flex_justification && ! empty( $layout_for_styles['justifyContent'] ) && array_key_exists( $layout_for_styles['justifyContent'], $justify_content_options ) ) {
    493720                                $layout_styles[] = array(
    494721                                        'selector'     => $selector,
    495                                         'declarations' => array( 'justify-content' => $justify_content_options[ $layout['justifyContent'] ] ),
     722                                        'declarations' => array( 'justify-content' => $justify_content_options[ $layout_for_styles['justifyContent'] ] ),
    496723                                );
    497724                        }
    498725
    499                         if ( ! empty( $layout['verticalAlignment'] ) && array_key_exists( $layout['verticalAlignment'], $vertical_alignment_options ) ) {
     726                        if ( $should_output_flex_alignment && ! empty( $layout_for_styles['verticalAlignment'] ) && array_key_exists( $layout_for_styles['verticalAlignment'], $vertical_alignment_options ) ) {
    500727                                $layout_styles[] = array(
    501728                                        'selector'     => $selector,
    502                                         'declarations' => array( 'align-items' => $vertical_alignment_options[ $layout['verticalAlignment'] ] ),
     729                                        'declarations' => array( 'align-items' => $vertical_alignment_options[ $layout_for_styles['verticalAlignment'] ] ),
    503730                                );
    504731                        }
    505732                } else {
    506                         $layout_styles[] = array(
    507                                 'selector'     => $selector,
    508                                 'declarations' => array( 'flex-direction' => 'column' ),
    509                         );
    510                         if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) {
     733                        if ( $should_output_flex_orientation ) {
    511734                                $layout_styles[] = array(
    512735                                        'selector'     => $selector,
    513                                         'declarations' => array( 'align-items' => $justify_content_options[ $layout['justifyContent'] ] ),
     736                                        'declarations' => array( 'flex-direction' => 'column' ),
    514737                                );
    515                         } else {
     738                        }
     739                        if ( $should_output_flex_justification && ! empty( $layout_for_styles['justifyContent'] ) && array_key_exists( $layout_for_styles['justifyContent'], $justify_content_options ) ) {
     740                                $layout_styles[] = array(
     741                                        'selector'     => $selector,
     742                                        'declarations' => array( 'align-items' => $justify_content_options[ $layout_for_styles['justifyContent'] ] ),
     743                                );
     744                        } elseif ( $should_output_flex_justification ) {
    516745                                $layout_styles[] = array(
    517746                                        'selector'     => $selector,
     
    519748                                );
    520749                        }
    521                         if ( ! empty( $layout['verticalAlignment'] ) && array_key_exists( $layout['verticalAlignment'], $vertical_alignment_options ) ) {
     750                        if ( $should_output_flex_alignment && ! empty( $layout_for_styles['verticalAlignment'] ) && array_key_exists( $layout_for_styles['verticalAlignment'], $vertical_alignment_options ) ) {
    522751                                $layout_styles[] = array(
    523752                                        'selector'     => $selector,
    524                                         'declarations' => array( 'justify-content' => $vertical_alignment_options[ $layout['verticalAlignment'] ] ),
     753                                        'declarations' => array( 'justify-content' => $vertical_alignment_options[ $layout_for_styles['verticalAlignment'] ] ),
    525754                                );
    526755                        }
     
    568797                }
    569798
    570                 if ( ! empty( $layout['columnCount'] ) && ! empty( $layout['minimumColumnWidth'] ) ) {
    571                         $max_value       = 'max(min(' . $layout['minimumColumnWidth'] . ', 100%), (100% - (' . $responsive_gap_value . ' * (' . $layout['columnCount'] . ' - 1))) /' . $layout['columnCount'] . ')';
     799                $should_output_grid_columns = null === $viewport_overrides || $has_viewport_property_override( 'minimumColumnWidth' ) || $has_viewport_property_override( 'columnCount' );
     800                $uses_gap_in_grid_columns   = ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['minimumColumnWidth'] );
     801                if ( $has_block_gap_override && $uses_gap_in_grid_columns ) {
     802                        $should_output_grid_columns = true;
     803                }
     804
     805                $should_output_grid_rows = ( null === $viewport_overrides || $has_viewport_property_override( 'rowCount' ) ) && ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['rowCount'] );
     806                $grid_declarations       = array();
     807
     808                if ( $should_output_grid_columns && ! empty( $layout_for_styles['columnCount'] ) && ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
     809                        $max_value                                  = 'max(min(' . $layout_for_styles['minimumColumnWidth'] . ', 100%), (100% - (' . $responsive_gap_value . ' * (' . $layout_for_styles['columnCount'] . ' - 1))) /' . $layout_for_styles['columnCount'] . ')';
     810                        $grid_declarations['grid-template-columns'] = 'repeat(auto-fill, minmax(' . $max_value . ', 1fr))';
     811                } elseif ( $should_output_grid_columns && ! empty( $layout_for_styles['columnCount'] ) ) {
     812                        $grid_declarations['grid-template-columns'] = 'repeat(' . $layout_for_styles['columnCount'] . ', minmax(0, 1fr))';
     813                } elseif ( $should_output_grid_columns ) {
     814                        $minimum_column_width                       = ! empty( $layout_for_styles['minimumColumnWidth'] ) ? $layout_for_styles['minimumColumnWidth'] : '12rem';
     815                        $grid_declarations['grid-template-columns'] = 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))';
     816                }
     817
     818                if ( ! empty( $grid_declarations ) ) {
     819                        $base_has_container_type = empty( $base_layout['columnCount'] ) || ( ! empty( $base_layout['columnCount'] ) && ! empty( $base_layout['minimumColumnWidth'] ) );
     820                        if ( empty( $layout_for_styles['columnCount'] ) || ! empty( $layout_for_styles['minimumColumnWidth'] ) ) {
     821                                if ( null === $viewport_overrides || ! $base_has_container_type ) {
     822                                        $grid_declarations['container-type'] = 'inline-size';
     823                                }
     824                        }
    572825                        $layout_styles[] = array(
    573826                                'selector'     => $selector,
    574                                 'declarations' => array(
    575                                         'grid-template-columns' => 'repeat(auto-fill, minmax(' . $max_value . ', 1fr))',
    576                                         'container-type'        => 'inline-size',
    577                                 ),
     827                                'declarations' => $grid_declarations,
    578828                        );
    579                         if ( ! empty( $layout['rowCount'] ) ) {
    580                                 $layout_styles[] = array(
    581                                         'selector'     => $selector,
    582                                         'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout['rowCount'] . ', minmax(1rem, auto))' ),
    583                                 );
    584                         }
    585                 } elseif ( ! empty( $layout['columnCount'] ) ) {
     829                }
     830
     831                if ( $should_output_grid_rows ) {
    586832                        $layout_styles[] = array(
    587833                                'selector'     => $selector,
    588                                 'declarations' => array( 'grid-template-columns' => 'repeat(' . $layout['columnCount'] . ', minmax(0, 1fr))' ),
     834                                'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout_for_styles['rowCount'] . ', minmax(1rem, auto))' ),
    589835                        );
    590                         if ( ! empty( $layout['rowCount'] ) ) {
    591                                 $layout_styles[] = array(
    592                                         'selector'     => $selector,
    593                                         'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout['rowCount'] . ', minmax(1rem, auto))' ),
    594                                 );
    595                         }
    596                 } else {
    597                         $minimum_column_width = ! empty( $layout['minimumColumnWidth'] ) ? $layout['minimumColumnWidth'] : '12rem';
    598 
    599                         $layout_styles[] = array(
    600                                 'selector'     => $selector,
    601                                 'declarations' => array(
    602                                         'grid-template-columns' => 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))',
    603                                         'container-type'        => 'inline-size',
    604                                 ),
    605                         );
    606                 }
    607 
    608                 if ( $has_block_gap_support && null !== $gap_value && ! $should_skip_gap_serialization ) {
     836                }
     837
     838                if ( $has_block_gap_support && $should_output_block_gap && null !== $gap_value && ! $should_skip_gap_serialization ) {
    609839                        $layout_styles[] = array(
    610840                                'selector'     => $selector,
     
    615845
    616846        if ( ! empty( $layout_styles ) ) {
     847                if ( ! empty( $rules_group ) ) {
     848                        foreach ( $layout_styles as $index => $layout_style ) {
     849                                $layout_styles[ $index ]['rules_group'] = $rules_group;
     850                        }
     851                }
     852
    617853                /*
    618854                 * Add to the style engine store to enqueue and render layout styles.
     
    651887        $block_type            = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
    652888        $block_supports_layout = block_has_support( $block_type, 'layout', false ) || block_has_support( $block_type, '__experimentalLayout', false );
    653         $child_layout          = $block['attrs']['style']['layout'] ?? null;
    654 
    655         if ( ! $block_supports_layout && ! $child_layout ) {
     889        $style_attr            = $block['attrs']['style'] ?? array();
     890        $child_layout          = $style_attr['layout'] ?? null;
     891
     892        /*
     893         * Collect responsive viewport child layout overrides so that a block with
     894         * only responsive child layout (no base child layout) is still processed.
     895         */
     896        $viewport_child_layouts = array();
     897        foreach ( WP_Theme_JSON::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
     898                $viewport_child = wp_get_layout_child_values( $style_attr[ $breakpoint ]['layout'] ?? null );
     899
     900                if ( ! empty( $viewport_child ) ) {
     901                        $viewport_child_layouts[ $breakpoint ] = array(
     902                                'media_query'  => $media_query,
     903                                'child_layout' => $viewport_child,
     904                        );
     905                }
     906        }
     907
     908        if ( ! $block_supports_layout && ! $child_layout && empty( $viewport_child_layouts ) ) {
    656909                return $block_content;
    657910        }
     
    660913
    661914        // Child layout specific logic.
    662         if ( $child_layout ) {
     915        if ( $child_layout || ! empty( $viewport_child_layouts ) ) {
     916                $base_child_layout = wp_get_layout_child_values( $child_layout );
     917                $parent_layout     = $block['parentLayout'] ?? array();
    663918                /*
    664919                 * Generates a unique class for child block layout styles.
     
    666921                 * To ensure consistent class generation across different page renders,
    667922                 * only properties that affect layout styling are used. These properties
    668                  * come from `$block['attrs']['style']['layout']` and `$block['parentLayout']`.
     923                 * come from `$block['attrs']['style']['layout']`, viewport overrides in
     924                 * `$block['attrs']['style'][$breakpoint]['layout']`, and `$block['parentLayout']`.
    669925                 *
    670926                 * As long as these properties coincide, the generated class will be the same.
    671927                 */
     928                $container_content_hash_input = array(
     929                        'layout'       => $base_child_layout,
     930                        'parentLayout' => array_intersect_key(
     931                                $parent_layout,
     932                                array_flip( array( 'minimumColumnWidth', 'columnCount' ) )
     933                        ),
     934                );
     935
     936                foreach ( $viewport_child_layouts as $breakpoint => $viewport_data ) {
     937                        $container_content_hash_input[ $breakpoint ] = $viewport_data['child_layout'];
     938                }
     939
    672940                $container_content_class = wp_unique_id_from_values(
    673                         array(
    674                                 'layout'       => array_intersect_key(
    675                                         $block['attrs']['style']['layout'] ?? array(),
    676                                         array_flip(
    677                                                 array( 'selfStretch', 'flexSize', 'columnStart', 'columnSpan', 'rowStart', 'rowSpan' )
    678                                         )
    679                                 ),
    680                                 'parentLayout' => array_intersect_key(
    681                                         $block['parentLayout'] ?? array(),
    682                                         array_flip(
    683                                                 array( 'minimumColumnWidth', 'columnCount' )
    684                                         )
    685                                 ),
    686                         ),
     941                        $container_content_hash_input,
    687942                        'wp-container-content-'
    688943                );
    689944
    690                 $child_layout_declarations = array();
    691                 $child_layout_styles       = array();
    692 
    693                 $self_stretch = $child_layout['selfStretch'] ?? null;
    694 
    695                 if ( 'fixed' === $self_stretch && isset( $child_layout['flexSize'] ) ) {
    696                         $child_layout_declarations['flex-basis'] = $child_layout['flexSize'];
    697                         $child_layout_declarations['box-sizing'] = 'border-box';
    698                 } elseif ( 'fill' === $self_stretch ) {
    699                         $child_layout_declarations['flex-grow'] = '1';
    700                 }
    701 
    702                 if ( isset( $child_layout['columnSpan'] ) ) {
    703                         $column_span                              = $child_layout['columnSpan'];
    704                         $child_layout_declarations['grid-column'] = "span $column_span";
    705                 }
    706                 if ( isset( $child_layout['rowSpan'] ) ) {
    707                         $row_span                              = $child_layout['rowSpan'];
    708                         $child_layout_declarations['grid-row'] = "span $row_span";
    709                 }
    710                 $child_layout_styles[] = array(
    711                         'selector'     => ".$container_content_class",
    712                         'declarations' => $child_layout_declarations,
    713                 );
     945                $child_layout_styles = wp_get_child_layout_style_rules( ".$container_content_class", $base_child_layout, $parent_layout );
    714946
    715947                /*
    716                  * If columnSpan is set, and the parent grid is responsive, i.e. if it has a minimumColumnWidth set,
    717                  * the columnSpan should be removed on small grids. If there's a minimumColumnWidth, the grid is responsive.
    718                  * But if the minimumColumnWidth value wasn't changed, it won't be set. In that case, if columnCount doesn't
    719                  * exist, we can assume that the grid is responsive.
     948                 * Emit responsive child layout CSS using the same container-content class
     949                 * so that base and responsive child layout share the exact same selector.
    720950                 */
    721                 if ( isset( $child_layout['columnSpan'] ) && ( isset( $block['parentLayout']['minimumColumnWidth'] ) || ! isset( $block['parentLayout']['columnCount'] ) ) ) {
    722                         $column_span_number  = floatval( $child_layout['columnSpan'] );
    723                         $parent_column_width = $block['parentLayout']['minimumColumnWidth'] ?? '12rem';
    724                         $parent_column_value = floatval( $parent_column_width );
    725                         $parent_column_unit  = explode( $parent_column_value, $parent_column_width );
    726 
    727                         /*
    728                          * If there is no unit, the width has somehow been mangled so we reset both unit and value
    729                          * to defaults.
    730                          * Additionally, the unit should be one of px, rem or em, so that also needs to be checked.
    731                          */
    732                         if ( count( $parent_column_unit ) <= 1 ) {
    733                                 $parent_column_unit  = 'rem';
    734                                 $parent_column_value = 12;
    735                         } else {
    736                                 $parent_column_unit = $parent_column_unit[1];
    737 
    738                                 if ( ! in_array( $parent_column_unit, array( 'px', 'rem', 'em' ), true ) ) {
    739                                         $parent_column_unit = 'rem';
    740                                 }
    741                         }
    742 
    743                         /*
    744                          * A default gap value is used for this computation because custom gap values may not be
    745                          * viable to use in the computation of the container query value.
    746                          */
    747                         $default_gap_value     = 'px' === $parent_column_unit ? 24 : 1.5;
    748                         $container_query_value = $column_span_number * $parent_column_value + ( $column_span_number - 1 ) * $default_gap_value;
    749                         $container_query_value = $container_query_value . $parent_column_unit;
    750 
    751                         $child_layout_styles[] = array(
    752                                 'rules_group'  => "@container (max-width: $container_query_value )",
    753                                 'selector'     => ".$container_content_class",
    754                                 'declarations' => array(
    755                                         'grid-column' => '1/-1',
    756                                 ),
     951                foreach ( $viewport_child_layouts as $viewport_data ) {
     952                        $viewport_child_styles = wp_get_child_layout_style_rules(
     953                                ".$container_content_class",
     954                                $base_child_layout,
     955                                $parent_layout,
     956                                $viewport_data['child_layout']
    757957                        );
     958
     959                        foreach ( $viewport_child_styles as $index => $rule ) {
     960                                $viewport_child_styles[ $index ]['rules_group'] = $viewport_data['media_query'];
     961                        }
     962
     963                        $child_layout_styles = array_merge( $child_layout_styles, $viewport_child_styles );
    758964                }
    759965
     
    8591065        if ( ! current_theme_supports( 'disable-layout-styles' ) ) {
    8601066
    861                 $gap_value = $block['attrs']['style']['spacing']['blockGap'] ?? null;
    862                 /*
    863                  * Skip if gap value contains unsupported characters.
    864                  * Regex for CSS value borrowed from `safecss_filter_attr`, and used here
    865                  * to only match against the value, not the CSS attribute.
    866                  */
    867                 if ( is_array( $gap_value ) ) {
    868                         foreach ( $gap_value as $key => $value ) {
    869                                 $gap_value[ $key ] = $value && preg_match( '%[\\\(&=}]|/\*%', $value ) ? null : $value;
    870                         }
    871                 } else {
    872                         $gap_value = $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
    873                 }
    874 
     1067                $gap_value          = wp_sanitize_block_gap_value( $style_attr['spacing']['blockGap'] ?? null );
    8751068                $fallback_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? '0.5em';
    876                 $block_spacing      = $block['attrs']['style']['spacing'] ?? null;
     1069                $block_spacing      = $style_attr['spacing'] ?? null;
    8771070
    8781071                /*
     
    9111104                }
    9121105
     1106                $container_class_hash_input = array(
     1107                        $used_layout,
     1108                        $has_block_gap_support,
     1109                        $gap_value,
     1110                        $should_skip_gap_serialization,
     1111                        $fallback_gap_value,
     1112                        $block_spacing,
     1113                );
     1114
     1115                foreach ( array_keys( WP_Theme_JSON::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
     1116                        $viewport_style = $style_attr[ $breakpoint ] ?? null;
     1117                        if ( ! is_array( $viewport_style ) ) {
     1118                                continue;
     1119                        }
     1120
     1121                        $viewport_container_layout = wp_get_layout_container_values( $viewport_style['layout'] ?? null );
     1122                        if ( ! empty( $viewport_container_layout ) ) {
     1123                                $container_class_hash_input[] = array(
     1124                                        'breakpoint' => $breakpoint,
     1125                                        'layout'     => $viewport_container_layout,
     1126                                );
     1127                        }
     1128
     1129                        if ( isset( $viewport_style['spacing']['blockGap'] ) ) {
     1130                                $container_class_hash_input[] = array(
     1131                                        'breakpoint' => $breakpoint,
     1132                                        'blockGap'   => wp_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] ),
     1133                                );
     1134                        }
     1135                }
     1136
    9131137                /*
    9141138                 * Generates a unique ID based on all the data required to obtain the
     
    9191143                 */
    9201144                $container_class = wp_unique_id_from_values(
    921                         array(
    922                                 $used_layout,
    923                                 $has_block_gap_support,
    924                                 $gap_value,
    925                                 $should_skip_gap_serialization,
    926                                 $fallback_gap_value,
    927                                 $block_spacing,
    928                         ),
     1145                        $container_class_hash_input,
    9291146                        'wp-container-' . sanitize_title( $block['blockName'] ) . '-is-layout-'
    9301147                );
     
    9391156                        $block_spacing
    9401157                );
     1158
     1159                /*
     1160                 * Emit responsive container layout styles using the same $container_class
     1161                 * selector as the base layout so they target the inner block wrapper.
     1162                 */
     1163                foreach ( WP_Theme_JSON::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
     1164                        $viewport_style = $style_attr[ $breakpoint ] ?? null;
     1165                        if ( ! is_array( $viewport_style ) ) {
     1166                                continue;
     1167                        }
     1168
     1169                        $viewport_container_layout = wp_get_layout_container_values( $viewport_style['layout'] ?? null );
     1170                        $has_viewport_layout       = ! empty( $viewport_container_layout );
     1171                        $has_viewport_block_gap    = isset( $viewport_style['spacing']['blockGap'] );
     1172
     1173                        if ( ! $has_viewport_layout && ! $has_viewport_block_gap ) {
     1174                                continue;
     1175                        }
     1176
     1177                        $viewport_gap_value = $has_viewport_block_gap
     1178                                ? wp_sanitize_block_gap_value( $viewport_style['spacing']['blockGap'] )
     1179                                : $gap_value;
     1180
     1181                        $viewport_block_spacing = is_array( $viewport_style['spacing'] ?? null )
     1182                                ? array_replace( is_array( $block_spacing ) ? $block_spacing : array(), $viewport_style['spacing'] )
     1183                                : $block_spacing;
     1184
     1185                        $viewport_styles = wp_get_layout_style(
     1186                                ".$container_class",
     1187                                $used_layout,
     1188                                $has_block_gap_support,
     1189                                $viewport_gap_value,
     1190                                $should_skip_gap_serialization,
     1191                                $fallback_gap_value,
     1192                                $viewport_block_spacing,
     1193                                array(
     1194                                        'rules_group'            => $media_query,
     1195                                        'viewport_overrides'     => $viewport_container_layout,
     1196                                        'has_block_gap_override' => $has_viewport_block_gap,
     1197                                )
     1198                        );
     1199
     1200                        if ( ! empty( $viewport_styles ) && ! in_array( $container_class, $class_names, true ) ) {
     1201                                $class_names[] = $container_class;
     1202                        }
     1203                }
    9411204
    9421205                // Only add container class and enqueue block support styles if unique styles were generated.
  • trunk/src/wp-includes/block-supports/states.php

    r62453 r62478  
    4949 */
    5050function wp_normalize_state_style_for_css_output( $style ) {
    51         return wp_normalize_state_preset_vars( $style );
     51        // Layout is processed separately by wp_render_layout_support_flag(), so we remove it before declaration generation.
     52        unset( $style['layout'] );
     53        $style = wp_normalize_state_preset_vars( $style );
     54        return $style;
    5255}
    5356
     
    445448         * State declarations need !important to apply reliably over inline styles and
    446449         * preset utility classes such as .has-accent-3-background-color.
     450         *
     451         * Layout-driven state styles (responsive layout, blockGap, child layout) are
     452         * handled by wp_render_layout_support_flag() so they share a selector with
     453         * the base layout and target the correct (inner) wrapper element.
    447454         */
    448455        $style_rules = array();
  • trunk/tests/phpunit/tests/block-supports/states.php

    r62453 r62478  
    3838         * @param string $block_name Block name.
    3939         * @param array  $selectors  Optional block selectors, e.g. array( 'root' => '.foo .bar' ).
     40         * @param array  $supports   Optional block supports.
    4041         * @return WP_Block_Type
    4142         */
    42         private function ensure_block_registered( $block_name, $selectors = array() ) {
     43        private function ensure_block_registered( $block_name, $selectors = array(), $supports = array() ) {
    4344                $registered_block = WP_Block_Type_Registry::get_instance()->get_registered( $block_name );
    4445                if ( $registered_block ) {
     
    5758                if ( ! empty( $selectors ) ) {
    5859                        $args['selectors'] = $selectors;
     60                }
     61                if ( ! empty( $supports ) ) {
     62                        $args['supports'] = $supports;
    5963                }
    6064                register_block_type( $block_name, $args );
     
    838842
    839843        /**
     844         * Tests that a responsive block gap state generates layout spacing CSS.
     845         *
     846         * Responsive layout CSS is owned by wp_render_layout_support_flag()
     847         * so it shares a selector with the base layout (the inner block wrapper for
     848         * wrapper blocks) instead of being scoped to a separate `wp-states-...` class.
     849         *
     850         * @covers ::wp_render_layout_support_flag
     851         *
     852         * @ticket 65164
     853         */
     854        public function test_responsive_block_gap_state_generates_layout_spacing_css() {
     855                $this->ensure_block_registered(
     856                        'test/responsive-flow-layout-state',
     857                        array(),
     858                        array(
     859                                'layout'  => array(
     860                                        'default' => array(
     861                                                'type' => 'default',
     862                                        ),
     863                                ),
     864                                'spacing' => array(
     865                                        'blockGap' => true,
     866                                ),
     867                        )
     868                );
     869
     870                add_theme_support( 'appearance-tools' );
     871                WP_Theme_JSON_Resolver::clean_cached_data();
     872
     873                try {
     874                        $block_content = '<div class="wp-block-test"><p>One</p><p>Two</p></div>';
     875                        $block         = array(
     876                                'blockName'    => 'test/responsive-flow-layout-state',
     877                                'innerContent' => array( '<div class="wp-block-test">', null, '</div>' ),
     878                                'attrs'        => array(
     879                                        'layout' => array(
     880                                                'type' => 'default',
     881                                        ),
     882                                        'style'  => array(
     883                                                'mobile' => array(
     884                                                        'spacing' => array(
     885                                                                'blockGap' => '12px',
     886                                                        ),
     887                                                ),
     888                                        ),
     889                                ),
     890                        );
     891
     892                        $actual = wp_render_layout_support_flag( $block_content, $block );
     893                        preg_match( '/wp-container-test-responsive-flow-layout-state-is-layout-[a-f0-9]{8}/', $actual, $matches );
     894                        $this->assertNotEmpty( $matches, "wp-container class missing in: $actual" );
     895                        $container_class   = $matches[0];
     896                        $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
     897
     898                        $this->assertStringContainsString(
     899                                '@media (width <= 480px){.' . $container_class . ' > *{margin-block-start:0;margin-block-end:0;}}',
     900                                $actual_stylesheet
     901                        );
     902                        $this->assertStringContainsString(
     903                                '@media (width <= 480px){.' . $container_class . ' > * + *{margin-block-start:12px;margin-block-end:0;}}',
     904                                $actual_stylesheet
     905                        );
     906                } finally {
     907                        remove_theme_support( 'appearance-tools' );
     908                        WP_Theme_JSON_Resolver::clean_cached_data();
     909                }
     910        }
     911
     912        /**
     913         * Tests that responsive block gap state CSS uses the block's active layout type.
     914         *
     915         * @covers ::wp_render_layout_support_flag
     916         *
     917         * @ticket 65164
     918         */
     919        public function test_responsive_block_gap_state_uses_active_layout_type() {
     920                $this->ensure_block_registered(
     921                        'test/responsive-flex-layout-state',
     922                        array(),
     923                        array(
     924                                'layout'  => array(
     925                                        'default' => array(
     926                                                'type' => 'flex',
     927                                        ),
     928                                ),
     929                                'spacing' => array(
     930                                        'blockGap' => true,
     931                                ),
     932                        )
     933                );
     934
     935                add_theme_support( 'appearance-tools' );
     936                WP_Theme_JSON_Resolver::clean_cached_data();
     937
     938                try {
     939                        $block_content = '<div class="wp-block-test"><p>One</p><p>Two</p></div>';
     940                        $block         = array(
     941                                'blockName'    => 'test/responsive-flex-layout-state',
     942                                'innerContent' => array( '<div class="wp-block-test">', null, '</div>' ),
     943                                'attrs'        => array(
     944                                        'layout' => array(
     945                                                'type' => 'flex',
     946                                        ),
     947                                        'style'  => array(
     948                                                'mobile' => array(
     949                                                        'spacing' => array(
     950                                                                'blockGap' => '12px',
     951                                                        ),
     952                                                ),
     953                                        ),
     954                                ),
     955                        );
     956
     957                        $actual = wp_render_layout_support_flag( $block_content, $block );
     958                        preg_match( '/wp-container-test-responsive-flex-layout-state-is-layout-[a-f0-9]{8}/', $actual, $matches );
     959                        $this->assertNotEmpty( $matches, "wp-container class missing in: $actual" );
     960                        $container_class   = $matches[0];
     961                        $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
     962
     963                        $this->assertStringContainsString(
     964                                '@media (width <= 480px){.' . $container_class . '{gap:12px;}}',
     965                                $actual_stylesheet
     966                        );
     967                } finally {
     968                        remove_theme_support( 'appearance-tools' );
     969                        WP_Theme_JSON_Resolver::clean_cached_data();
     970                }
     971        }
     972
     973        /**
     974         * Tests that responsive layout state CSS can override grid layout values.
     975         *
     976         * @covers ::wp_render_layout_support_flag
     977         *
     978         * @ticket 65164
     979         */
     980        public function test_responsive_layout_state_generates_grid_layout_css() {
     981                $this->ensure_block_registered(
     982                        'test/responsive-grid-layout-state',
     983                        array(),
     984                        array(
     985                                'layout' => array(
     986                                        'default' => array(
     987                                                'type' => 'grid',
     988                                        ),
     989                                ),
     990                        )
     991                );
     992
     993                $block_content = '<div class="wp-block-test"><p>One</p><p>Two</p></div>';
     994                $block         = array(
     995                        'blockName'    => 'test/responsive-grid-layout-state',
     996                        'innerContent' => array( '<div class="wp-block-test">', null, '</div>' ),
     997                        'attrs'        => array(
     998                                'layout' => array(
     999                                        'type' => 'grid',
     1000                                ),
     1001                                'style'  => array(
     1002                                        'mobile' => array(
     1003                                                'layout' => array(
     1004                                                        'minimumColumnWidth' => '8rem',
     1005                                                ),
     1006                                        ),
     1007                                ),
     1008                        ),
     1009                );
     1010
     1011                $actual = wp_render_layout_support_flag( $block_content, $block );
     1012                preg_match( '/wp-container-test-responsive-grid-layout-state-is-layout-[a-f0-9]{8}/', $actual, $matches );
     1013                $this->assertNotEmpty( $matches, "wp-container class missing in: $actual" );
     1014                $container_class   = $matches[0];
     1015                $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
     1016
     1017                $this->assertStringContainsString(
     1018                        '@media (width <= 480px){.' . $container_class . '{grid-template-columns:repeat(auto-fill, minmax(min(8rem, 100%), 1fr));}}',
     1019                        $actual_stylesheet
     1020                );
     1021        }
     1022
     1023        /**
     1024         * Tests that responsive layout state CSS can override grid columns.
     1025         *
     1026         * @covers ::wp_render_layout_support_flag
     1027         *
     1028         * @ticket 65164
     1029         */
     1030        public function test_responsive_layout_state_generates_grid_column_count_css() {
     1031                $this->ensure_block_registered(
     1032                        'test/responsive-grid-column-layout-state',
     1033                        array(),
     1034                        array(
     1035                                'layout' => array(
     1036                                        'default' => array(
     1037                                                'type' => 'grid',
     1038                                        ),
     1039                                ),
     1040                        )
     1041                );
     1042
     1043                $block_content = '<div class="wp-block-test"><p>One</p><p>Two</p></div>';
     1044                $block         = array(
     1045                        'blockName'    => 'test/responsive-grid-column-layout-state',
     1046                        'innerContent' => array( '<div class="wp-block-test">', null, '</div>' ),
     1047                        'attrs'        => array(
     1048                                'layout' => array(
     1049                                        'type' => 'grid',
     1050                                ),
     1051                                'style'  => array(
     1052                                        'mobile' => array(
     1053                                                'layout' => array(
     1054                                                        'columnCount' => 3,
     1055                                                ),
     1056                                        ),
     1057                                ),
     1058                        ),
     1059                );
     1060
     1061                $actual = wp_render_layout_support_flag( $block_content, $block );
     1062                preg_match( '/wp-container-test-responsive-grid-column-layout-state-is-layout-[a-f0-9]{8}/', $actual, $matches );
     1063                $this->assertNotEmpty( $matches, "wp-container class missing in: $actual" );
     1064                $container_class   = $matches[0];
     1065                $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
     1066
     1067                $this->assertStringContainsString(
     1068                        '@media (width <= 480px){.' . $container_class . '{grid-template-columns:repeat(3, minmax(0, 1fr));}}',
     1069                        $actual_stylesheet
     1070                );
     1071        }
     1072
     1073        /**
     1074         * Tests that different responsive layout states generate different container
     1075         * classes, even when the base layout configuration is identical.
     1076         *
     1077         * @covers ::wp_render_layout_support_flag
     1078         *
     1079         * @ticket 65164
     1080         */
     1081        public function test_responsive_layout_state_generates_distinct_container_classes_for_distinct_viewport_styles() {
     1082                $this->ensure_block_registered(
     1083                        'test/responsive-grid-distinct-layout-state',
     1084                        array(),
     1085                        array(
     1086                                'layout' => array(
     1087                                        'default' => array(
     1088                                                'type' => 'grid',
     1089                                        ),
     1090                                ),
     1091                        )
     1092                );
     1093
     1094                $block_content = '<div class="wp-block-test"><p>One</p><p>Two</p></div>';
     1095                $base_block    = array(
     1096                        'blockName'    => 'test/responsive-grid-distinct-layout-state',
     1097                        'innerContent' => array( '<div class="wp-block-test">', null, '</div>' ),
     1098                        'attrs'        => array(
     1099                                'layout' => array(
     1100                                        'type' => 'grid',
     1101                                ),
     1102                        ),
     1103                );
     1104                $first_block   = array_replace_recursive(
     1105                        $base_block,
     1106                        array(
     1107                                'attrs' => array(
     1108                                        'style' => array(
     1109                                                'mobile' => array(
     1110                                                        'layout' => array(
     1111                                                                'columnCount' => 3,
     1112                                                        ),
     1113                                                ),
     1114                                        ),
     1115                                ),
     1116                        )
     1117                );
     1118                $second_block  = array_replace_recursive(
     1119                        $base_block,
     1120                        array(
     1121                                'attrs' => array(
     1122                                        'style' => array(
     1123                                                'mobile' => array(
     1124                                                        'layout' => array(
     1125                                                                'columnCount' => 4,
     1126                                                        ),
     1127                                                ),
     1128                                        ),
     1129                                ),
     1130                        )
     1131                );
     1132
     1133                $first_actual  = wp_render_layout_support_flag( $block_content, $first_block );
     1134                $second_actual = wp_render_layout_support_flag( $block_content, $second_block );
     1135
     1136                preg_match( '/wp-container-test-responsive-grid-distinct-layout-state-is-layout-[a-f0-9]{8}/', $first_actual, $first_matches );
     1137                preg_match( '/wp-container-test-responsive-grid-distinct-layout-state-is-layout-[a-f0-9]{8}/', $second_actual, $second_matches );
     1138
     1139                $this->assertNotEmpty( $first_matches, "wp-container class missing in: $first_actual" );
     1140                $this->assertNotEmpty( $second_matches, "wp-container class missing in: $second_actual" );
     1141
     1142                $first_container_class  = $first_matches[0];
     1143                $second_container_class = $second_matches[0];
     1144
     1145                $this->assertNotSame( $first_container_class, $second_container_class );
     1146
     1147                $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
     1148
     1149                $this->assertStringContainsString(
     1150                        '@media (width <= 480px){.' . $first_container_class . '{grid-template-columns:repeat(3, minmax(0, 1fr));}}',
     1151                        $actual_stylesheet
     1152                );
     1153                $this->assertStringContainsString(
     1154                        '@media (width <= 480px){.' . $second_container_class . '{grid-template-columns:repeat(4, minmax(0, 1fr));}}',
     1155                        $actual_stylesheet
     1156                );
     1157        }
     1158
     1159        /**
     1160         * Tests that responsive grid layout and block gap state CSS are both generated.
     1161         *
     1162         * @covers ::wp_render_layout_support_flag
     1163         *
     1164         * @ticket 65164
     1165         */
     1166        public function test_responsive_layout_state_generates_grid_columns_and_gap_css() {
     1167                $this->ensure_block_registered(
     1168                        'test/responsive-grid-columns-gap-layout-state',
     1169                        array(),
     1170                        array(
     1171                                'layout'  => array(
     1172                                        'default' => array(
     1173                                                'type' => 'grid',
     1174                                        ),
     1175                                ),
     1176                                'spacing' => array(
     1177                                        'blockGap' => true,
     1178                                ),
     1179                        )
     1180                );
     1181
     1182                add_theme_support( 'appearance-tools' );
     1183                WP_Theme_JSON_Resolver::clean_cached_data();
     1184
     1185                try {
     1186                        $block_content = '<div class="wp-block-test"><p>One</p><p>Two</p></div>';
     1187                        $block         = array(
     1188                                'blockName'    => 'test/responsive-grid-columns-gap-layout-state',
     1189                                'innerContent' => array( '<div class="wp-block-test">', null, '</div>' ),
     1190                                'attrs'        => array(
     1191                                        'layout' => array(
     1192                                                'type' => 'grid',
     1193                                        ),
     1194                                        'style'  => array(
     1195                                                'mobile' => array(
     1196                                                        'layout'  => array(
     1197                                                                'columnCount' => 3,
     1198                                                        ),
     1199                                                        'spacing' => array(
     1200                                                                'blockGap' => '12px',
     1201                                                        ),
     1202                                                ),
     1203                                        ),
     1204                                ),
     1205                        );
     1206
     1207                        $actual = wp_render_layout_support_flag( $block_content, $block );
     1208                        preg_match( '/wp-container-test-responsive-grid-columns-gap-layout-state-is-layout-[a-f0-9]{8}/', $actual, $matches );
     1209                        $this->assertNotEmpty( $matches, "wp-container class missing in: $actual" );
     1210                        $container_class   = $matches[0];
     1211                        $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
     1212
     1213                        $this->assertStringContainsString(
     1214                                '@media (width <= 480px){.' . $container_class . '{grid-template-columns:repeat(3, minmax(0, 1fr));gap:12px;}}',
     1215                                $actual_stylesheet
     1216                        );
     1217                } finally {
     1218                        remove_theme_support( 'appearance-tools' );
     1219                        WP_Theme_JSON_Resolver::clean_cached_data();
     1220                }
     1221        }
     1222
     1223        /**
     1224         * Tests that responsive grid block gap CSS does not repeat unchanged layout declarations.
     1225         *
     1226         * @covers ::wp_render_layout_support_flag
     1227         *
     1228         * @ticket 65164
     1229         */
     1230        public function test_responsive_grid_block_gap_state_only_outputs_changed_layout_css() {
     1231                $this->ensure_block_registered(
     1232                        'test/responsive-grid-gap-state',
     1233                        array(),
     1234                        array(
     1235                                'layout'  => array(
     1236                                        'default' => array(
     1237                                                'type'               => 'grid',
     1238                                                'minimumColumnWidth' => '12rem',
     1239                                        ),
     1240                                ),
     1241                                'spacing' => array(
     1242                                        'blockGap' => true,
     1243                                ),
     1244                        )
     1245                );
     1246
     1247                add_theme_support( 'appearance-tools' );
     1248                WP_Theme_JSON_Resolver::clean_cached_data();
     1249
     1250                try {
     1251                        $block_content = '<div class="wp-block-test"><p>One</p><p>Two</p></div>';
     1252                        $block         = array(
     1253                                'blockName'    => 'test/responsive-grid-gap-state',
     1254                                'innerContent' => array( '<div class="wp-block-test">', null, '</div>' ),
     1255                                'attrs'        => array(
     1256                                        'layout' => array(
     1257                                                'type'               => 'grid',
     1258                                                'minimumColumnWidth' => '12rem',
     1259                                        ),
     1260                                        'style'  => array(
     1261                                                'tablet' => array(
     1262                                                        'spacing' => array(
     1263                                                                'blockGap' => '12px',
     1264                                                        ),
     1265                                                ),
     1266                                        ),
     1267                                ),
     1268                        );
     1269
     1270                        $actual = wp_render_layout_support_flag( $block_content, $block );
     1271                        preg_match( '/wp-container-test-responsive-grid-gap-state-is-layout-[a-f0-9]{8}/', $actual, $matches );
     1272                        $this->assertNotEmpty( $matches, "wp-container class missing in: $actual" );
     1273                        $container_class   = $matches[0];
     1274                        $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
     1275
     1276                        $this->assertStringContainsString(
     1277                                '@media (480px < width <= 782px){.' . $container_class . '{gap:12px;}}',
     1278                                $actual_stylesheet
     1279                        );
     1280                        $this->assertStringNotContainsString(
     1281                                '@media (480px < width <= 782px){.' . $container_class . '{grid-template-columns:',
     1282                                $actual_stylesheet
     1283                        );
     1284                        $this->assertStringNotContainsString(
     1285                                '@media (480px < width <= 782px){.' . $container_class . '{container-type:',
     1286                                $actual_stylesheet
     1287                        );
     1288                } finally {
     1289                        remove_theme_support( 'appearance-tools' );
     1290                        WP_Theme_JSON_Resolver::clean_cached_data();
     1291                }
     1292        }
     1293
     1294        /**
     1295         * Tests that responsive child layout state CSS is generated.
     1296         *
     1297         * @covers ::wp_render_layout_support_flag
     1298         *
     1299         * @ticket 65164
     1300         */
     1301        public function test_responsive_child_layout_state_generates_grid_span_css() {
     1302                $this->ensure_block_registered( 'test/responsive-child-layout-state' );
     1303
     1304                $block_content = '<p>Some text.</p>';
     1305                $block         = array(
     1306                        'blockName'    => 'test/responsive-child-layout-state',
     1307                        'innerContent' => array( '<p>Some text.</p>' ),
     1308                        'attrs'        => array(
     1309                                'style' => array(
     1310                                        'mobile' => array(
     1311                                                'layout' => array(
     1312                                                        'columnSpan' => '2',
     1313                                                ),
     1314                                        ),
     1315                                ),
     1316                        ),
     1317                        'parentLayout' => array(
     1318                                'type'        => 'grid',
     1319                                'columnCount' => 3,
     1320                        ),
     1321                );
     1322
     1323                $actual = wp_render_layout_support_flag( $block_content, $block );
     1324                preg_match( '/wp-container-content-[a-f0-9]{8}/', $actual, $matches );
     1325                $this->assertNotEmpty( $matches, "wp-container-content class missing in: $actual" );
     1326                $container_content_class = $matches[0];
     1327                $actual_stylesheet       = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
     1328
     1329                $this->assertStringContainsString(
     1330                        '@media (width <= 480px){.' . $container_content_class . '{grid-column:span 2;}}',
     1331                        $actual_stylesheet
     1332                );
     1333        }
     1334
     1335        /**
     1336         * Tests that a wrapper block (markup with an inner content wrapper) receives
     1337         * responsive grid layout CSS scoped to the inner wrapper, not the outermost tag.
     1338         *
     1339         * Regression test for the bug where wp-states-... was added to the outer tag
     1340         * while the wp-container-... layout class lives on the inner wrapper, causing
     1341         * the responsive @media rule to apply to the wrong element.
     1342         *
     1343         * @covers ::wp_render_layout_support_flag
     1344         *
     1345         * @ticket 65164
     1346         */
     1347        public function test_responsive_layout_state_targets_inner_wrapper_for_wrapper_blocks() {
     1348                $this->ensure_block_registered(
     1349                        'test/responsive-wrapper-grid-state',
     1350                        array(),
     1351                        array(
     1352                                'layout' => array(
     1353                                        'default' => array(
     1354                                                'type' => 'grid',
     1355                                        ),
     1356                                ),
     1357                        )
     1358                );
     1359
     1360                $block_content = '<div class="wp-block-wrapper"><div class="wp-block-wrapper__inner-container"><p>One</p></div></div>';
     1361                $block         = array(
     1362                        'blockName'    => 'test/responsive-wrapper-grid-state',
     1363                        'innerContent' => array(
     1364                                '<div class="wp-block-wrapper"><div class="wp-block-wrapper__inner-container">',
     1365                                null,
     1366                                '</div></div>',
     1367                        ),
     1368                        'attrs'        => array(
     1369                                'layout' => array(
     1370                                        'type' => 'grid',
     1371                                ),
     1372                                'style'  => array(
     1373                                        'mobile' => array(
     1374                                                'layout' => array(
     1375                                                        'columnCount' => 3,
     1376                                                ),
     1377                                        ),
     1378                                ),
     1379                        ),
     1380                );
     1381
     1382                $actual = wp_render_layout_support_flag( $block_content, $block );
     1383
     1384                // The wp-container-...-is-layout-... class should land on the inner wrapper.
     1385                $this->assertMatchesRegularExpression(
     1386                        '/<div class="wp-block-wrapper__inner-container [^"]*wp-container-test-responsive-wrapper-grid-state-is-layout-[a-f0-9]{8}/',
     1387                        $actual
     1388                );
     1389
     1390                preg_match( '/wp-container-test-responsive-wrapper-grid-state-is-layout-[a-f0-9]{8}/', $actual, $matches );
     1391                $container_class   = $matches[0];
     1392                $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
     1393
     1394                // The responsive @media rule must target the same selector that lives on
     1395                // the inner wrapper element.
     1396                $this->assertStringContainsString(
     1397                        '@media (width <= 480px){.' . $container_class . '{grid-template-columns:repeat(3, minmax(0, 1fr));}}',
     1398                        $actual_stylesheet
     1399                );
     1400        }
     1401
     1402        /**
    8401403         * Tests that state declarations are marked important.
    8411404         *
Note: See TracChangeset for help on using the changeset viewer.