Commit 2311d0bd1b7 for woocommerce

commit 2311d0bd1b74ba671e6c632d8268231cf5916d08
Author: Ahmed <ahmed.el.azzabi@automattic.com>
Date:   Wed Jul 22 15:16:30 2026 +0300

    Fix duplicate headers on Settings UI drill-down pages (#66854)

    * fix(settings): Fix duplicate drill-down headers

    Settings UI drill-downs rendered both the legacy wc-admin header and the shell header, leaving duplicate navigation and extra vertical offset. Mark successful drill-down render contexts explicitly, hide the legacy header for them, and preserve it when schema or script resolution falls back to legacy output.

    Refs WOOPRD-3585

    * chore: Add changelog for drill-down header fix

    Document the Settings UI drill-down header correction for the next WooCommerce release.

    Refs WOOPRD-3585

    * fix(settings): Use exact class-token matching for Settings UI body classes

    The Settings UI body-class filter used str_contains() to check whether
    `woocommerce-settings-ui-page` or `woocommerce-settings-ui-drill-down`
    were already present on the body element. Substring matching means a
    differently-prefixed class added by another admin_body_class callback
    (e.g. `woocommerce-settings-ui-page-preview`) would be mistaken for the
    exact class, so the real class would silently never get added — and the
    CSS that depends on that exact token would fail to apply.

    Tokenize the class string and use strict in_array() membership checks
    instead, and add a regression test that seeds a prefixed class to lock
    in the exact-token behavior.

    Refs WOOPRD-3585

diff --git a/plugins/woocommerce/changelog/wooprd-3585-settings-ui-drill-down-header b/plugins/woocommerce/changelog/wooprd-3585-settings-ui-drill-down-header
new file mode 100644
index 00000000000..7041f638045
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooprd-3585-settings-ui-drill-down-header
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix duplicate headers on Settings UI drill-down pages.
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/settings-embed/settings-ui.scss b/plugins/woocommerce/client/admin/client/wp-admin-scripts/settings-embed/settings-ui.scss
index 6eadc8673a7..1c5c12f84a7 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/settings-embed/settings-ui.scss
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/settings-embed/settings-ui.scss
@@ -3,6 +3,16 @@
 body.woocommerce_page_wc-settings.woocommerce-settings-ui-page {
 	background-color: var(--wpds-color-background-surface-neutral);

+	&.woocommerce-settings-ui-drill-down {
+		#wpbody {
+			margin-top: 0;
+		}
+
+		.woocommerce-layout__header {
+			display: none;
+		}
+	}
+
 	#mainform,
 	#wpcontent,
 	#wpbody-content,
diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-page.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-page.php
index d89f14908b4..65127abe6f1 100644
--- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-page.php
+++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-page.php
@@ -145,11 +145,23 @@ if ( ! class_exists( 'WC_Settings_Page', false ) ) :
 				return $classes;
 			}

-			if ( str_contains( $classes, 'woocommerce-settings-ui-page' ) ) {
-				return $classes;
+			$body_classes = explode( ' ', $classes );
+
+			if ( ! in_array( 'woocommerce-settings-ui-page', $body_classes, true ) ) {
+				$classes       .= ' woocommerce-settings-ui-page';
+				$body_classes[] = 'woocommerce-settings-ui-page';
+			}
+
+			if (
+				$context->is_drill_down()
+				&& ! $context->has_schema_failed()
+				&& ! $context->has_script_handles_failed()
+				&& ! in_array( 'woocommerce-settings-ui-drill-down', $body_classes, true )
+			) {
+				$classes .= ' woocommerce-settings-ui-drill-down';
 			}

-			return "$classes woocommerce-settings-ui-page";
+			return $classes;
 		}

 		/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/SettingsUIFeatureFlagTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/SettingsUIFeatureFlagTest.php
