Commit aeaaef5605c for woocommerce

commit aeaaef5605c8e64883b1792946a3c0d2e62fd9d2
Author: Daniel Mallory <daniel.mallory@automattic.com>
Date:   Thu Sep 3 16:34:21 2026 +0100

    Remove card header actions and obsolete styles from the Settings UI (#67865)

    * Remove card header actions from the Settings UI schema

    * Add changelog entries for Settings UI group actions

    * Remove the group actions schema validation

    Card header actions are no longer part of the Settings UI schema, so
    validating the actions key guarded a contract that no longer exists.
    The validator ignores unknown group keys, so a schema still declaring
    actions now degrades to ignored instead of failing closed.

    Refs WOOPRD-3610

    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

    * Remove obsolete Settings UI styles

    * Add changelog entry for Settings UI style cleanup

    * refactor(settings-ui): drop the unused runtime exports from the package index

    The package index re-exported the registry resolvers and the hidden-input
    helpers alongside the mount components. Nothing outside the package used
    them: the adapter and the page consume the resolvers directly, and the page
    owns hidden-input serialisation for the form_post save adapter. Exporting
    them invited extensions to couple to renderer internals, which is the
    contract the DataForm move deliberately removed.

    Trim the index to the mount components, registerSettingsExtension, and the
    types an extension author needs.

    Refs WOOPRD-3603

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LZLDkAqFMmHDyq9XrEeb1P

    * refactor(settings-ui): remove the window.wcSettingsUI global

    The registry assigned a second global, window.wcSettingsUI, next to the
    window.wc.settingsUi global that the wc-settings-ui script handle already
    provides. The docs and README only ever show the import path, which
    resolves to wc.settingsUi at runtime, so the extra global was an
    undocumented alias with its own Window typing to maintain.

    Drop the assignment, the SettingsUIRegistry type and the global Window
    declaration, and point the e2e registration plugin at wc.settingsUi.

    Refs WOOPRD-3603

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LZLDkAqFMmHDyq9XrEeb1P

    ---------

    Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

diff --git a/packages/js/settings-ui/changelog/update-remove-group-actions b/packages/js/settings-ui/changelog/update-remove-group-actions
new file mode 100644
index 00000000000..ec45996dd8d
--- /dev/null
+++ b/packages/js/settings-ui/changelog/update-remove-group-actions
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Remove group header actions from the Settings UI schema.
diff --git a/packages/js/settings-ui/src/index.ts b/packages/js/settings-ui/src/index.ts
index 082f55280f7..04219b856d4 100644
--- a/packages/js/settings-ui/src/index.ts
+++ b/packages/js/settings-ui/src/index.ts
@@ -1,19 +1,9 @@
 export { SettingsUIErrorBoundary, SettingsUIPage } from './settings-ui-page';
-export { HiddenInputs, getHiddenInputs } from './hidden-inputs';
-export {
-	registerSettingsExtension,
-	resolveFieldComponent,
-	resolveFieldVisibilityPredicate,
-	resolveGroupVisibilityPredicate,
-	resolveRegionComponent,
-	resolveSaveHandler,
-} from './registry';
+export { registerSettingsExtension } from './registry';
 export type {
 	SettingsUIField,
 	SettingsUIGroup,
-	SettingsUIGroupAction,
 	SettingsUIOption,
-	SettingsUIRegistry,
 	SettingsUISaveSchema,
 	SettingsUISaveStrategy,
 	SettingsUISchema,
diff --git a/packages/js/settings-ui/src/registry.ts b/packages/js/settings-ui/src/registry.ts
index 428c1aae09c..c7a4c21c44a 100644
--- a/packages/js/settings-ui/src/registry.ts
+++ b/packages/js/settings-ui/src/registry.ts
@@ -268,10 +268,3 @@ export const resolveRegionComponent = (
 	} );
 	return undefined;
 };
