Commit 4595486a05 for wordpress.org

commit 4595486a05d28d4b78f46e5b0a2173500d054b4f
Author: aaronrobertshaw <aaronrobertshaw@git.wordpress.org>
Date:   Tue Oct 20 06:26:54 2026 +0000

    Editor: Scope state rules to the block's own element.

    Per-instance state styles never applied to a block whose root selector starts with another block's class. List Item registers `.wp-block-list > li`, so scoping swapped `.wp-block-list` for the instance class and produced `.wp-states-xxxx > li`, which matches nothing because the instance class sits on the `li` itself.

    A new `wp_get_state_selector_ancestor()` returns the leading compound and its combinator when that compound names a different block, and `wp_build_state_selector()` gains a `$block_name` parameter so the ancestor is kept and the instance class lands on the block's own compound, giving `.wp-block-list > .wp-states-xxxx`. Selectors for nested elements, such as `.wp-block-button .wp-block-button__link`, keep their existing behavior.

    This brings in changes from https://github.com/WordPress/gutenberg/pull/83986.

    Developed in: https://github.com/WordPress/wordpress-develop/pull/14039

    Props ramonopoly, talldanwp.
    Fixes #66252.

    Built from https://develop.svn.wordpress.org/trunk@64591


    git-svn-id: http://core.svn.wordpress.org/trunk@63741 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/block-supports/states.php b/wp-includes/block-supports/states.php
index 2657558486..2a80c26db4 100644
--- a/wp-includes/block-supports/states.php
+++ b/wp-includes/block-supports/states.php
@@ -611,17 +611,74 @@ function wp_split_selector_list( $selector ) {
 	return $selectors;
 }

+/**
+ * Returns the ancestor part of a block selector whose leading compound names
+ * a different block, e.g. `.wp-block-list > ` in List Item's `.wp-block-list > li`.
+ *
+ * The block-instance class is on the block's own wrapper, so a leading compound
+ * naming another block must be kept rather than swapped for that class. When the
+ * next compound carries a `wp-block-*` class (`.wp-block-button .wp-block-button__link`),
+ * the selector describes a nested element and keeps the leading swap.
+ *
+ * @since 7.2.0
+ *
+ * @param string      $selector   A single block or feature selector.
+ * @param string|null $block_name Name of the block being scoped.
+ * @return string The leading compound and its combinator, or an empty string.
+ */
+function wp_get_state_selector_ancestor( $selector, $block_name ) {
+	if (
+		! is_string( $block_name ) ||
+		'' === $block_name ||
+		! preg_match( '/^\.(wp-block-[-_a-zA-Z0-9]+)/', $selector, $matches ) ||
+		wp_get_block_default_classname( $block_name ) === $matches[1]
+	) {
+		return '';
+	}
+
+	$ancestor_length = 0;
+	$next_compound   = '';
+	$depth           = 0;
+	$selector_length = strlen( $selector );
+
+	for ( $i = 0; $i < $selector_length; $i++ ) {
+		$char = $selector[ $i ];
+
+		if ( '(' === $char || '[' === $char ) {
+			++$depth;
+		} elseif ( ( ')' === $char || ']' === $char ) && $depth > 0 ) {
+			--$depth;
+		} elseif ( 0 === $depth && str_contains( " \t\n\r\f>+~", $char ) ) {
+			if ( $ancestor_length ) {
+				break;
+			}
+			$ancestor_length = $i + strspn( $selector, " \t\n\r\f>+~", $i );
+			$i               = $ancestor_length - 1;
+		} elseif ( 0 === $depth && $ancestor_length ) {
+			$next_compound .= $char;
+		}
+	}
+
+	if ( '' === $next_compound || str_contains( $next_compound, '.wp-block-' ) ) {
+		return '';
+	}
+
+	return substr( $selector, 0, $ancestor_length );
+}
+
 /**
  * Builds a scoped selector from a block selector and optional pseudo-state.
  *
  * @since 7.1.0
+ * @since 7.2.0 Added the `$block_name` parameter.
  *
  * @param string      $base_selector  Block-instance scoping selector.
  * @param string|null $block_selector Block or feature selector from metadata.
  * @param string      $state          Pseudo-state selector.
+ * @param string|null $block_name     Optional. Name of the block being scoped.
  * @return string Scoped selector.
  */
-function wp_build_state_selector( $base_selector, $block_selector, $state ) {
+function wp_build_state_selector( $base_selector, $block_selector, $state, $block_name = null ) {
 	if ( ! is_string( $block_selector ) || '' === trim( $block_selector ) ) {
 		return $base_selector . $state;
 	}
@@ -635,6 +692,9 @@ function wp_build_state_selector( $base_selector, $block_selector, $state ) {
 			continue;
 		}

+		$ancestor = wp_get_state_selector_ancestor( $selector, $block_name );
+		$selector = substr( $selector, strlen( $ancestor ) );
+
 		/*
 		 * Replace only the leading block selector part (e.g. class name,
 		 * attribute selector, ID, or tag name) with the block instance selector.
@@ -642,11 +702,11 @@ function wp_build_state_selector( $base_selector, $block_selector, $state ) {
 		 * same element and combinators without spaces.
 		 */
 		if ( preg_match( '/^([.#]?[-_a-zA-Z0-9]+|\[[^\]]+\])/', $selector, $matches ) ) {
-			$scoped_selectors[] = $base_selector . substr( $selector, strlen( $matches[0] ) ) . $state;
+			$scoped_selectors[] = $ancestor . $base_selector . substr( $selector, strlen( $matches[0] ) ) . $state;
 			continue;
 		}

-		$scoped_selectors[] = $base_selector . $state;
+		$scoped_selectors[] = $ancestor . $base_selector . $state;
 	}

 	return empty( $scoped_selectors )
@@ -839,7 +899,8 @@ function wp_render_block_states_support( $block_content, $block ) {
 		$selector             = wp_build_state_selector(
 			".$unique_class",
 			$rule['selector'],
-			$rule['state']
+			$rule['state'],
+			$block_name
 		);
 		$important_style_rule = array(
 			'selector'     => $selector,
diff --git a/wp-includes/version.php b/wp-includes/version.php
index ce61ecff64..86ff5de376 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-alpha-64590';
+$wp_version = '7.2-alpha-64591';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.