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.' );
}