Commit 66a25f33c7f for woocommerce

commit 66a25f33c7f86412bf00fbd3e588fa5b4514e74a
Author: Rostislav Wolný <1082140+costasovo@users.noreply.github.com>
Date:   Tue Aug 11 19:53:12 2026 +0200

    [Email Editor] Restore text and background color controls on WP 7.1 (#67467)

    * Restore text and background color controls on WordPress 7.1

    WordPress 7.1 (Gutenberg PR 77279) removed the Text and Background rows
    from the block editor's private ColorPanel, which the email editor's
    Styles > Colors screen delegates to, so both controls disappeared with
    no replacement. Follow the new core placement instead of forking the old
    panel: on 7.1+ a Background screen (core BackgroundPanel scoped to color
    only) joins the styles sidebar and a Color control is added to
    Typography > Text, while 6.9/7.0 keep the current UI. Placement is
    decided by probing the unlocked useHasColorPanel/useHasBackgroundPanel
    gates rather than sniffing WP versions, with fallbacks at the unlock
    site degrading to legacy behavior if an export is missing.

    * Add changelog entry for email editor color controls

diff --git a/packages/js/email-editor/changelog/add-background-and-text-color-controls-wp71 b/packages/js/email-editor/changelog/add-background-and-text-color-controls-wp71
new file mode 100644
index 00000000000..a8248949d10
--- /dev/null
+++ b/packages/js/email-editor/changelog/add-background-and-text-color-controls-wp71
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Restore text and background color controls in the styles sidebar on WordPress 7.1+ — text color moves to the typography panel and background color to a new Background screen, matching where WordPress 7.1 places them.
diff --git a/packages/js/email-editor/src/components/styles-sidebar/hooks.ts b/packages/js/email-editor/src/components/styles-sidebar/hooks.ts
new file mode 100644
index 00000000000..afdb3da55b4
--- /dev/null
+++ b/packages/js/email-editor/src/components/styles-sidebar/hooks.ts
@@ -0,0 +1,61 @@
+/**
+ * External dependencies
+ */
+import { useMemo } from '@wordpress/element';
+import { useSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { storeName } from '../../store';
+import {
+	useHasStylesColorPanel,
+	useHasStylesBackgroundPanel,
+} from '../../private-apis';
+
+// Settings describing text and background color support only. Up to
+// WordPress 7.0 the core ColorPanel handles these controls, so its gate
+// returns true for them; since WordPress 7.1 text color renders in the
+// typography panel and background color in the background panel, and the
+// gate returns false.
+const TEXT_BACKGROUND_PROBE_SETTINGS = {
+	color: { text: true, background: true, custom: true },
+};
+
+/**
+ * Whether the text color control belongs in the typography panel rather than
+ * the Colors screen in the running WordPress version.
+ */
+export function useHasTextColorInTypographyPanel(): boolean {
+	return ! useHasStylesColorPanel( TEXT_BACKGROUND_PROBE_SETTINGS );
+}
+
+/**
+ * Theme settings scoped to the background color control only. Background
+ * image and gradient stay disabled — the global styles Background screen
+ * manages just the email background color; per-block background images are
+ * unaffected.
+ */
+export function useBackgroundScreenSettings() {
+	const theme = useSelect( ( select ) => select( storeName ).getTheme(), [] );
+	return useMemo(
+		() => ( {
+			...theme?.settings,
+			background: {
+				backgroundImage: false,
+				backgroundSize: false,
+				gradient: false,
+			},
+		} ),
+		[ theme?.settings ]
+	);
+}
+
+/**
+ * Whether the styles sidebar has a Background screen. True only in WordPress
+ * versions whose BackgroundPanel renders the background color control
+ * (7.1+) — older versions keep background color in the Colors screen.
+ */
+export function useHasBackgroundScreen(): boolean {
+	return useHasStylesBackgroundPanel( useBackgroundScreenSettings() );
+}
diff --git a/packages/js/email-editor/src/components/styles-sidebar/panels/color-dropdown-item.tsx b/packages/js/email-editor/src/components/styles-sidebar/panels/color-dropdown-item.tsx
new file mode 100644
index 00000000000..e4e62b6e577
--- /dev/null
+++ b/packages/js/email-editor/src/components/styles-sidebar/panels/color-dropdown-item.tsx
@@ -0,0 +1,129 @@
+/**
+ * External dependencies
+ */
+import clsx from 'clsx';
+import { __ } from '@wordpress/i18n';
+import { useRef } from '@wordpress/element';
+import { reset as resetIcon } from '@wordpress/icons';
+import {
+	Button,
+	ColorIndicator,
+	Dropdown,
+	FlexItem,
+	__experimentalDropdownContentWrapper as DropdownContentWrapper, // eslint-disable-line
+	__experimentalHStack as HStack, // eslint-disable-line
+	__experimentalToolsPanelItem as ToolsPanelItem, // eslint-disable-line
+} from '@wordpress/components';
+// eslint-disable-next-line
+import {
+	// @ts-expect-error TS7016: Could not find a declaration file for module '@wordpress/block-editor'.
+	__experimentalColorGradientControl as ColorGradientControl, // eslint-disable-line
+} from '@wordpress/block-editor';
+
+const popoverProps = {
+	placement: 'left-start' as const,
+	offset: 36,
+	shift: true,
+};
+
+/**
+ * Color-only port of the dropdown item used by the global styles color panels
+ * in the block editor, which is not exposed through the private APIs surface.
+ * Reuses the core class names so the block editor stylesheet applies.
+ *
+ * @see https://github.com/WordPress/gutenberg/blob/trunk/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js
+ */
+export function ColorDropdownItem( {
+	label,
+	hasValue,
+	resetValue,
+	isShownByDefault,
+	inheritedValue,
+	userValue,
+	setValue,
+	colorGradientControlSettings,
+}: {
+	label: string;
+	hasValue: () => boolean;
+	resetValue: () => void;
+	isShownByDefault: boolean;
+	inheritedValue?: string;
+	userValue?: string;
+	setValue: ( newValue?: string ) => void;
+	colorGradientControlSettings: Record< string, unknown >;
+} ): JSX.Element {
+	const dropdownButtonRef = useRef< HTMLButtonElement | null >( null );
+	return (
+		<ToolsPanelItem
+			className="block-editor-color-gradient-item block-editor-tools-panel-color-gradient-settings__item"
+			hasValue={ hasValue }
+			label={ label }
+			onDeselect={ resetValue }
+			isShownByDefault={ isShownByDefault }
+		>
+			<Dropdown
+				popoverProps={ popoverProps }
+				className="block-editor-tools-panel-color-gradient-settings__dropdown"
+				renderToggle={ ( { onToggle, isOpen } ) => (
+					<>
+						<Button
+							onClick={ onToggle }
+							className={ clsx(
+								'block-editor-panel-color-gradient-settings__dropdown',
+								{ 'is-open': isOpen }
+							) }
+							aria-expanded={ isOpen }
+							ref={ dropdownButtonRef }
+							__next40pxDefaultSize
+						>
+							<HStack justify="flex-start">
+								<ColorIndicator
+									className="block-editor-panel-color-gradient-settings__color-indicator"
+									colorValue={ inheritedValue }
+								/>
+								<FlexItem
+									className="block-editor-panel-color-gradient-settings__color-name"
+									title={ label }
+								>
+									{ label }
+								</FlexItem>
+							</HStack>
+						</Button>
+						{ hasValue() && (
+							<Button
+								__next40pxDefaultSize
+								label={ __( 'Reset', __i18n_text_domain__ ) }
+								className="block-editor-panel-color-gradient-settings__reset"
+								size="small"
+								icon={ resetIcon }
+								onClick={ () => {
+									resetValue();
+									if ( isOpen ) {
+										onToggle();
+									}
+									dropdownButtonRef.current?.focus();
+								} }
+							/>
+						) }
+					</>
+				) }
+				renderContent={ () => (
+					<DropdownContentWrapper paddingSize="none">
+						<div className="block-editor-panel-color-gradient-settings__dropdown-content">
+							<ColorGradientControl
+								{ ...colorGradientControlSettings }
+								showTitle={ false }
+								enableAlpha
+								__experimentalIsRenderedInSidebar
+								colorValue={ inheritedValue }
+								onColorChange={ setValue }
+								clearable={ inheritedValue === userValue }
+								headingLevel={ 3 }
+							/>
+						</div>
+					</DropdownContentWrapper>
+				) }
+			/>
+		</ToolsPanelItem>
+	);
+}
diff --git a/packages/js/email-editor/src/components/styles-sidebar/panels/typography-element-panel.tsx b/packages/js/email-editor/src/components/styles-sidebar/panels/typography-element-panel.tsx
index cafbbdfaa94..43e1445466e 100644
--- a/packages/js/email-editor/src/components/styles-sidebar/panels/typography-element-panel.tsx
+++ b/packages/js/email-editor/src/components/styles-sidebar/panels/typography-element-panel.tsx
@@ -3,6 +3,11 @@
  */
 import { __ } from '@wordpress/i18n';
 import { useCallback } from '@wordpress/element';
+import { useSelect } from '@wordpress/data';
+import {
+	getValueFromVariable,
+	getPresetVariableFromValue,
+} from '@wordpress/global-styles-engine';
 import {
 	FontSizePicker,
 	__experimentalToolsPanel as ToolsPanel, // eslint-disable-line
@@ -25,16 +30,22 @@ import {
 	__experimentalTextDecorationControl as TextDecorationControl, // eslint-disable-line
 	// @ts-expect-error TS7016: Could not find a declaration file for module '@wordpress/block-editor'.
 	__experimentalTextTransformControl as TextTransformControl, // eslint-disable-line
+	// @ts-expect-error TS7016: Could not find a declaration file for module '@wordpress/block-editor'.
+	__experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, // eslint-disable-line
 } from '@wordpress/block-editor';

 /**
  * Internal dependencies
  */
-import { useEmailStyles } from '../../../hooks';
+import { useEmailStyles, setImmutably } from '../../../hooks';
+import { storeName } from '../../../store';
 import { getElementStyles } from '../utils';
+import { useHasTextColorInTypographyPanel } from '../hooks';
+import { ColorDropdownItem } from './color-dropdown-item';
 import { recordEvent, debouncedRecordEvent } from '../../../events';

 export const DEFAULT_CONTROLS = {
+	textColor: true,
 	fontFamily: true,
 	fontSize: true,
 	fontAppearance: true,
@@ -62,7 +73,14 @@ export function TypographyElementPanel( {

 	// Ref: https://github.com/WordPress/gutenberg/issues/59778
 	const fontFamilies = blockLevelFontFamilies?.default || [];
-	const { styles, defaultStyles, updateStyleProp } = useEmailStyles();
+	const theme = useSelect( ( select ) => select( storeName ).getTheme(), [] );
+	const colorGradientSettings = useMultipleOriginColorsAndGradients();
+	// Text color renders here only when the running WordPress no longer offers
+	// it in the Colors screen (WordPress 7.1 moved it to the typography panel).
+	const showTextColor =
+		useHasTextColorInTypographyPanel() && element === 'text';
+	const { styles, defaultStyles, userStyles, updateStyleProp, updateStyles } =
+		useEmailStyles();
 	const elementStyles = getElementStyles( styles, element, headingLevel );
 	const defaultElementStyles = getElementStyles(
 		defaultStyles,
@@ -91,6 +109,24 @@ export function TypographyElementPanel( {
 		textTransform: defaultTextTransform,
 	} = defaultElementStyles.typography;

+	// The text color renders only for the `text` element, whose styles live at
+	// the root of the styles object.
+	const userTextColor = userStyles?.color?.text;
+	const defaultTextColor = defaultElementStyles.color?.text;
+	const decodedTextColor = getValueFromVariable(
+		{ settings: theme?.settings },
+		'',
+		elementStyles.color?.text
+	);
+	const decodedUserTextColor = userTextColor
+		? getValueFromVariable(
+				{ settings: theme?.settings },
+				'',
+				userTextColor
+		  )
+		: undefined;
+
+	const hasTextColor = () => !! userTextColor;
 	const hasFontFamily = () => fontFamily !== defaultFontFamily;
 	const hasFontSize = () => fontSize !== defaultFontSize;
 	const hasFontAppearance = () =>
@@ -118,6 +154,29 @@ export function TypographyElementPanel( {
 		[ element, updateStyleProp, headingLevel ]
 	);

+	const setTextColor = ( newValue ) => {
+		// Store palette colors as preset references so later palette changes
+		// propagate, matching how the core ColorPanel writes them.
+		const encodedValue =
+			newValue === undefined
+				? undefined
+				: getPresetVariableFromValue(
+						theme?.settings,
+						undefined,
+						'color.text',
+						newValue
+				  );
+		updateElementStyleProp( [ 'color', 'text' ], encodedValue );
+		debouncedRecordEvent(
+			'styles_sidebar_screen_typography_element_panel_set_text_color',
+			{
+				element,
+				newValue,
+				selectedDefaultTextColor: encodedValue === defaultTextColor,
+			}
+		);
+	};
+
 	const setLetterSpacing = ( newValue ) => {
 		updateElementStyleProp( [ 'typography', 'letterSpacing' ], newValue );
 		debouncedRecordEvent(
@@ -211,7 +270,23 @@ export function TypographyElementPanel( {
 	};

 	const resetAll = () => {
-		updateElementStyleProp( [ 'typography' ], {} );
+		if ( showTextColor ) {
+			// Single update — a second updateStyleProp call in the same
+			// handler would work from a stale userTheme and clobber this edit.
+			updateStyles(
+				setImmutably(
+					setImmutably(
+						userStyles ?? {},
+						[ 'color', 'text' ],
+						undefined
+					),
+					[ 'typography' ],
+					{}
+				)
+			);
+		} else {
+			updateElementStyleProp( [ 'typography' ], {} );
+		}
 		recordEvent(
 			'styles_sidebar_screen_typography_element_panel_reset_all_styles_selected',
 			{
@@ -226,6 +301,18 @@ export function TypographyElementPanel( {
 			label={ __( 'Typography', __i18n_text_domain__ ) }
 			resetAll={ resetAll }
 		>
+			{ showTextColor && (
+				<ColorDropdownItem
+					label={ __( 'Color', __i18n_text_domain__ ) }
+					hasValue={ hasTextColor }
+					resetValue={ () => setTextColor( undefined ) }
+					isShownByDefault={ defaultControls.textColor }
+					inheritedValue={ decodedTextColor }
+					userValue={ decodedUserTextColor }
+					setValue={ setTextColor }
+					colorGradientControlSettings={ colorGradientSettings }
+				/>
+			) }
 			<ToolsPanelItem
 				label={ __( 'Font family', __i18n_text_domain__ ) }
 				hasValue={ hasFontFamily }
diff --git a/packages/js/email-editor/src/components/styles-sidebar/screens/index.ts b/packages/js/email-editor/src/components/styles-sidebar/screens/index.ts
index e1abc16c948..7d3df3bebd8 100644
--- a/packages/js/email-editor/src/components/styles-sidebar/screens/index.ts
+++ b/packages/js/email-editor/src/components/styles-sidebar/screens/index.ts
@@ -3,3 +3,4 @@ export * from './screen-layout';
 export * from './screen-root';
 export * from './screen-typography-element';
 export * from './screen-colors';
+export * from './screen-background';
diff --git a/packages/js/email-editor/src/components/styles-sidebar/screens/screen-background.tsx b/packages/js/email-editor/src/components/styles-sidebar/screens/screen-background.tsx
new file mode 100644
index 00000000000..163b92d90d6
--- /dev/null
+++ b/packages/js/email-editor/src/components/styles-sidebar/screens/screen-background.tsx
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import ScreenHeader from './screen-header';
+import { useEmailStyles } from '../../../hooks';
+import { useBackgroundScreenSettings } from '../hooks';
+import { recordEvent, recordEventOnce } from '../../../events';
+import { StylesBackgroundPanel } from '../../../private-apis';
+
+export function ScreenBackground(): JSX.Element {
+	recordEventOnce( 'styles_sidebar_screen_background_opened' );
+	const { userStyles, styles, updateStyles } = useEmailStyles();
+	const settings = useBackgroundScreenSettings();
+
+	const handleOnChange = ( newStyles ) => {
+		updateStyles( newStyles );
+		recordEvent( 'styles_sidebar_screen_background_styles_updated' ); // We can't log the updated color here because the onChange function returns the complete object.
+	};
+
+	return (
+		<>
+			<ScreenHeader
+				title={ __( 'Background', __i18n_text_domain__ ) }
+				description={ __(
+					'Manage the background color of the email.',
+					__i18n_text_domain__
+				) }
+			/>
+			<StylesBackgroundPanel
+				value={ userStyles }
+				inheritedValue={ styles }
+				onChange={ handleOnChange }
+				settings={ settings }
+				panelId="background"
+			/>
+		</>
+	);
+}
diff --git a/packages/js/email-editor/src/components/styles-sidebar/screens/screen-root.tsx b/packages/js/email-editor/src/components/styles-sidebar/screens/screen-root.tsx
index b6ad66e977c..d317a1c0b7b 100644
--- a/packages/js/email-editor/src/components/styles-sidebar/screens/screen-root.tsx
+++ b/packages/js/email-editor/src/components/styles-sidebar/screens/screen-root.tsx
@@ -2,7 +2,7 @@
  * External dependencies
  */
 import { __ } from '@wordpress/i18n';
-import { typography, color, layout } from '@wordpress/icons';
+import { typography, color, background, layout } from '@wordpress/icons';
 import {
 	__experimentalVStack as VStack, // eslint-disable-line
 	Card,
@@ -20,9 +20,11 @@ import {
  * Internal dependencies
  */
 import { Preview } from './preview';
+import { useHasBackgroundScreen } from '../hooks';
 import { recordEvent } from '../../../events';

 export function ScreenRoot(): JSX.Element {
+	const hasBackgroundScreen = useHasBackgroundScreen();
 	return (
 		<Card
 			size="small"
@@ -76,6 +78,29 @@ export function ScreenRoot(): JSX.Element {
 								</HStack>
 							</Item>
 						</NavigatorButton>
+						{ hasBackgroundScreen && (
+							<NavigatorButton
+								path="/background"
+								onClick={ () =>
+									recordEvent(
+										'styles_sidebar_navigation_click',
+										{ path: 'background' }
+									)
+								}
+							>
+								<Item>
+									<HStack justify="flex-start">
+										<Icon icon={ background } size={ 24 } />
+										<FlexItem>
+											{ __(
+												'Background',
+												__i18n_text_domain__
+											) }
+										</FlexItem>
+									</HStack>
+								</Item>
+							</NavigatorButton>
+						) }
 						<NavigatorButton
 							path="/layout"
 							onClick={ () =>
diff --git a/packages/js/email-editor/src/components/styles-sidebar/styles-sidebar.tsx b/packages/js/email-editor/src/components/styles-sidebar/styles-sidebar.tsx
index eb78e7db460..c6076b98733 100644
--- a/packages/js/email-editor/src/components/styles-sidebar/styles-sidebar.tsx
+++ b/packages/js/email-editor/src/components/styles-sidebar/styles-sidebar.tsx
@@ -17,6 +17,7 @@ import {
 	ScreenLayout,
 	ScreenRoot,
 	ScreenColors,
+	ScreenBackground,
 } from './screens';
 import { Navigator } from './navigator';

@@ -72,6 +73,10 @@ export function RawStylesSidebar(): JSX.Element {
 							<ScreenColors />
 						</Navigator.Screen>

+						<Navigator.Screen path="/background">
+							<ScreenBackground />
+						</Navigator.Screen>
+
 						<Navigator.Screen path="/layout">
 							<ScreenLayout />
 						</Navigator.Screen>
diff --git a/packages/js/email-editor/src/components/test/__mocks__/wordpress-block-editor.ts b/packages/js/email-editor/src/components/test/__mocks__/wordpress-block-editor.ts
index 1cb347ba9a8..7c841a4b53e 100644
--- a/packages/js/email-editor/src/components/test/__mocks__/wordpress-block-editor.ts
+++ b/packages/js/email-editor/src/components/test/__mocks__/wordpress-block-editor.ts
@@ -3,6 +3,9 @@ jest.mock( '@wordpress/block-editor', () => ( {
 	privateApis: {
 		// Mock the private APIs that are used by the email editor
 		ColorPanel: jest.fn( () => null ),
+		BackgroundPanel: jest.fn( () => null ),
+		useHasColorPanel: jest.fn( () => true ),
+		useHasBackgroundPanel: jest.fn( () => false ),
 		useGlobalStylesOutputWithConfig: jest.fn( () => [ [], {} ] ),
 	},
 } ) );
diff --git a/packages/js/email-editor/src/private-apis/index.ts b/packages/js/email-editor/src/private-apis/index.ts
index 5ba12070e36..8fff7300c6d 100644
--- a/packages/js/email-editor/src/private-apis/index.ts
+++ b/packages/js/email-editor/src/private-apis/index.ts
@@ -18,9 +18,26 @@ const { unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules(
 );

 /**
- * We use the ColorPanel component from the block editor to render the color panel in the style settings sidebar.
+ * We use the ColorPanel and BackgroundPanel components from the block editor to render
+ * the color and background panels in the style settings sidebar.
+ *
+ * Since WordPress 7.1 the ColorPanel no longer renders the text and background color
+ * controls — text color lives in the typography panel and background color in the
+ * background panel. The useHasColorPanel and useHasBackgroundPanel hooks let us detect
+ * where the controls live in the running WordPress version. The fallbacks cover a WordPress
+ * version whose private API surface lacks these exports and resolve to the legacy
+ * behavior: text and background color handled by ColorPanel, no background screen.
  */
-const { ColorPanel: StylesColorPanel } = unlock( blockEditorPrivateApis );
+const {
+	ColorPanel: StylesColorPanel,
+	BackgroundPanel,
+	useHasColorPanel,
+	useHasBackgroundPanel,
+} = unlock( blockEditorPrivateApis );
+
+const StylesBackgroundPanel = BackgroundPanel ?? ( () => null );
+const useHasStylesColorPanel = useHasColorPanel ?? ( () => true );
+const useHasStylesBackgroundPanel = useHasBackgroundPanel ?? ( () => false );

 /**
  * The Editor is the main component for the email editor.
@@ -39,6 +56,9 @@ const { registerEntityAction, unregisterEntityAction } = unlock(

 export {
 	StylesColorPanel,
+	StylesBackgroundPanel,
+	useHasStylesColorPanel,
+	useHasStylesBackgroundPanel,
 	Editor,
 	FullscreenMode,
 	ViewMoreMenuGroup,