Make WordPress Core


Ignore:
Timestamp:
06/09/2026 04:25:45 AM (3 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.

File:
1 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.
Note: See TracChangeset for help on using the changeset viewer.