Commit 9c347aa4003 for woocommerce
commit 9c347aa40032dfe4879e5530eba76f2d0d46bb94
Author: Allison Levine <1689238+allilevine@users.noreply.github.com>
Date: Tue Oct 20 08:57:57 2026 -0400
[Email Editor] Fix italic Appearance options not being saved (#70006)
* Save italic Appearance options in the email editor Styles sidebar
`setFontAppearance` dispatched twice from one handler, once for `fontStyle`
and once for `fontWeight`. Both dispatches derived from the same captured
`userTheme`, because React has not re-rendered between two synchronous calls,
so the second overwrote the first. The weight landed and the style was always
dropped: picking "Bold Italic" stored only `fontWeight: 700` and the control
re-read the theme's inherited `fontStyle: normal`, showing "Bold".
Write both props in a single `updateStyles` call built with nested
`setImmutably`, the shape `resetAll` in the same file already uses. The
element path logic moves out of `updateElementStyleProp` into
`getElementStylePath` so both call sites share it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Cover the Appearance deselect path in the typography panel tests
`onDeselect` routes through `setFontAppearance` with both props undefined,
which the single-update rewrite has to clear together. Assert that with
`toStrictEqual` so a missing key fails rather than matching `undefined`.
Fold the four `onChange` invocations into a `selectAppearance` helper that
checks the control rendered first, so a panel that stops rendering
FontAppearanceControl fails on the assertion instead of a TypeError.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/packages/js/email-editor/changelog/fix-NL-1053-appearance-font-style b/packages/js/email-editor/changelog/fix-NL-1053-appearance-font-style
new file mode 100644
index 00000000000..fe4b3f17d4e
--- /dev/null
+++ b/packages/js/email-editor/changelog/fix-NL-1053-appearance-font-style
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Save the italic Appearance options in the email editor Styles sidebar instead of silently reverting to the non-italic equivalent.
diff --git a/packages/js/email-editor/src/components/styles-sidebar/panels/test/typography-element-panel.spec.tsx b/packages/js/email-editor/src/components/styles-sidebar/panels/test/typography-element-panel.spec.tsx
index 4468f9e36cf..502d2d5047a 100644
--- a/packages/js/email-editor/src/components/styles-sidebar/panels/test/typography-element-panel.spec.tsx
+++ b/packages/js/email-editor/src/components/styles-sidebar/panels/test/typography-element-panel.spec.tsx
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { render } from '@testing-library/react';
+import { act, render } from '@testing-library/react';
import { FontSizePicker } from '@wordpress/components';
import { useSettings } from '@wordpress/block-editor';
@@ -10,6 +10,20 @@ import { useSettings } from '@wordpress/block-editor';
*/
import { TypographyElementPanel } from '../typography-element-panel';
+// Holds the `onChange` the panel passes to FontAppearanceControl, so tests can
+// fire an Appearance selection the way the dropdown does.
+const fontAppearance: {
+ onChange?: ( value: { fontStyle?: string; fontWeight?: string } ) => void;
+} = {};
+
+const emailStyles = {
+ styles: {},
+ defaultStyles: {},
+ userStyles: {} as Record< string, unknown >,
+ updateStyleProp: jest.fn(),
+ updateStyles: jest.fn(),
+};
+
jest.mock( '@wordpress/components', () => ( {
FontSizePicker: jest.fn( () => null ),
__experimentalToolsPanel: ( { children } ) => <div>{ children }</div>,
@@ -18,7 +32,10 @@ jest.mock( '@wordpress/components', () => ( {
jest.mock( '@wordpress/block-editor', () => ( {
useSettings: jest.fn(),
- __experimentalFontAppearanceControl: () => null,
+ __experimentalFontAppearanceControl: ( props ) => {
+ fontAppearance.onChange = props.onChange;
+ return null;
+ },
__experimentalLetterSpacingControl: () => null,
__experimentalFontFamilyControl: () => null,
LineHeightControl: () => null,
@@ -40,15 +57,18 @@ jest.mock( '@wordpress/global-styles-engine', () => ( {
getPresetVariableFromValue: jest.fn(),
} ) );
+// Stubbed so the real `use-email-styles` module below can load without
+// pulling in `@wordpress/core-data`.
+jest.mock( '../../../../hooks/use-user-theme', () => ( {
+ useUserTheme: () => ( { userTheme: {}, updateUserTheme: jest.fn() } ),
+} ) );
+
+// `setImmutably` stays real: the panel builds the saved styles object with it,
+// and a stub would hide whether both Appearance props survive the write.
jest.mock( '../../../../hooks', () => ( {
- useEmailStyles: () => ( {
- styles: {},
- defaultStyles: {},
- userStyles: {},
- updateStyleProp: jest.fn(),
- updateStyles: jest.fn(),
- } ),
- setImmutably: jest.fn(),
+ setImmutably: jest.requireActual( '../../../../hooks/use-email-styles' )
+ .setImmutably,
+ useEmailStyles: () => emailStyles,
} ) );
jest.mock( '../../utils', () => ( {
@@ -71,18 +91,29 @@ jest.mock( '../../../../events', () => ( {
const useSettingsMock = useSettings as jest.Mock;
const fontSizePickerMock = FontSizePicker as unknown as jest.Mock;
+const selectAppearance = ( value: {
+ fontStyle?: string;
+ fontWeight?: string;
+} ) => {
+ expect( fontAppearance.onChange ).toBeDefined();
+ act( () => fontAppearance.onChange( value ) );
+};
+
describe( 'TypographyElementPanel', () => {
beforeEach( () => {
fontSizePickerMock.mockClear();
- } );
-
- it( 'passes the spacing units from the email theme to the font size picker', () => {
+ emailStyles.updateStyleProp.mockClear();
+ emailStyles.updateStyles.mockClear();
+ emailStyles.userStyles = {};
+ delete fontAppearance.onChange;
useSettingsMock.mockImplementation( ( ...paths: string[] ) =>
paths[ 0 ] === 'spacing.units'
? [ [ 'px' ] ]
: [ [], { default: [] } ]
);
+ } );
+ it( 'passes the spacing units from the email theme to the font size picker', () => {
render( <TypographyElementPanel element="text" headingLevel="" /> );
expect( fontSizePickerMock ).toHaveBeenCalled();
@@ -90,4 +121,88 @@ describe( 'TypographyElementPanel', () => {
'px',
] );
} );
+
+ it( 'stores both Appearance props in a single update, keeping other user styles', () => {
+ emailStyles.userStyles = {
+ typography: { fontSize: '16px' },
+ color: { text: '#111111' },
+ };
+
+ render( <TypographyElementPanel element="text" headingLevel="" /> );
+
+ selectAppearance( { fontStyle: 'italic', fontWeight: '700' } );
+
+ // Two separate writes would both read the same stale theme, so the
+ // second would overwrite the first and drop `fontStyle`.
+ expect( emailStyles.updateStyleProp ).not.toHaveBeenCalled();
+ expect( emailStyles.updateStyles ).toHaveBeenCalledTimes( 1 );
+ expect( emailStyles.updateStyles ).toHaveBeenCalledWith( {
+ typography: {
+ fontSize: '16px',
+ fontStyle: 'italic',
+ fontWeight: '700',
+ },
+ color: { text: '#111111' },
+ } );
+ } );
+
+ it( 'stores both Appearance props under the heading level for headings', () => {
+ render(
+ <TypographyElementPanel element="heading" headingLevel="h2" />
+ );
+
+ selectAppearance( { fontStyle: 'italic', fontWeight: '400' } );
+
+ expect( emailStyles.updateStyles ).toHaveBeenCalledTimes( 1 );
+ expect( emailStyles.updateStyles ).toHaveBeenCalledWith( {
+ elements: {
+ h2: {
+ typography: { fontStyle: 'italic', fontWeight: '400' },
+ },
+ },
+ } );
+ } );
+
+ it( 'clears both Appearance props in a single update when deselected', () => {
+ emailStyles.userStyles = {
+ typography: {
+ fontSize: '16px',
+ fontStyle: 'italic',
+ fontWeight: '700',
+ },
+ color: { text: '#111111' },
+ };
+
+ render( <TypographyElementPanel element="text" headingLevel="" /> );
+
+ // What the Appearance tool's onDeselect hands back.
+ selectAppearance( { fontStyle: undefined, fontWeight: undefined } );
+
+ expect( emailStyles.updateStyleProp ).not.toHaveBeenCalled();
+ expect( emailStyles.updateStyles ).toHaveBeenCalledTimes( 1 );
+ // toStrictEqual, so a missing key fails rather than matching undefined.
+ expect( emailStyles.updateStyles.mock.calls[ 0 ][ 0 ] ).toStrictEqual( {
+ typography: {
+ fontSize: '16px',
+ fontStyle: undefined,
+ fontWeight: undefined,
+ },
+ color: { text: '#111111' },
+ } );
+ } );
+
+ it( 'stores both Appearance props under the element for other elements', () => {
+ render( <TypographyElementPanel element="link" headingLevel="" /> );
+
+ selectAppearance( { fontStyle: 'italic', fontWeight: '700' } );
+
+ expect( emailStyles.updateStyles ).toHaveBeenCalledTimes( 1 );
+ expect( emailStyles.updateStyles ).toHaveBeenCalledWith( {
+ elements: {
+ link: {
+ typography: { fontStyle: 'italic', fontWeight: '700' },
+ },
+ },
+ } );
+ } );
} );
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 e3d92d55734..da05489cc93 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
@@ -139,20 +139,26 @@ export function TypographyElementPanel( {
const showToolFontSize =
element !== 'heading' || headingLevel !== 'heading';
- const updateElementStyleProp = useCallback(
- ( path, newValue ) => {
+ // Text styles live at the root of the styles object, every other element
+ // under `elements`, and headings under their level.
+ const getElementStylePath = useCallback(
+ ( path ) => {
if ( element === 'heading' ) {
- updateStyleProp(
- [ 'elements', headingLevel, ...path ],
- newValue
- );
- } else if ( element === 'text' ) {
- updateStyleProp( [ ...path ], newValue );
- } else {
- updateStyleProp( [ 'elements', element, ...path ], newValue );
+ return [ 'elements', headingLevel, ...path ];
+ }
+ if ( element === 'text' ) {
+ return [ ...path ];
}
+ return [ 'elements', element, ...path ];
},
- [ element, updateStyleProp, headingLevel ]
+ [ element, headingLevel ]
+ );
+
+ const updateElementStyleProp = useCallback(
+ ( path, newValue ) => {
+ updateStyleProp( getElementStylePath( path ), newValue );
+ },
+ [ getElementStylePath, updateStyleProp ]
);
const setTextColor = ( newValue ) => {
@@ -256,8 +262,20 @@ export function TypographyElementPanel( {
fontStyle: newFontStyle,
fontWeight: newFontWeight,
} ) => {
- updateElementStyleProp( [ 'typography', 'fontStyle' ], newFontStyle );
- updateElementStyleProp( [ 'typography', 'fontWeight' ], newFontWeight );
+ // Single update — a second updateStyleProp call in the same handler
+ // would work from a stale userTheme and clobber this edit, dropping
+ // fontStyle.
+ updateStyles(
+ setImmutably(
+ setImmutably(
+ userStyles ?? {},
+ getElementStylePath( [ 'typography', 'fontStyle' ] ),
+ newFontStyle
+ ),
+ getElementStylePath( [ 'typography', 'fontWeight' ] ),
+ newFontWeight
+ )
+ );
debouncedRecordEvent(
'styles_sidebar_screen_typography_element_panel_set_font_appearance',
{