Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Layout Support: Replace incremental IDs with hashes #68210

Open
wants to merge 11 commits into
base: trunk
Choose a base branch
from
75 changes: 64 additions & 11 deletions lib/block-supports/layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,24 @@ function gutenberg_incremental_id_per_prefix( $prefix = '' ) {
return $prefix . (string) ++$id_counters[ $prefix ];
}

/**
* Generates a unique ID based on the structure and values of a given array.
*
* This function serializes the array into a JSON string and generates a hash
* that serves as a unique identifier. Optionally, a prefix can be added to
* the generated ID for context or categorization.
*
* @param array $data The input array to generate an ID from.
* @param string $prefix Optional. A prefix to prepend to the generated ID. Default ''.
*
* @return string The generated unique ID for the array.
*/
function gutenberg_unique_id_from_values( array $data, string $prefix = '' ): string {
$serialized = wp_json_encode( $data );
$hash = substr( md5( $serialized ), 0, 8 );
return $prefix . $hash;
}

/**
* Renders the layout config to the block wrapper.
*
Expand All @@ -603,7 +621,33 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {

// Child layout specific logic.
if ( $child_layout ) {
$container_content_class = wp_unique_prefixed_id( 'wp-container-content-' );
/*
* Generates a unique class for child block layout styles.
*
* To ensure consistent class generation across different page renders,
* only properties that affect layout styling are used. These properties
* come from `$block['attrs']['style']['layout']` and `$block['parentLayout']`.
*
* As long as these properties coincide, the generated class will be the same.
*/
$container_content_class = gutenberg_unique_id_from_values(
array(
'layout' => array_intersect_key(
$block['attrs']['style']['layout'] ?? array(),
array_flip(
array( 'selfStretch', 'flexSize', 'columnStart', 'columnSpan', 'rowStart', 'rowSpan' )
)
),
'parentLayout' => array_intersect_key(
$block['parentLayout'] ?? array(),
array_flip(
array( 'minimumColumnWidth', 'columnCount' )
)
),
),
'wp-container-content-'
);

$child_layout_declarations = array();
$child_layout_styles = array();

Expand Down Expand Up @@ -755,16 +799,6 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
$class_names = array();
$layout_definitions = gutenberg_get_layout_definitions();

/*
* We use an incremental ID that is independent per prefix to make sure that
* rendering different numbers of blocks doesn't affect the IDs of other
* blocks. We need this to make the CSS class names stable across paginations
* for features like the enhanced pagination of the Query block.
*/
$container_class = gutenberg_incremental_id_per_prefix(
'wp-container-' . sanitize_title( $block['blockName'] ) . '-is-layout-'
);

// Set the correct layout type for blocks using legacy content width.
if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] || isset( $used_layout['contentSize'] ) && $used_layout['contentSize'] ) {
$used_layout['type'] = 'constrained';
Expand Down Expand Up @@ -838,6 +872,25 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
$block_gap = $global_settings['spacing']['blockGap'] ?? null;
$has_block_gap_support = isset( $block_gap );

/*
* We generate a unique ID based on all the data required to obtain the
* corresponding layout style. This way, the CSS class names keep the same
* even for different blocks with the same layout definition. We need this to
* make the CSS class names stable across paginations for features like the
* enhanced pagination of the Query block.
*/
$container_class = gutenberg_unique_id_from_values(
array(
$used_layout,
$has_block_gap_support,
$gap_value,
$should_skip_gap_serialization,
$fallback_gap_value,
$block_spacing,
),
'wp-container-' . sanitize_title( $block['blockName'] ) . '-is-layout-'
Copy link
Member

@gziolo gziolo Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker: I have a general thought after looking at the generated CSS. The fact that the class name contains the name of the block might be no longer needed. It's currently a general-purpose class that groups similar styles. It might be interesting to explore in the follow-up PR whether the class name prefix can be simplified to the wp-container-block-is-layout-. That would be also similar to the child layouts and the wp-container-content- prefix.

Screenshot 2025-02-26 at 11 07 00

);

$style = gutenberg_get_layout_style(
".$container_class",
$used_layout,
Expand Down
185 changes: 184 additions & 1 deletion phpunit/block-supports/layout-test.php
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,47 @@
),
),
),
'expected_output' => '<p class="wp-container-content-1">Some text.</p>', // The generated classname number assumes `wp_unique_prefixed_id( 'wp-container-content-' )` will not have run previously in this test.
'expected_output' => '<p class="wp-container-content-b7aa651c">Some text.</p>',
),
'single wrapper block layout with flex type' => array(
'args' => array(
'block_content' => '<div class="wp-block-group"></div>',
'block' => array(
'blockName' => 'core/group',
'attrs' => array(
'layout' => array(
'type' => 'flex',
'orientation' => 'horizontal',
'flexWrap' => 'nowrap',
),
),
'innerBlocks' => array(),
'innerHTML' => '<div class="wp-block-group"></div>',
'innerContent' => array(
'<div class="wp-block-group"></div>',
),
),
),
'expected_output' => '<div class="wp-block-group is-horizontal is-nowrap is-layout-flex wp-container-core-group-is-layout-67f0b8e2 wp-block-group-is-layout-flex"></div>',
),
'single wrapper block layout with grid type' => array(
'args' => array(
'block_content' => '<div class="wp-block-group"></div>',
'block' => array(
'blockName' => 'core/group',
'attrs' => array(
'layout' => array(
'type' => 'grid',
),
),
'innerBlocks' => array(),
'innerHTML' => '<div class="wp-block-group"></div>',
'innerContent' => array(
'<div class="wp-block-group"></div>',
),
),
),
'expected_output' => '<div class="wp-block-group is-layout-grid wp-container-core-group-is-layout-9649a0d9 wp-block-group-is-layout-grid"></div>',
),
);
}
Expand Down Expand Up @@ -593,4 +633,147 @@
),
);
}