index 0398ec41074..f73398752d8 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/SettingsUIFeatureFlagTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/SettingsUIFeatureFlagTest.php
@@ -408,7 +408,7 @@ class SettingsUIFeatureFlagTest extends WC_Unit_Test_Case {
 	}

 	/**
-	 * It adds the settings UI body class when the feature flag is enabled.
+	 * @testdox Should add only the top-level Settings UI body class for top-level pages.
 	 */
 	public function test_settings_ui_body_class_is_added_when_feature_flag_is_enabled(): void {
 		add_filter( 'woocommerce_admin_features', array( $this, 'enable_settings_ui_feature' ) );
@@ -421,6 +421,78 @@ class SettingsUIFeatureFlagTest extends WC_Unit_Test_Case {

 		$this->assertStringContainsString( 'existing-class', $classes );
 		$this->assertStringContainsString( 'woocommerce-settings-ui-page', $classes );
+		$this->assertStringNotContainsString( 'woocommerce-settings-ui-drill-down', $classes );
+	}
+
+	/**
+	 * @testdox Should add the drill-down body class for Settings UI drill-down pages.
+	 */
+	public function test_settings_ui_drill_down_body_class_is_added_for_drill_down_pages(): void {
+		add_filter( 'woocommerce_admin_features', array( $this, 'enable_settings_ui_feature' ) );
+
+		global $current_section, $current_tab;
+		$current_section = 'test_gateway';
+		$current_tab     = 'checkout';
+		$page            = $this->get_settings_ui_test_page_for_drill_down();
+
+		$classes = $page->add_settings_ui_body_class( 'existing-class woocommerce-settings-ui-page' );
+
+		$this->assertStringContainsString( 'existing-class', $classes );
+		$this->assertSame( 1, substr_count( $classes, 'woocommerce-settings-ui-page' ) );
+		$this->assertSame( 1, substr_count( $classes, 'woocommerce-settings-ui-drill-down' ) );
+	}
+
+	/**
+	 * @testdox Should add the exact Settings UI body classes even when a similarly prefixed class is already present.
+	 */
+	public function test_settings_ui_body_classes_use_exact_token_matching_against_prefixed_classes(): void {
+		add_filter( 'woocommerce_admin_features', array( $this, 'enable_settings_ui_feature' ) );
+
+		global $current_section, $current_tab;
+		$current_section = 'test_gateway';
+		$current_tab     = 'checkout';
+		$page            = $this->get_settings_ui_test_page_for_drill_down();
+
+		$classes      = $page->add_settings_ui_body_class( 'existing-class woocommerce-settings-ui-page-preview' );
+		$body_classes = explode( ' ', $classes );
+
+		$this->assertContains( 'woocommerce-settings-ui-page-preview', $body_classes );
+		$this->assertCount( 1, array_keys( $body_classes, 'woocommerce-settings-ui-page', true ) );
+		$this->assertCount( 1, array_keys( $body_classes, 'woocommerce-settings-ui-drill-down', true ) );
+	}
+
+	/**
+	 * @testdox Should not add the drill-down body class when schema generation falls back to legacy rendering.
+	 */
+	public function test_settings_ui_drill_down_body_class_is_not_added_when_schema_generation_fails(): void {
+		add_filter( 'woocommerce_admin_features', array( $this, 'enable_settings_ui_feature' ) );
+
+		global $current_section, $current_tab;
+		$current_section = 'test_gateway';
+		$current_tab     = 'checkout';
+		$page            = $this->get_settings_ui_test_page_with_failing_schema( 'checkout' );
+
+		$classes = $page->add_settings_ui_body_class( 'existing-class' );
+
+		$this->assertStringContainsString( 'woocommerce-settings-ui-page', $classes );
+		$this->assertStringNotContainsString( 'woocommerce-settings-ui-drill-down', $classes );
+	}
+
+	/**
+	 * @testdox Should not add the drill-down body class when script handle resolution falls back to legacy rendering.
+	 */
+	public function test_settings_ui_drill_down_body_class_is_not_added_when_script_handle_resolution_fails(): void {
+		add_filter( 'woocommerce_admin_features', array( $this, 'enable_settings_ui_feature' ) );
+
+		global $current_section, $current_tab;
+		$current_section = 'test_gateway';
+		$current_tab     = 'checkout';
+		$page            = $this->get_settings_ui_test_page_with_failing_script_handles( 'checkout' );
+
+		$classes = $page->add_settings_ui_body_class( 'existing-class' );
+
+		$this->assertStringContainsString( 'woocommerce-settings-ui-page', $classes );
+		$this->assertStringNotContainsString( 'woocommerce-settings-ui-drill-down', $classes );
 	}

 	/**
@@ -649,15 +721,18 @@ class SettingsUIFeatureFlagTest extends WC_Unit_Test_Case {
 	/**
 	 * Build a settings page whose settings UI adapter cannot provide script handles.
 	 *
+	 * @param string $page_id Page id.
 	 * @return \WC_Settings_Page
 	 */
-	private function get_settings_ui_test_page_with_failing_script_handles(): \WC_Settings_Page {
-		return new class() extends \WC_Settings_Page {
+	private function get_settings_ui_test_page_with_failing_script_handles( string $page_id = 'settings_ui_flag_test' ): \WC_Settings_Page {
+		return new class( $page_id ) extends \WC_Settings_Page {
 			/**
 			 * Constructor.
+			 *
+			 * @param string $page_id Page id.
 			 */
-			public function __construct() {
-				$this->id    = 'settings_ui_flag_test';
+			public function __construct( string $page_id ) {
+				$this->id    = $page_id;
 				$this->label = 'Settings UI flag test';
 			}

@@ -675,7 +750,7 @@ class SettingsUIFeatureFlagTest extends WC_Unit_Test_Case {
 					 * @return array
 					 */
 					public function get_script_handles( string $section_id ): array {
-						if ( 'advanced' === $section_id ) {
+						if ( '' !== $section_id ) {
 							throw new \RuntimeException( 'Unable to load extension script handles.' );
 						}

@@ -705,15 +780,18 @@ class SettingsUIFeatureFlagTest extends WC_Unit_Test_Case {
 	/**
 	 * Build a settings page whose settings UI adapter cannot provide a schema.
 	 *
+	 * @param string $page_id Page id.
 	 * @return \WC_Settings_Page
 	 */
-	private function get_settings_ui_test_page_with_failing_schema(): \WC_Settings_Page {
-		return new class() extends \WC_Settings_Page {
+	private function get_settings_ui_test_page_with_failing_schema( string $page_id = 'settings_ui_flag_test' ): \WC_Settings_Page {
+		return new class( $page_id ) extends \WC_Settings_Page {
 			/**
 			 * Constructor.
+			 *
+			 * @param string $page_id Page id.
 			 */
-			public function __construct() {
-				$this->id    = 'settings_ui_flag_test';
+			public function __construct( string $page_id ) {
+				$this->id    = $page_id;
 				$this->label = 'Settings UI flag test';
 			}

@@ -731,7 +809,7 @@ class SettingsUIFeatureFlagTest extends WC_Unit_Test_Case {
 					 * @return array
 					 */
 					public function get_schema( string $section_id ): array {
-						if ( 'advanced' === $section_id ) {
+						if ( '' !== $section_id ) {
 							throw new \RuntimeException( 'Unable to build settings UI schema.' );
 						}