-
-if ( typeof window !== 'undefined' ) {
-	window.wcSettingsUI = {
-		...( window.wcSettingsUI || {} ),
-		registerSettingsExtension,
-	};
-}
diff --git a/packages/js/settings-ui/src/types.ts b/packages/js/settings-ui/src/types.ts
index 21a606801ed..726dd1ab1e3 100644
--- a/packages/js/settings-ui/src/types.ts
+++ b/packages/js/settings-ui/src/types.ts
@@ -43,20 +43,10 @@ export type SettingsUIField = {
 	save?: SettingsUISaveSchema;
 };

-export type SettingsUIGroupAction = {
-	id: string;
-	label: string;
-	href: string;
-	variant?: 'primary' | 'secondary' | 'tertiary' | 'link' | string;
-	target?: string;
-	rel?: string;
-};
-
 export type SettingsUIGroup = {
 	id: string;
 	title?: string;
 	description?: string;
-	actions?: SettingsUIGroupAction[];
 	fields: SettingsUIField[];
 };

@@ -201,15 +191,3 @@ export type SettingsExtensionRegistration = {
 	saveHandlers?: Record< string, SettingsSaveHandler >;
 	regions?: Record< string, SettingsRegionComponent >;
 };
-
-export type SettingsUIRegistry = {
-	registerSettingsExtension: (
-		registration: SettingsExtensionRegistration
-	) => void;
-};
-
-declare global {
-	interface Window {
-		wcSettingsUI?: SettingsUIRegistry;
-	}
-}
diff --git a/plugins/woocommerce/changelog/dev-clean-settings-ui-styles b/plugins/woocommerce/changelog/dev-clean-settings-ui-styles
new file mode 100644
index 00000000000..bf47496c849
--- /dev/null
+++ b/plugins/woocommerce/changelog/dev-clean-settings-ui-styles
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Remove obsolete Settings UI renderer styles and duplicate DataViews CSS; no merchant-facing change.
diff --git a/plugins/woocommerce/changelog/dev-remove-settings-ui-group-actions b/plugins/woocommerce/changelog/dev-remove-settings-ui-group-actions
new file mode 100644
index 00000000000..31df787c562
--- /dev/null
+++ b/plugins/woocommerce/changelog/dev-remove-settings-ui-group-actions
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Remove unsupported group header actions from the experimental Settings UI schema.
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 0e3369ea51d..f7d6a9debe6 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
@@ -1,5 +1,3 @@
-@import "@wordpress/dataviews/build-style/style.css";
-
 body.woocommerce_page_wc-settings.woocommerce-settings-ui-page {
 	background-color: var(--wpds-color-background-surface-neutral);

@@ -216,164 +214,6 @@ body.woocommerce_page_wc-settings.woocommerce-settings-ui-page {
 		max-width: var(--wpds-dimension-surface-width-xl);
 	}

-	.wc-settings-ui__section {
-		margin: 0;
-	}
-
-	.wc-settings-ui__section-card {
-		background: #fff;
-		border: 1px solid var(--wpds-color-stroke-surface-neutral-weak, #f0f0f0);
-		border-radius: var(--wpds-border-radius-lg, 8px);
-		box-sizing: border-box;
-		display: flex;
-		flex-direction: column;
-		width: 100%;
-	}
-
-	.wc-settings-ui__section-header {
-		align-items: flex-start;
-		display: flex;
-		gap: var(--wpds-dimension-gap-xl, 24px);
-		justify-content: space-between;
-		padding: 24px;
-	}
-
-	.wc-settings-ui__section-heading {
-		min-width: 0;
-	}
-
-	.wc-settings-ui__section-header h2 {
-		color: #1e1e1e;
-		font-size: 15px;
-		font-weight: var(--wpds-typography-font-weight-medium, 500);
-		line-height: 20px;
-		margin: 0;
-	}
-
-	.wc-settings-ui__section-description {
-		color: var(--wpds-color-foreground-content-neutral-weak);
-		margin: var(--wpds-dimension-gap-sm) 0 0;
-
-		p {
-			margin: 0 0 var(--wpds-dimension-gap-sm);
-		}
-
-		p:last-child {
-			margin-bottom: 0;
-		}
-	}
-
-	.wc-settings-ui__section-actions {
-		display: flex;
-		flex: 0 0 auto;
-		flex-wrap: wrap;
-		gap: var(--wpds-dimension-gap-sm, 8px);
-	}
-
-	.wc-settings-ui__section-fields {
-		display: flex;
-		flex-direction: column;
-		gap: var(--wpds-dimension-gap-lg, 16px);
-		padding: 0 24px 24px;
-	}
-
-	.wc-settings-ui__section-card > .wc-settings-ui__section-fields:first-child {
-		padding-top: 24px;
-	}
-
-	.wc-settings-ui__field {
-		margin: 0;
-		width: 100%;
-	}
-
-	.wc-settings-ui__info {
-		background: var(--wpds-color-background-surface-neutral-weak);
-		border-radius: var(--wpds-border-radius-md);
-		padding: var(--wpds-dimension-padding-lg);
-
-		strong {
-			display: block;
-			margin-bottom: var(--wpds-dimension-gap-xs);
-		}
-
-		p {
-			margin: 0;
-		}
-	}
-
-	.wc-settings-ui__control.components-base-control {
-		margin-bottom: 0;
-		width: 100%;
-	}
-
-	.wc-settings-ui__control {
-		.components-base-control__field {
-			margin-bottom: 0;
-		}
-
-		.components-input-control__label {
-			line-height: 16px;
-		}
-
-		.components-base-control__help {
-			color: var(--wpds-color-foreground-content-neutral-weak);
-			font-size: var(--wpds-typography-font-size-md);
-			line-height: var(--wpds-typography-line-height-sm);
-			margin-top: var(--wpds-dimension-gap-sm);
-		}
-	}
-
-	.wc-settings-ui__number-control {
-		position: relative;
-	}
-
-	// Owned input skin: deliberately not borrowing internal
-	// @wordpress/components classnames, which carry no compatibility
-	// guarantee. Values mirror the default-size TextControl input.
-	.wc-settings-ui__number-control-input {
-		appearance: textfield;
-		border: var(--wpds-border-width-xs) solid
-			var(--wpds-color-stroke-interactive-neutral);
-		border-radius: var(--wpds-border-radius-sm);
-		box-shadow: 0 0 0 transparent;
-		box-sizing: border-box;
-		font-family: inherit;
-		// Fonts smaller than 16px cause mobile Safari to zoom on focus.
-		font-size: 16px;
-		line-height: normal;
-		margin: 0;
-		min-height: var(--wpds-dimension-size-lg);
-		padding: 6px var(--wpds-dimension-padding-md);
-		// Keep typed values clear of the spin buttons.
-		padding-right: 64px;
-		transition: box-shadow 0.1s linear;
-		width: 100%;
-
-		@media (min-width: 600px) {
-			font-size: var(--wpds-typography-font-size-md);
-		}
-
-		&:focus {
-			border-color: var(--wpds-color-stroke-focus);
-			box-shadow: 0 0 0 0.5px var(--wpds-color-stroke-focus);
-			outline: var(--wpds-border-width-sm) solid transparent;
-		}
-
-		&::-webkit-inner-spin-button,
-		&::-webkit-outer-spin-button {
-			appearance: none;
-			margin: 0;
-		}
-	}
-
-	.wc-settings-ui__number-control-spin-buttons {
-		display: flex;
-		position: absolute;
-		right: 6px;
-		top: 50%;
-		transform: translateY(-50%);
-	}
-
 	.wc-settings-ui__unsaved-changes-modal {
 		.components-modal__header {
 			min-height: 72px;
@@ -437,13 +277,5 @@ body.woocommerce_page_wc-settings.woocommerce-settings-ui-page {
 				var(--wpds-dimension-gap-2xl);
 			max-width: none;
 		}
-
-		.wc-settings-ui__section-header {
-			display: block;
-		}
-
-		.wc-settings-ui__section-actions {
-			margin-top: var(--wpds-dimension-gap-lg);
-		}
 	}
 }
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/SettingsUISchema.php b/plugins/woocommerce/src/Internal/Admin/Settings/SettingsUISchema.php
index 2720114e301..2fae9a99db3 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings/SettingsUISchema.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/SettingsUISchema.php
@@ -66,7 +66,6 @@ class SettingsUISchema {
 					'id'          => $current_id,
 					'title'       => isset( $setting['title'] ) && is_scalar( $setting['title'] ) ? html_entity_decode( (string) $setting['title'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ) : '',
 					'description' => isset( $setting['desc'] ) && is_scalar( $setting['desc'] ) ? wp_kses_post( (string) $setting['desc'] ) : '',
-					'actions'     => self::get_group_actions( $setting ),
 					'order'       => isset( $setting['order'] ) ? (int) $setting['order'] : $group_index,
 					'fields'      => array(),
 				);
@@ -197,7 +196,6 @@ class SettingsUISchema {
 		foreach ( $schema['groups'] as $group ) {
 			$group_id = $group['id'];
 			self::assert_optional_strings( $group, array( 'title', 'description' ), sprintf( 'Group "%s"', $group_id ) );
-			self::assert_group_actions( $group['actions'] ?? null, $group_id );

 			if ( ! isset( $group['fields'] ) || ! is_array( $group['fields'] ) || ! ArrayUtil::array_is_list( $group['fields'] ) ) {
 				throw self::invalid_schema( sprintf( 'Group "%s" fields must be a list.', $group_id ) );
@@ -800,58 +798,6 @@ class SettingsUISchema {
 		return $attributes;
 	}

-	/**
-	 * Normalize group header actions.
-	 *
-	 * @param array $setting Legacy title setting definition.
-	 * @return array
-	 */
-	private static function get_group_actions( array $setting ): array {
-		if ( empty( $setting['actions'] ) || ! is_array( $setting['actions'] ) ) {
-			return array();
-		}
-
-		$actions = array();
-
-		foreach ( $setting['actions'] as $index => $action ) {
-			if ( ! is_array( $action ) || empty( $action['label'] ) || ! is_scalar( $action['label'] ) ) {
-				continue;
-			}
-
-			$href = $action['href'] ?? $action['url'] ?? '';
-			if ( ! is_scalar( $href ) || '' === (string) $href ) {
-				continue;
-			}
-
-			$href = esc_url_raw( (string) $href );
-			if ( '' === $href ) {
-				continue;
-			}
-
-			$normalized_action = array(
-				'id'    => isset( $action['id'] ) && is_scalar( $action['id'] ) ? sanitize_key( (string) $action['id'] ) : 'action_' . $index,
-				'label' => wp_strip_all_tags( html_entity_decode( (string) $action['label'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ) ),
-				'href'  => $href,
-			);
-
-			if ( isset( $action['variant'] ) && is_scalar( $action['variant'] ) ) {
-				$normalized_action['variant'] = sanitize_key( (string) $action['variant'] );
-			}
-
-			if ( isset( $action['target'] ) && is_scalar( $action['target'] ) && in_array( (string) $action['target'], array( '_blank', '_self', '_parent', '_top' ), true ) ) {
-				$normalized_action['target'] = (string) $action['target'];
-			}
-
-			if ( isset( $action['rel'] ) && is_scalar( $action['rel'] ) ) {
-				$normalized_action['rel'] = sanitize_text_field( (string) $action['rel'] );
-			}
-
-			$actions[] = $normalized_action;
-		}
-
-		return $actions;
-	}
-
 	// The assertion helpers deliberately propagate InvalidArgumentException to
 	// the public boundary method, whose contract documents that exception.
 	// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.Missing
@@ -1002,42 +948,6 @@ class SettingsUISchema {
 		}
 	}

-	/**
-	 * Assert group header actions.
-	 *
-	 * @param mixed  $actions Group actions, or null when omitted.
-	 * @param string $group_id Group id.
-	 */
-	private static function assert_group_actions( $actions, string $group_id ): void {
-		if ( null === $actions ) {
-			return;
-		}
-
-		if ( ! is_array( $actions ) || ! ArrayUtil::array_is_list( $actions ) ) {
-			throw self::invalid_schema( sprintf( 'Group "%s" actions must be a list.', $group_id ) );
-		}
-
-		$ids = array();
-		foreach ( $actions as $index => $action ) {
-			if ( ! is_array( $action ) ) {
-				throw self::invalid_schema( sprintf( 'Group "%s" action %d must be an array.', $group_id, $index ) );
-			}
-
-			self::assert_non_empty_string( $action['id'] ?? null, sprintf( 'Group "%s" action %d id must be a non-empty string.', $group_id, $index ) );
-			if ( isset( $ids[ $action['id'] ] ) ) {
-				throw self::invalid_schema( sprintf( 'Group "%s" action id "%s" is duplicated.', $group_id, $action['id'] ) );
-			}
-			$ids[ $action['id'] ] = true;
-
-			foreach ( array( 'label', 'href' ) as $property ) {
-				if ( ! isset( $action[ $property ] ) || ! is_string( $action[ $property ] ) ) {
-					throw self::invalid_schema( sprintf( 'Group "%s" action %d %s must be a string.', $group_id, $index, $property ) );
-				}
-			}
-
-			self::assert_optional_strings( $action, array( 'variant', 'target', 'rel' ), sprintf( 'Group "%s" action %d', $group_id, $index ) );
-		}
-	}

 	/**
 	 * Assert a field definition.
@@ -1275,7 +1185,6 @@ class SettingsUISchema {
 			'id'          => $group_id,
 			'title'       => '',
 			'description' => '',
-			'actions'     => array(),
 			'order'       => $order,
 			'fields'      => array(),
 		);
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/settings-ui-component-registration/settings-ui-component-registration.php b/plugins/woocommerce/tests/e2e/test-plugins/settings-ui-component-registration/settings-ui-component-registration.php
index 0b51299f659..0fc7b5b6c8d 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/settings-ui-component-registration/settings-ui-component-registration.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/settings-ui-component-registration/settings-ui-component-registration.php
@@ -41,7 +41,7 @@ final class WC_Settings_UI_Component_Registration_Test_Plugin {
 		wp_add_inline_script(
 			self::REGISTERED_HANDLE,
 			<<<'JS'
-window.wcSettingsUI.registerSettingsExtension( {
+window.wc.settingsUi.registerSettingsExtension( {
 	scope: { page: 'products', section: 'settings_ui_component_registered' },
 	components: {
 		'woocommerce/settings-ui-component-test': function SettingsUIComponentTest( props ) {
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/SettingsUISchemaTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/SettingsUISchemaTest.php
index 3f3f0e93228..5bd028c85c8 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/SettingsUISchemaTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/SettingsUISchemaTest.php
@@ -144,9 +144,9 @@ class SettingsUISchemaTest extends WC_Unit_Test_Case {
 	}

 	/**
-	 * @testdox It preserves sanitized group description markup and header actions.
+	 * @testdox It preserves sanitized group description markup and omits header actions.
 	 */
-	public function test_from_legacy_settings_preserves_group_description_and_actions(): void {
+	public function test_from_legacy_settings_preserves_group_description_without_actions(): void {
 		$schema = SettingsUISchema::from_legacy_settings(
 			'test',
 			'advanced',
@@ -174,19 +174,7 @@ class SettingsUISchemaTest extends WC_Unit_Test_Case {
 		$group = $schema['groups']['group'];

 		$this->assertSame( 'Read the <a href="https://woocommerce.com">documentation</a>alert("x").', $group['description'] );
-		$this->assertSame(
-			array(
-				array(
-					'id'      => 'learn-more',
-					'label'   => 'Learn more',
-					'href'    => 'https://woocommerce.com/documentation',
-					'variant' => 'secondary',
-					'target'  => '_blank',
-					'rel'     => 'noopener noreferrer',
-				),
-			),
-			$group['actions']
-		);
+		$this->assertArrayNotHasKey( 'actions', $group );
 	}

 	/**
@@ -1193,14 +1181,6 @@ class SettingsUISchemaTest extends WC_Unit_Test_Case {
 				'intent' => array( 'invalid' ),
 			),
 		);
-		$invalid_action                                      = $valid;
-		$invalid_action['groups']['main']['actions']         = array(
-			array(
-				'id'    => '',
-				'label' => 'Docs',
-				'href'  => 'https://example.com',
-			),
-		);
 		$invalid_page_save                                   = $valid;
 		$invalid_page_save['save']                           = array( 'adapter' => 'custom' );
 		$invalid_navigation_component                        = $valid;
@@ -1229,7 +1209,6 @@ class SettingsUISchemaTest extends WC_Unit_Test_Case {
 			'malformed shell navigation'  => array( $invalid_shell, 'Shell navigation item 0 href must be a string.' ),
 			'malformed breadcrumb'        => array( $invalid_breadcrumb, 'Shell breadcrumb 0 label must be a string.' ),
 			'invalid badge intent'        => array( $invalid_badge, 'Shell badge 0 intent must be a string.' ),
-			'empty group action id'       => array( $invalid_action, 'Group "main" action 0 id must be a non-empty string.' ),
 			'custom save without handler' => array( $invalid_page_save, 'Schema custom save strategy must define a non-empty handler.' ),
 			'empty navigation component'  => array( $invalid_navigation_component, 'Shell navigationComponent must be a non-empty string.' ),
 			'group map id mismatch'       => array( $invalid_group_map, 'Group map key "other" must match group id "main".' ),
@@ -1237,15 +1216,15 @@ class SettingsUISchemaTest extends WC_Unit_Test_Case {
 	}

 	/**
-	 * @testdox It accepts valid optional shell, field, visibility, action, and save metadata.
+	 * @testdox It accepts valid optional shell, field, visibility, and save metadata.
 	 */
 	public function test_assert_valid_schema_accepts_optional_metadata(): void {
-		$schema                              = self::get_valid_schema_for_validation();
-		$schema['save']                      = array(
+		$schema          = self::get_valid_schema_for_validation();
+		$schema['save']  = array(
 			'adapter' => 'custom',
 			'handler' => 'acme/save',
 		);
-		$schema['shell']                     = array(
+		$schema['shell'] = array(
 			'header'              => 'visible',
 			'title'               => 'Acme settings',
 			'subtitle'            => 'Configure Acme.',
@@ -1272,16 +1251,6 @@ class SettingsUISchemaTest extends WC_Unit_Test_Case {
 			'sectionNavigation'   => array(),
 			'navigationComponent' => 'acme/navigation',
 		);
-		$schema['groups']['main']['actions'] = array(
-			array(
-				'id'      => 'docs',
-				'label'   => 'Documentation',
-				'href'    => 'https://example.com/docs',
-				'variant' => 'link',
-				'target'  => '_blank',
-				'rel'     => 'noopener',
-			),
-		);
 		$schema['groups']['main']['fields'][0]['component']  = 'acme/text';
 		$schema['groups']['main']['fields'][0]['visibility'] = array(
 			'controller' => 'acme_enabled',
@@ -1322,7 +1291,6 @@ class SettingsUISchemaTest extends WC_Unit_Test_Case {
 					'id'          => 'main',
 					'title'       => 'Main',
 					'description' => 'Main settings.',
-					'actions'     => array(),
 					'fields'      => array(
 						array(
 							'id'          => 'acme_field',