/**
* Check that gutenberg_render_layout_support_flag() renders consistent hashes
* for the container class when the relevant layout properties are the same.
*
* @dataProvider data_layout_support_flag_renders_consistent_container_hash
*
* @covers ::gutenberg_render_layout_support_flag
*
* @param array $block_attrs Dataset to test.
* @param array $expected_hash Hash generated for the passed dataset.
*/
public function test_layout_support_flag_renders_consistent_container_hash( $block_attrs, $expected_hash ) {
switch_theme( 'default' );

$block_content = '<div class="wp-block-group"></div>';
$block = array(
'blockName' => 'core/group',
'innerBlocks' => array(),
'innerHTML' => '<div class="wp-block-group"></div>',
'innerContent' => array(
'<div class="wp-block-group"></div>',
),
'attrs' => $block_attrs,
);

/*
* The `appearance-tools` theme support is temporarily added to ensure
* that the block gap support is enabled during rendering, which is
* necessary to compute styles for layouts with block gap values.
*/
add_theme_support( 'appearance-tools' );
$output = gutenberg_render_layout_support_flag( $block_content, $block );
remove_theme_support( 'appearance-tools' );

/*
* Iterate over the list of classes of the first rendered element to find
* the hash generated for the container's layout class.
*/
$hash = '';
$processor = new WP_HTML_Tag_Processor( $output );
$matcher = '/wp-container-core-group-is-layout-([a-f0-9]{8})/';
if ( $processor->next_tag() ) {
foreach ( $processor->class_list() as $class ) {
if ( preg_match( $matcher, $class, $matches ) ) {
$hash = $matches[1];
break;
}
}
}

$this->assertSame( $expected_hash, $hash );
}

/**
* Data provider for test_layout_support_flag_renders_consistent_container_hash.
*
* @return array
*/
public function data_layout_support_flag_renders_consistent_container_hash() {
return array(
'default type block gap 12px' => array(
'block_attributes' => array(
'layout' => array(
'type' => 'default',
),
'style' => array(
'spacing' => array(
'blockGap' => '12px',
),
),
),
'expected_hash' => 'c5c7d83f',
),
'default type block gap 24px' => array(
'block_attributes' => array(
'layout' => array(
'type' => 'default',
),
'style' => array(
'spacing' => array(
'blockGap' => '24px',
),
),
),
'expected_hash' => '634f0b9d',
),
'constrained type justified left' => array(
'block_attributes' => array(
'layout' => array(
'type' => 'constrained',
'justifyContent' => 'left',
),
),
'expected_hash' => '12dd3699',
),
'constrained type justified right' => array(
'block_attributes' => array(
'layout' => array(
'type' => 'constrained',
'justifyContent' => 'right',
),
),
'expected_hash' => 'f1f2ed93',
),
'flex type horizontal' => array(

Check warning on line 741 in phpunit/block-supports/layout-test.php

View workflow job for this annotation

GitHub Actions / PHP coding standards

Array double arrow not aligned correctly; expected 13 space(s) between "'flex type horizontal'" and double arrow, but found 1.
'block_attributes' => array(
'layout' => array(
'type' => 'flex',
'orientation' => 'horizontal',
'flexWrap' => 'nowrap',
),
),
'expected_hash' => '2487dcaa',
),
'flex type vertical' => array(
'block_attributes' => array(
'layout' => array(
'type' => 'flex',
'orientation' => 'vertical',
),
),
'expected_hash' => 'fe9cc265',
),
'grid type' => array(
'block_attributes' => array(
'layout' => array(
'type' => 'grid',
),
),
'expected_hash' => '478b6e6b',
),
'grid type 3 columns' => array(
'block_attributes' => array(
'layout' => array(
'type' => 'grid',
'columnCount' => 3,
),
),
'expected_hash' => 'd3b710ac',
),
);
}
}
Loading