Commit da009bce482 for woocommerce

commit da009bce4820d32fd114d52b657b0ecbda91782f
Author: Daniel Mallory <daniel.mallory@automattic.com>
Date:   Wed Sep 2 18:55:28 2026 +0100

    Remove Woo-owned Settings UI field renderers (#67863)

    * Remove the Woo-owned field renderers

    * Update changelog entries for DataForm controls

    * refactor(settings-ui): drop the unused createElement import

    Removing the info wrapper left the DataForm adapter with no JSX, so the
    createElement pragma import no longer has a use and tsc flags it under
    noUnusedLocals.

    Refs WOOPRD-3597

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

    ---------

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

diff --git a/docs/extensions/settings-and-config/settings-ui.md b/docs/extensions/settings-and-config/settings-ui.md
index a68e6aa1692..3382c7f0c68 100644
--- a/docs/extensions/settings-and-config/settings-ui.md
+++ b/docs/extensions/settings-and-config/settings-ui.md
@@ -49,7 +49,7 @@ add_filter(

 A `WC_Settings_Page` subclass opts in by returning a settings UI adapter from `get_settings_ui_page()`.

-For pages that only need native fields, use `LegacySettingsPageAdapter`:
+For pages that only need built-in fields, use `LegacySettingsPageAdapter`:

 ```php
 <?php
@@ -139,9 +139,9 @@ WooCommerce creates the settings UI adapter for registered sections internally.

 Use a section id that does not conflict with an existing section on the same settings tab. For the `checkout` tab, ids that match existing payment gateway sections are reserved.

-### Provide a native Settings UI page for a registered section
+### Provide a custom Settings UI page for a registered section

-Sections with custom navigation, save handlers, or native Settings UI schemas can provide their own Settings UI page instead of using the legacy settings adapter.
+Sections with custom navigation, save handlers, or custom Settings UI schemas can provide their own Settings UI page instead of using the legacy settings adapter.

 ```php
 <?php
@@ -159,7 +159,7 @@ final class My_Plugin_Settings_Section extends SettingsSection {

 When `get_settings_ui_page()` returns a `SettingsUIPageInterface`, WooCommerce uses it directly for the registered section. Returning `null` keeps the default behavior: WooCommerce converts the section's legacy `get_settings()` array into a Settings UI schema.

-### Section navigation on native pages
+### Section navigation on custom pages

 The Settings UI shell renders sibling-section navigation from the `shell.sectionNavigation` schema key. How it applies depends on where the page is registered:

@@ -178,7 +178,7 @@ $schema['shell']['sectionNavigation'] = array(
 );
 ```

-## Native field migration
+## DataForm field migration

 The legacy adapter converts the existing `get_settings()` array into a canonical schema for React. It supports common settings fields:

@@ -332,13 +332,13 @@ $schema['shell']['badges']   = array(

 The Products settings page is the Core reference migration. With `settings-ui` enabled, the Products tab renders through the settings UI. With the flag disabled, it renders through the existing legacy settings UI.

-Use this page to verify the native migration path before testing a plugin-specific page such as WooPayments.
+Use this page to verify the DataForm migration path before testing a plugin-specific page such as WooPayments.

 ## Testing an extension integration

 1. Enable the `settings-ui` feature flag.
 2. Return a settings UI adapter from your `WC_Settings_Page` subclass.
-3. Start with native fields and confirm the page renders and saves.
+3. Start with built-in fields and confirm the page renders and saves.
 4. Add `component` metadata only for fields that need custom UI.
 5. Register scoped JavaScript components with `registerSettingsExtension()`.
 6. Return custom script handles from `get_script_handles()` so they load before mount.
diff --git a/packages/js/settings-ui/README.md b/packages/js/settings-ui/README.md
index 6921e4923e5..8328c92e2b4 100644
--- a/packages/js/settings-ui/README.md
+++ b/packages/js/settings-ui/README.md
@@ -38,4 +38,4 @@ array(
 )
 ```

-Use native fields when possible and register custom components only for fields that need plugin-specific UI.
+Use DataForm's built-in controls when possible and register custom components only for fields that need plugin-specific UI.
diff --git a/packages/js/settings-ui/changelog/fix-number-custom-attributes b/packages/js/settings-ui/changelog/fix-number-custom-attributes
deleted file mode 100644
index 1b9b934da6b..00000000000
--- a/packages/js/settings-ui/changelog/fix-number-custom-attributes
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Honor `disabled` and `placeholder` custom attributes on number fields.
diff --git a/packages/js/settings-ui/changelog/fix-number-spin-control-scientific-precision b/packages/js/settings-ui/changelog/fix-number-spin-control-scientific-precision
deleted file mode 100644
index 661cf5f5c31..00000000000
--- a/packages/js/settings-ui/changelog/fix-number-spin-control-scientific-precision
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Clamp NumberSpinControl precision so scientific-notation steps cannot exceed toFixed limits.
diff --git a/packages/js/settings-ui/changelog/update-number-spin-control b/packages/js/settings-ui/changelog/update-number-spin-control
deleted file mode 100644
index acf938ff33a..00000000000
--- a/packages/js/settings-ui/changelog/update-number-spin-control
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-Render number fields with explicit +/- spin buttons (NumberSpinControl, composed from stable components) instead of the native browser spinner.
diff --git a/packages/js/settings-ui/changelog/update-remove-woo-field-renderers b/packages/js/settings-ui/changelog/update-remove-woo-field-renderers
new file mode 100644
index 00000000000..6b1ea102610
--- /dev/null
+++ b/packages/js/settings-ui/changelog/update-remove-woo-field-renderers
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Remove the Woo-owned field renderers now that Settings UI fields use DataForm controls.
diff --git a/packages/js/settings-ui/package.json b/packages/js/settings-ui/package.json
index 5963fb192af..ff26f5037d2 100644
--- a/packages/js/settings-ui/package.json
+++ b/packages/js/settings-ui/package.json
@@ -37,7 +37,6 @@
 	"dependencies": {
 		"@woocommerce/sanitize": "workspace:*",
 		"@wordpress/admin-ui": "catalog:wp-bundled",
-		"@wordpress/a11y": "catalog:wp-min",
 		"@wordpress/components": "catalog:wp-min",
 		"@wordpress/dataviews": "17.1.0",
 		"@wordpress/element": "catalog:wp-min",
diff --git a/packages/js/settings-ui/src/dataform-adapter.tsx b/packages/js/settings-ui/src/dataform-adapter.tsx
index 420f2d8ff6e..ddfe51aefd3 100644
--- a/packages/js/settings-ui/src/dataform-adapter.tsx
+++ b/packages/js/settings-ui/src/dataform-adapter.tsx
@@ -1,7 +1,6 @@
 /**
  * External dependencies
  */
-import { createElement } from '@wordpress/element';
 import type {
 	Field,
 	FieldTypeName,
@@ -296,15 +295,10 @@ export const buildDataFormField = (

 	if ( settingsField.type === 'info' ) {
 		field.readOnly = true;
-		// DataForm paints the label for a read-only field and drops its
-		// description, so info shows the sanitized element the field already
-		// carries rather than sanitizing the same string again per render.
+		// The description is already a sanitized element, and DataForm paints
+		// the label for a read-only field, so info reuses it as its body.
 		field.render = ( { field: normalizedField } ) =>
-			normalizedField.description ? (
-				<div className="wc-settings-ui__info">
-					{ normalizedField.description }
-				</div>
-			) : null;
+			normalizedField.description ?? null;
 		return field;
 	}

diff --git a/packages/js/settings-ui/src/index.ts b/packages/js/settings-ui/src/index.ts
index 4b929680235..082f55280f7 100644
--- a/packages/js/settings-ui/src/index.ts
+++ b/packages/js/settings-ui/src/index.ts
@@ -1,5 +1,4 @@
 export { SettingsUIErrorBoundary, SettingsUIPage } from './settings-ui-page';
-export { NativeSettingsField } from './native-fields';
 export { HiddenInputs, getHiddenInputs } from './hidden-inputs';
 export {
 	registerSettingsExtension,
@@ -27,8 +26,6 @@ export type {
 	SettingsEditControlProps,
 	SettingsExtensionRegistration,
 	SettingsExtensionScope,
-	SettingsFieldComponent,
-	SettingsFieldComponentProps,
 	SettingsFieldContext,
 	SettingsRegionComponent,
 	SettingsRegionComponentProps,
diff --git a/packages/js/settings-ui/src/native-fields.tsx b/packages/js/settings-ui/src/native-fields.tsx
deleted file mode 100644
index 6af32ac3449..00000000000
--- a/packages/js/settings-ui/src/native-fields.tsx
+++ /dev/null
@@ -1,262 +0,0 @@
-/**
- * External dependencies
- */
-import {
-	BaseControl,
-	CheckboxControl,
-	SelectControl,
-	TextControl,
-	TextareaControl,
-} from '@wordpress/components';
-import { createElement, RawHTML } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
-
-/**
- * Internal dependencies
- */
-import { warn } from './diagnostics';
-import {
-	createSettingsHelpElement as getHelp,
-	sanitizeSettingsHtml,
-} from './html';
-import { NumberSpinControl } from './number-spin-control';
-import type { SettingsFieldComponentProps, SettingsValue } from './types';
-
-type TextInputType =
-	| 'text'
-	| 'password'
-	| 'datetime-local'
-	| 'date'
-	| 'time'
-	| 'email'
-	| 'url'
-	| 'tel';
-
-const textInputTypes: TextInputType[] = [
-	'text',
-	'password',
-	'datetime-local',
-	'date',
-	'time',
-	'email',
-	'url',
-	'tel',
-];
-
-const nativeFieldTypes = new Set( [
-	'info',
-	'checkbox',
-	'textarea',
-	'select',
-	'radio',
-	'array',
-	'number',
-] );
-
-const toStringValue = ( value: SettingsValue ) =>
-	value === null || typeof value === 'undefined' ? '' : String( value );
-
-const isTextInputType = ( type: string ): type is TextInputType =>
-	textInputTypes.includes( type as TextInputType );
-
-export const isNativeSettingsFieldType = ( type: string ) =>
-	nativeFieldTypes.has( type ) || isTextInputType( type );
-
-// Use HTML boolean attribute presence semantics: disabled="false" still
-// means disabled, while a boolean false remains false.
-const toPresenceBooleanCustomAttribute = (
-	value: string | number | boolean | undefined
-): boolean | undefined => {
-	if ( typeof value === 'undefined' ) {
-		return undefined;
-	}
-
-	return typeof value === 'boolean' ? value : true;
-};
-
-const toStringCustomAttribute = (
-	value: string | number | undefined
-): string | undefined => {
-	return typeof value === 'undefined' ? undefined : String( value );
-};
-
-const getNumberInputAttributes = (
-	customAttributes?: Record< string, string | number | boolean >
-) => {
-	const safeAttributes =
-		customAttributes && typeof customAttributes === 'object'
-			? customAttributes
-			: {};
-	const { disabled, placeholder, ...inputAttributes } = safeAttributes;
-	const placeholderAttribute =
-		typeof placeholder === 'boolean' ? undefined : placeholder;
-
-	return {
-		disabled: toPresenceBooleanCustomAttribute( disabled ),
-		placeholder: toStringCustomAttribute( placeholderAttribute ),
-		inputAttributes,
-	};
-};
-
-export const NativeSettingsField = ( {
-	field,
-	value,
-	onChange,
-}: SettingsFieldComponentProps ) => {
-	if ( field.type === 'info' ) {
-		return (
-			<div className="wc-settings-ui__info" id={ field.id }>
-				<strong>{ field.label }</strong>
-				{ field.description ? (
-					<RawHTML>
-						{ sanitizeSettingsHtml( field.description ) }
-					</RawHTML>
-				) : null }
-			</div>
-		);
-	}
-
-	if ( field.type === 'checkbox' ) {
-		return (
-			<CheckboxControl
-				className="wc-settings-ui__control"
-				label={ field.label }
-				help={ getHelp( field.description ) }
-				checked={ value === true || value === 'yes' || value === '1' }
-				disabled={ field.disabled }
-				onChange={ onChange }
-				__nextHasNoMarginBottom
-			/>
-		);
-	}
-
-	if ( field.type === 'textarea' ) {
-		return (
-			<TextareaControl
-				className="wc-settings-ui__control"
-				label={ field.label }
-				help={ getHelp( field.description ) }
-				value={ toStringValue( value ) }
-				placeholder={ field.placeholder }
-				disabled={ field.disabled }
-				onChange={ onChange }
-				__nextHasNoMarginBottom
-			/>
-		);
-	}
-
-	if ( field.type === 'select' || field.type === 'radio' ) {
-		const selectValue = toStringValue( value );
-		const options = Array.isArray( field.options ) ? field.options : [];
-		const selectOptions = options.some(
-			( option ) => option.value === selectValue
-		)
-			? options
-			: [
-					{
-						value: selectValue,
-						label: __( 'Select', 'woocommerce' ),
-						disabled: true,
-					},
-					...options,
-			  ];
-
-		return (
-			<SelectControl
-				className="wc-settings-ui__control"
-				label={ field.label }
-				help={ getHelp( field.description ) }
-				value={ selectValue }
-				options={ selectOptions }
-				disabled={ field.disabled }
-				onChange={ ( nextValue ) => onChange( nextValue ) }
-				__next40pxDefaultSize
-				__nextHasNoMarginBottom
-			/>
-		);
-	}
-
-	if ( field.type === 'array' ) {
-		const selectedValues = Array.isArray( value ) ? value : [];
-
-		return (
-			<BaseControl
-				className="wc-settings-ui__control"
-				id={ field.id }
-				label={ field.label }
-				help={ getHelp( field.description ) }
-				__nextHasNoMarginBottom
-			>
-				<select
-					id={ field.id }
-					multiple
-					disabled={ field.disabled }
-					value={ selectedValues }
-					onChange={ ( event ) => {
-						onChange(
-							Array.from(
-								event.currentTarget.selectedOptions
-							).map( ( option ) => option.value )
-						);
-					} }
-				>
-					{ ( field.options || [] ).map( ( option ) => (
-						<option key={ option.value } value={ option.value }>
-							{ option.label }
-						</option>
-					) ) }
-				</select>
-			</BaseControl>
-		);
-	}
-
-	if ( field.type === 'number' ) {
-		const numberInput = getNumberInputAttributes( field.customAttributes );
-
-		return (
-			<NumberSpinControl
-				id={ field.id }
-				label={ field.label }
-				help={ getHelp( field.description ) }
-				value={ toStringValue( value ) }
-				placeholder={ field.placeholder ?? numberInput.placeholder }
-				disabled={ field.disabled ?? numberInput.disabled }
-				onChange={ onChange }
-				inputAttributes={ numberInput.inputAttributes }
-			/>
-		);
-	}
-
-	if ( isTextInputType( field.type ) ) {
-		return (
-			<TextControl
-				className="wc-settings-ui__control"
-				type={ field.type }
-				label={ field.label }
-				help={ getHelp( field.description ) }
-				value={ toStringValue( value ) }
-				placeholder={ field.placeholder }
-				disabled={ field.disabled }
-				onChange={ onChange }
-				__next40pxDefaultSize
-				__nextHasNoMarginBottom
-				{ ...field.customAttributes }
-			/>
-		);
-	}
-
-	warn( `Field type "${ field.type }" is not supported.`, { field } );
-
-	return (
-		<TextControl
-			className="wc-settings-ui__control"
-			label={ field.label }
-			help={ getHelp( field.description ) }
-			value={ toStringValue( value ) }
-			disabled={ field.disabled }
-			onChange={ onChange }
-			__next40pxDefaultSize
-			__nextHasNoMarginBottom
-		/>
-	);
-};
diff --git a/packages/js/settings-ui/src/number-spin-control.tsx b/packages/js/settings-ui/src/number-spin-control.tsx
deleted file mode 100644
index b93dfb34138..00000000000
--- a/packages/js/settings-ui/src/number-spin-control.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-/**
- * External dependencies
- */
-import { speak } from '@wordpress/a11y';
-import { BaseControl, Button } from '@wordpress/components';
-import { createElement } from '@wordpress/element';
-import { __, sprintf } from '@wordpress/i18n';
-import type { ReactNode } from 'react';
-
-export type NumberSpinControlProps = {
-	id: string;
-	label?: string;
-	help?: ReactNode;
-	value: string;
-	placeholder?: string;
-	disabled?: boolean;
-	onChange: ( next: string ) => void;
-	inputAttributes?: Record< string, string | number | boolean >;
-};
-
-const plusIcon = (
-	<svg
-		xmlns="http://www.w3.org/2000/svg"
-		viewBox="0 0 24 24"
-		width="24"
-		height="24"
-		aria-hidden="true"
-		focusable="false"
-	>
-		<path d="M11 12.5V17.5H12.5V12.5H17.5V11H12.5V6H11V11H6V12.5H11Z" />
-	</svg>
-);
-
-const minusIcon = (
-	<svg
-		xmlns="http://www.w3.org/2000/svg"
-		viewBox="0 0 24 24"
-		width="24"
-		height="24"
-		aria-hidden="true"
-		focusable="false"
-	>
-		<path d="M7 11.25h10v1.5H7z" />
-	</svg>
-);
-
-const toFiniteNumber = ( raw: unknown ): number | undefined => {
-	if ( typeof raw === 'number' && Number.isFinite( raw ) ) {
-		return raw;
-	}
-
-	if ( typeof raw === 'string' && raw.trim() !== '' ) {
-		const parsed = Number( raw );
-
-		if ( Number.isFinite( parsed ) ) {
-			return parsed;
-		}
-	}
-
-	return undefined;
-};
-
-const decimalPlaces = ( value: number ) => {
-	const normalized = String( value ).toLowerCase();
-
-	if ( normalized.includes( 'e-' ) ) {
-		const [ coefficient, exponent ] = normalized.split( 'e-' );
-		const coefficientDecimals = coefficient.split( '.' )[ 1 ]?.length ?? 0;
-		return Number( exponent ) + coefficientDecimals;
-	}
-
-	const fraction = normalized.split( '.' )[ 1 ];
-	return fraction ? fraction.length : 0;
-};
-
-const stepDecimals = ( ...values: number[] ) =>
-	Math.max( ...values.map( decimalPlaces ) );
-
-const MAX_TO_FIXED_PRECISION = 100;
-
-/**
- * A number input with explicit +/- spin buttons, per the settings designs.
- *
- * Composed from stable @wordpress/components APIs only; the native browser
- * spinner is hidden via CSS and stepping is handled by the buttons, while
- * typing and keyboard arrows keep the native input behavior.
- */
-export const NumberSpinControl = ( {
-	id,
-	label,
-	help,
-	value,
-	placeholder,
-	disabled,
-	onChange,
-	inputAttributes,
-}: NumberSpinControlProps ) => {
-	const min = toFiniteNumber( inputAttributes?.min );
-	const max = toFiniteNumber( inputAttributes?.max );
-	const parsedStep = toFiniteNumber( inputAttributes?.step );
-	// A zero or negative step would make the buttons no-ops or invert them;
-	// fall back to 1 like the native number input does for an invalid step.
-	const step =
-		typeof parsedStep === 'number' && parsedStep > 0 ? parsedStep : 1;
-	const current = toFiniteNumber( value );
-
-	const stepBy = ( direction: 1 | -1 ) => {
-		let next = ( current ?? 0 ) + direction * step;
-
-		if ( typeof min !== 'undefined' ) {
-			next = Math.max( min, next );
-		}
-
-		if ( typeof max !== 'undefined' ) {
-			next = Math.min( max, next );
-		}
-
-		const requiredPrecision = stepDecimals(
-			step,
-			current ?? 0,
-			min ?? 0,
-			max ?? 0
-		);
-		const precision = Math.min(
-			Math.max( requiredPrecision, 0 ),
-			MAX_TO_FIXED_PRECISION
-		);
-		const nextValue =
-			requiredPrecision > MAX_TO_FIXED_PRECISION
-				? String( next )
-				: String( Number( next.toFixed( precision ) ) );
-
-		onChange( nextValue );
-		// Focus stays on the spin button while the input updates, so the
-		// new value must be announced to assistive technology explicitly.
-		speak( nextValue );
-	};
-
-	const incrementDisabled =
-		disabled ||
-		( typeof max !== 'undefined' &&
-			typeof current !== 'undefined' &&
-			current >= max );
-	const decrementDisabled =
-		disabled ||
-		( typeof min !== 'undefined' &&
-			typeof current !== 'undefined' &&
-			current <= min );
-
-	const incrementLabel = label
-		? sprintf(
-				// translators: %s: the label of the number field being stepped.
-				__( 'Increment %s', 'woocommerce' ),
-				label
-		  )
-		: __( 'Increment', 'woocommerce' );
-	const decrementLabel = label
-		? sprintf(
-				// translators: %s: the label of the number field being stepped.
-				__( 'Decrement %s', 'woocommerce' ),
-				label
-		  )
-		: __( 'Decrement', 'woocommerce' );
-
-	return (
-		<BaseControl
-			className="wc-settings-ui__control"
-			id={ id }
-			label={ label }
-			help={ help }
-			__nextHasNoMarginBottom
-		>
-			<div className="wc-settings-ui__number-control">
-				{ /* Schema-provided attributes are spread first so they can
-				     never override the controlled props below. */ }
-				<input
-					{ ...inputAttributes }
-					className="wc-settings-ui__number-control-input"
-					type="number"
-					id={ id }
-					value={ value }
-					placeholder={ placeholder }
-					disabled={ disabled }
-					aria-describedby={ help ? `${ id }__help` : undefined }
-					onChange={ ( event ) =>
-						onChange( event.currentTarget.value )
-					}
-				/>
-				<div className="wc-settings-ui__number-control-spin-buttons">
-					<Button
-						size="small"
-						icon={ plusIcon }
-						label={ incrementLabel }
-						disabled={ incrementDisabled }
-						accessibleWhenDisabled
-						onClick={ () => stepBy( 1 ) }
-					/>
-					<Button
-						size="small"
-						icon={ minusIcon }
-						label={ decrementLabel }
-						disabled={ decrementDisabled }
-						accessibleWhenDisabled
-						onClick={ () => stepBy( -1 ) }
-					/>
-				</div>
-			</div>
-		</BaseControl>
-	);
-};
diff --git a/packages/js/settings-ui/src/test/dataform-adapter.test.tsx b/packages/js/settings-ui/src/test/dataform-adapter.test.tsx
index bb0bf9c99c2..93e66217a3e 100644
--- a/packages/js/settings-ui/src/test/dataform-adapter.test.tsx
+++ b/packages/js/settings-ui/src/test/dataform-adapter.test.tsx
@@ -165,7 +165,7 @@ describe( 'dataform adapter', () => {
 			expect( field.elements ).toEqual( options );
 		} );

-		it( 'renders info fields read-only with only the sanitized description', () => {
+		it( 'renders sanitized info content without a Woo-owned control', () => {
 			const infoField: SettingsUIField = {
 				id: 'info_field',
 				label: 'Read this',
@@ -190,11 +190,14 @@ describe( 'dataform adapter', () => {
 			);
 			expect(
 				container.querySelector( '.wc-settings-ui__info' )
-			).not.toBeNull();
-			expect( container.querySelector( 'script' ) ).toBeNull();
-			// DataForm owns the label for read-only fields, so the render
+			).toBeNull();
+			// DataForm owns the label for a read-only field, so the render
 			// contributes the description and nothing else.
 			expect( container.textContent ).toBe( 'Useful information.' );
+			expect( container.querySelector( 'strong' )?.textContent ).toBe(
+				'information'
+			);
+			expect( container.querySelector( 'script' ) ).toBeNull();
 		} );

 		it.each( [
diff --git a/packages/js/settings-ui/src/test/html-rendering.test.tsx b/packages/js/settings-ui/src/test/html-rendering.test.tsx
index 3d12548a8ae..61fbd0e9296 100644
--- a/packages/js/settings-ui/src/test/html-rendering.test.tsx
+++ b/packages/js/settings-ui/src/test/html-rendering.test.tsx
@@ -363,7 +363,7 @@ describe( 'settings HTML rendering', () => {
 		container.remove();
 	} );

-	it( 'sanitizes native field descriptions before rendering', () => {
+	it( 'sanitizes field descriptions before rendering', () => {
 		const schema: SettingsUISchema = {
 			id: 'test-page',
 			title: 'Test page',
@@ -394,7 +394,7 @@ describe( 'settings HTML rendering', () => {
 		container.remove();
 	} );

-	it( 'hides fields with unmet native visibility rules', () => {
+	it( 'hides fields with unmet schema visibility rules', () => {
 		const schema: SettingsUISchema = {
 			id: 'test-page',
 			title: 'Test page',
@@ -1154,8 +1154,7 @@ describe( 'settings HTML rendering', () => {
 		).toHaveLength( 2 );

 		// The info description keeps sanitized markup while the group
-		// description renders as plain text, so the only strong tag left is
-		// the one from the info description.
+		// description and the DataForm field label render as plain text.
 		const strongTexts = Array.from(
 			container.querySelectorAll( 'strong' )
 		).map( ( el ) => el.textContent );
diff --git a/packages/js/settings-ui/src/test/native-fields.test.tsx b/packages/js/settings-ui/src/test/native-fields.test.tsx
deleted file mode 100644
index 6519c02becb..00000000000
--- a/packages/js/settings-ui/src/test/native-fields.test.tsx
+++ /dev/null
@@ -1,559 +0,0 @@
-/**
- * External dependencies
- */
-/* global HTMLSelectElement */
-import { speak } from '@wordpress/a11y';
-import { createElement } from '@wordpress/element';
-import { act } from 'react';
-import { createRoot } from 'react-dom/client';
-
-/**
- * Internal dependencies
- */
-import {
-	isNativeSettingsFieldType,
-	NativeSettingsField,
-} from '../native-fields';
-import type {
-	SettingsFieldComponentProps,
-	SettingsUIField,
-	SettingsValue,
-} from '../types';
-
-jest.mock( '@wordpress/a11y', () => ( {
-	speak: jest.fn(),
-} ) );
-
-const previousActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT;
-globalThis.IS_REACT_ACT_ENVIRONMENT = true;
-
-afterAll( () => {
-	globalThis.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment;
-} );
-
-const renderElement = ( element: JSX.Element ) => {
-	const container = document.createElement( 'div' );
-	document.body.appendChild( container );
-	const root = createRoot( container );
-
-	act( () => {
-		root.render( element );
-	} );
-
-	return { container, root };
-};
-
-const makeProps = (
-	field: SettingsUIField,
-	value: SettingsValue,
-	onChange: ( next: SettingsValue ) => void = () => {}
-): SettingsFieldComponentProps => ( {
-	field,
-	value,
-	onChange,
-	values: { [ field.id ]: value },
-	initialValues: { [ field.id ]: value },
-	setValue: () => {},
-	setValues: () => {},
-	context: { page: 'test-page' },
-} );
-
-describe( 'NativeSettingsField', () => {
-	let cleanup: ( () => void ) | null = null;
-
-	afterEach( () => {
-		cleanup?.();
-		cleanup = null;
-	} );
-
-	const render = ( element: JSX.Element ) => {
-		const { container, root } = renderElement( element );
-		cleanup = () => {
-			act( () => {
-				root.unmount();
-			} );
-			container.remove();
-		};
-		return container;
-	};
-
-	const clickButton = ( button: HTMLElement ) => {
-		act( () => {
-			button.dispatchEvent(
-				new MouseEvent( 'click', { bubbles: true } )
-			);
-		} );
-	};
-
-	const getSpinButton = ( container: HTMLElement, ariaLabel: string ) => {
-		const button = container.querySelector(
-			`button[aria-label="${ ariaLabel }"]`
-		);
-
-		if ( ! ( button instanceof HTMLButtonElement ) ) {
-			throw new Error(
-				`Expected a spin button labelled "${ ariaLabel }".`
-			);
-		}
-
-		return button;
-	};
-
-	// Spin buttons stay perceivable when disabled (accessibleWhenDisabled),
-	// so the disabled state surfaces as aria-disabled, not [disabled].
-	const isSpinButtonDisabled = ( button: HTMLButtonElement ) =>
-		button.disabled || button.getAttribute( 'aria-disabled' ) === 'true';
-
-	describe( 'number fields', () => {
-		const numberField: SettingsUIField = {
-			id: 'wc_test_number',
-			label: 'Low stock threshold',
-			type: 'number',
-			customAttributes: { min: 0, step: 1 },
-		};
-
-		it( 'renders a number input with custom spin buttons instead of native spinners', () => {
-			const container = render(
-				<NativeSettingsField { ...makeProps( numberField, '5' ) } />
-			);
-
-			const input = container.querySelector( 'input[type="number"]' );
-			expect( input ).not.toBeNull();
-			expect( input?.getAttribute( 'min' ) ).toBe( '0' );
-			expect( input?.getAttribute( 'step' ) ).toBe( '1' );
-
-			expect(
-				getSpinButton( container, 'Increment Low stock threshold' )
-			).toBeInstanceOf( HTMLButtonElement );
-			expect(
-				getSpinButton( container, 'Decrement Low stock threshold' )
-			).toBeInstanceOf( HTMLButtonElement );
-		} );
-
-		it( 'honors placeholder and disabled custom attributes for number inputs', () => {
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps(
-						{
-							...numberField,
-							customAttributes: {
-								...numberField.customAttributes,
-								disabled: 'true',
-								placeholder: 'Only configurable in code',
-							},
-						},
-						''
-					) }
-				/>
-			);
-
-			const input = container.querySelector( 'input[type="number"]' );
-			expect( input ).toBeInstanceOf( HTMLInputElement );
-			expect( input ).toHaveAttribute(
-				'placeholder',
-				'Only configurable in code'
-			);
-			expect( input?.getAttribute( 'min' ) ).toBe( '0' );
-			expect( input?.getAttribute( 'step' ) ).toBe( '1' );
-			expect( ( input as HTMLInputElement ).disabled ).toBe( true );
-			expect(
-				isSpinButtonDisabled(
-					getSpinButton( container, 'Increment Low stock threshold' )
-				)
-			).toBe( true );
-			expect(
-				isSpinButtonDisabled(
-					getSpinButton( container, 'Decrement Low stock threshold' )
-				)
-			).toBe( true );
-		} );
-
-		it( 'uses presence semantics for disabled custom attributes on number inputs', () => {
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps(
-						{
-							...numberField,
-							customAttributes: {
-								...numberField.customAttributes,
-								disabled: 'false',
-							},
-						},
-						'5'
-					) }
-				/>
-			);
-
-			const input = container.querySelector( 'input[type="number"]' );
-			expect( input ).toBeInstanceOf( HTMLInputElement );
-			expect( ( input as HTMLInputElement ).disabled ).toBe( true );
-			expect(
-				isSpinButtonDisabled(
-					getSpinButton( container, 'Increment Low stock threshold' )
-				)
-			).toBe( true );
-			expect(
-				isSpinButtonDisabled(
-					getSpinButton( container, 'Decrement Low stock threshold' )
-				)
-			).toBe( true );
-		} );
-
-		it( 'lets top-level disabled props override number input custom attributes', () => {
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps(
-						{
-							...numberField,
-							disabled: false,
-							customAttributes: {
-								...numberField.customAttributes,
-								disabled: 'true',
-							},
-						},
-						'5'
-					) }
-				/>
-			);
-
-			const input = container.querySelector( 'input[type="number"]' );
-			expect( input ).toBeInstanceOf( HTMLInputElement );
-			expect( ( input as HTMLInputElement ).disabled ).toBe( false );
-			expect(
-				isSpinButtonDisabled(
-					getSpinButton( container, 'Increment Low stock threshold' )
-				)
-			).toBe( false );
-			expect(
-				isSpinButtonDisabled(
-					getSpinButton( container, 'Decrement Low stock threshold' )
-				)
-			).toBe( false );
-		} );
-
-		it( 'calls onChange with the stepped value and announces it when a spin button is clicked', () => {
-			const onChange = jest.fn();
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps( numberField, '5', onChange ) }
-				/>
-			);
-
-			clickButton(
-				getSpinButton( container, 'Increment Low stock threshold' )
-			);
-
-			expect( onChange ).toHaveBeenCalledWith( '6' );
-			expect( speak ).toHaveBeenCalledWith( '6' );
-		} );
-
-		it( 'disables the decrement button at the minimum value', () => {
-			const container = render(
-				<NativeSettingsField { ...makeProps( numberField, '0' ) } />
-			);
-
-			expect(
-				isSpinButtonDisabled(
-					getSpinButton( container, 'Decrement Low stock threshold' )
-				)
-			).toBe( true );
-			expect(
-				isSpinButtonDisabled(
-					getSpinButton( container, 'Increment Low stock threshold' )
-				)
-			).toBe( false );
-		} );
-
-		it( 'disables the increment button at the maximum value', () => {
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps(
-						{
-							...numberField,
-							customAttributes: { min: 0, max: 10, step: 1 },
-						},
-						'10'
-					) }
-				/>
-			);
-
-			expect(
-				isSpinButtonDisabled(
-					getSpinButton( container, 'Increment Low stock threshold' )
-				)
-			).toBe( true );
-			expect(
-				isSpinButtonDisabled(
-					getSpinButton( container, 'Decrement Low stock threshold' )
-				)
-			).toBe( false );
-		} );
-
-		it( 'falls back to a step of 1 when the schema provides a non-positive step', () => {
-			const onChange = jest.fn();
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps(
-						{
-							...numberField,
-							customAttributes: { min: 0, step: 0 },
-						},
-						'5',
-						onChange
-					) }
-				/>
-			);
-
-			clickButton(
-				getSpinButton( container, 'Increment Low stock threshold' )
-			);
-
-			expect( onChange ).toHaveBeenCalledWith( '6' );
-		} );
-
-		it( 'clamps stepping to the maximum and avoids float precision drift', () => {
-			const onChange = jest.fn();
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps(
-						{
-							...numberField,
-							customAttributes: { min: 0, max: 0.3, step: 0.2 },
-						},
-						'0.1',
-						onChange
-					) }
-				/>
-			);
-
-			clickButton(
-				getSpinButton( container, 'Increment Low stock threshold' )
-			);
-
-			expect( onChange ).toHaveBeenCalledWith( '0.3' );
-		} );
-
-		it( 'preserves current value precision when it exceeds step precision', () => {
-			const onChange = jest.fn();
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps(
-						{
-							...numberField,
-							customAttributes: { min: 0, step: 0.1 },
-						},
-						'0.05',
-						onChange
-					) }
-				/>
-			);
-
-			clickButton(
-				getSpinButton( container, 'Increment Low stock threshold' )
-			);
-
-			expect( onChange ).toHaveBeenCalledWith( '0.15' );
-		} );
-
-		it( 'handles scientific-notation steps', () => {
-			const onChange = jest.fn();
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps(
-						{
-							...numberField,
-							customAttributes: { min: 0, step: 1e-7 },
-						},
-						'0',
-						onChange
-					) }
-				/>
-			);
-
-			clickButton(
-				getSpinButton( container, 'Increment Low stock threshold' )
-			);
-
-			expect( onChange ).toHaveBeenCalledWith( '1e-7' );
-		} );
-
-		it( 'does not exceed toFixed precision limits for tiny scientific-notation steps', () => {
-			const onChange = jest.fn();
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps(
-						{
-							...numberField,
-							customAttributes: { min: 0, step: 1e-200 },
-						},
-						'0',
-						onChange
-					) }
-				/>
-			);
-
-			expect( () =>
-				clickButton(
-					getSpinButton( container, 'Increment Low stock threshold' )
-				)
-			).not.toThrow();
-			expect( onChange ).toHaveBeenCalledWith( '1e-200' );
-		} );
-
-		it( 'steps onto the minimum from an empty value', () => {
-			const onChange = jest.fn();
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps(
-						{
-							...numberField,
-							customAttributes: { min: 2, step: 1 },
-						},
-						'',
-						onChange
-					) }
-				/>
-			);
-
-			clickButton(
-				getSpinButton( container, 'Increment Low stock threshold' )
-			);
-
-			expect( onChange ).toHaveBeenCalledWith( '2' );
-		} );
-	} );
-
-	describe( 'select fields', () => {
-		it( 'renders a public select control and propagates scalar values', () => {
-			const onChange = jest.fn();
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps(
-						{
-							id: 'wc_test_select',
-							label: 'Inventory format',
-							description: 'Choose how inventory is displayed.',
-							type: 'select',
-							options: [
-								{ value: 'one', label: 'One' },
-								{ value: 'two', label: 'Two' },
-							],
-						},
-						'one',
-						onChange
-					) }
-				/>
-			);
-
-			const select = container.querySelector( 'select' );
-			expect( select ).toBeInstanceOf( HTMLSelectElement );
-			expect( select ).toHaveValue( 'one' );
-			expect( container.textContent ).toContain( 'Inventory format' );
-			expect( container.textContent ).toContain(
-				'Choose how inventory is displayed.'
-			);
-
-			act( () => {
-				if ( select instanceof HTMLSelectElement ) {
-					select.value = 'two';
-					select.dispatchEvent(
-						new Event( 'change', {
-							bubbles: true,
-							cancelable: true,
-						} )
-					);
-				}
-			} );
-
-			expect( onChange ).toHaveBeenCalledWith( 'two' );
-		} );
-
-		it.each( [
-			[ 'an empty option list', [], '' ],
-			[
-				'an unmatched stored value',
-				[ { label: 'One', value: 'one' } ],
-				'legacy',
-			],
-		] )(
-			'keeps the labeled control for %s',
-			( _scenario, options, value ) => {
-				const container = render(
-					<NativeSettingsField
-						{ ...makeProps(
-							{
-								id: 'wc_test_select',
-								label: 'Test select',
-								type: 'select',
-								options,
-							},
-							value
-						) }
-					/>
-				);
-
-				const select = container.querySelector( 'select' );
-				expect( select ).toBeInstanceOf( HTMLSelectElement );
-				expect( select ).toHaveAccessibleName( 'Test select' );
-				expect( select ).toHaveValue( value );
-				expect( select?.selectedOptions[ 0 ] ).toHaveTextContent(
-					'Select'
-				);
-				expect( select?.selectedOptions[ 0 ] ).toBeDisabled();
-			}
-		);
-
-		it.each( [ {}, 'invalid' ] )(
-			'handles malformed non-array options without throwing',
-			( options ) => {
-				const field = {
-					id: 'wc_test_select',
-					label: 'Test select',
-					type: 'select' as const,
-					options,
-				} as unknown as SettingsUIField;
-
-				const container = render(
-					<NativeSettingsField { ...makeProps( field, 'legacy' ) } />
-				);
-
-				const select = container.querySelector( 'select' );
-				expect( select ).toBeInstanceOf( HTMLSelectElement );
-				expect( select ).toHaveValue( 'legacy' );
-				expect( select?.options ).toHaveLength( 1 );
-			}
-		);
-	} );
-
-	describe( 'text fields', () => {
-		it( 'renders text fields without spin buttons', () => {
-			const container = render(
-				<NativeSettingsField
-					{ ...makeProps(
-						{
-							id: 'wc_test_text',
-							label: 'Store name',
-							type: 'text',
-						},
-						'hello'
-					) }
-				/>
-			);
-
-			expect(
-				container.querySelector( 'input[type="text"]' )
-			).not.toBeNull();
-			expect(
-				container.querySelector( '.wc-settings-ui__number-control' )
-			).toBeNull();
-		} );
-	} );
-
-	it( 'reports which field types have a native renderer', () => {
-		expect( isNativeSettingsFieldType( 'text' ) ).toBe( true );
-		expect( isNativeSettingsFieldType( 'select' ) ).toBe( true );
-		expect( isNativeSettingsFieldType( 'extension_defined' ) ).toBe(
-			false
-		);
-	} );
-} );
diff --git a/packages/js/settings-ui/src/types.ts b/packages/js/settings-ui/src/types.ts
index c90b3a7b88c..21a606801ed 100644
--- a/packages/js/settings-ui/src/types.ts
+++ b/packages/js/settings-ui/src/types.ts
@@ -119,21 +119,6 @@ export type SettingsFieldContext = {
 	section?: string;
 };

-export type SettingsFieldComponentProps = {
-	field: SettingsUIField;
-	value: SettingsValue;
-	onChange: ( value: SettingsValue ) => void;
-	values: SettingsValues;
-	initialValues: SettingsValues;
-	setValue: ( fieldId: string, value: SettingsValue ) => void;
-	setValues: ( values: Partial< SettingsValues > ) => void;
-	context: SettingsFieldContext;
-};
-
-export type SettingsFieldComponent = (
-	props: SettingsFieldComponentProps
-) => JSX.Element | null;
-
 /**
  * The field surface a registered edit control receives. A frozen subset of
  * the DataForm field, so extensions do not couple to package internals.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 855134b1651..220c70a520e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2284,9 +2284,6 @@ importers:
       '@woocommerce/sanitize':
         specifier: workspace:*
         version: link:../sanitize
-      '@wordpress/a11y':
-        specifier: catalog:wp-min
-        version: 4.33.1
       '@wordpress/admin-ui':
         specifier: catalog:wp-bundled
         version: 2.4.1(@date-fns/tz@1.4.1)(@emotion/is-prop-valid@1.4.0)(@types/react@18.3.28)(date-fns@4.1.0)(postcss@8.5.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@16.26.1(typescript@5.7.3))