Commit 44dcd04eb0c for woocommerce
commit 44dcd04eb0c503077b5e1b32d293b64070cc238f
Author: Seun Olorunsola <30554163+triple0t@users.noreply.github.com>
Date: Fri Oct 23 17:37:51 2026 +0100
[Email Editor] Only offer font weights the email font can show (#70047)
* [Email Editor] Only offer font weights the email font can show
The Appearance control offered every weight and style for email fonts because
none of them declare font faces. Pass font faces to the control: the font's own
list when it has one, otherwise regular and bold. Keep a saved weight in the
list so the control still shows it.
* [Email Editor] Read each font's weights from the email theme.json
The email does not load web fonts, so the reader sees the first font in the stack that their device has. Most email fonts only show regular and bold, but stacks that fall back to Helvetica Neue also show light and medium on Apple devices.
List the weights each font can show in theme.json under settings.custom.fontWeights and build the Appearance options from them, each with italic. A font with neither font faces nor a weight list keeps core's full list.
* [Email Editor] Store each font's weights as a string
Values in theme.json custom settings must be strings, numbers or objects, so the weight lists are now comma-separated strings and the editor splits them.
* [Email Editor] Use the inherited font for the Appearance options
A heading level, link or button with no font of its own uses the heading or email font, so look up the weights for the font the element renders with.
* [Email Editor] Reset the style mocks before each panel test
* [Email Editor] Prefer theme and custom fonts over default ones with the same slug
* [Email Editor] Read the font slug without a regular expression
* [Email Editor] Keep a saved weight outside a variable font's range
* [Email Editor] Show a saved weight or style that has no saved partner
* [Email Editor] Treat inherited button font values as not set
Core's theme.json gives buttons inherit for font family, style and weight. The Appearance list showed inherit as an option, and a button on the Default font did not follow the Text font.
diff --git a/packages/js/email-editor/changelog/fix-email-editor-appearance-font-faces b/packages/js/email-editor/changelog/fix-email-editor-appearance-font-faces
new file mode 100644
index 00000000000..21e0d433f22
--- /dev/null
+++ b/packages/js/email-editor/changelog/fix-email-editor-appearance-font-faces
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Only offer font weights and styles in the email editor Appearance list that the selected font can show.
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 502d2d5047a..69b31fd67b8 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
@@ -3,12 +3,17 @@
*/
import { act, render } from '@testing-library/react';
import { FontSizePicker } from '@wordpress/components';
-import { useSettings } from '@wordpress/block-editor';
+import {
+ useSettings,
+ __experimentalFontAppearanceControl as FontAppearanceControl,
+} from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import { TypographyElementPanel } from '../typography-element-panel';
+import { useEmailStyles } from '../../../../hooks';
+import { getElementStyles } from '../../utils';
// Holds the `onChange` the panel passes to FontAppearanceControl, so tests can
// fire an Appearance selection the way the dropdown does.
@@ -32,10 +37,10 @@ jest.mock( '@wordpress/components', () => ( {
jest.mock( '@wordpress/block-editor', () => ( {
useSettings: jest.fn(),
- __experimentalFontAppearanceControl: ( props ) => {
+ __experimentalFontAppearanceControl: jest.fn( ( props ) => {
fontAppearance.onChange = props.onChange;
return null;
- },
+ } ),
__experimentalLetterSpacingControl: () => null,
__experimentalFontFamilyControl: () => null,
LineHeightControl: () => null,
@@ -68,11 +73,18 @@ jest.mock( '../../../../hooks/use-user-theme', () => ( {
jest.mock( '../../../../hooks', () => ( {
setImmutably: jest.requireActual( '../../../../hooks/use-email-styles' )
.setImmutably,
- useEmailStyles: () => emailStyles,
+ useEmailStyles: jest.fn( () => emailStyles ),
} ) );
jest.mock( '../../utils', () => ( {
- getElementStyles: () => ( { typography: { fontSize: '16px' }, color: {} } ),
+ getElementStyles: jest.fn( () => ( {
+ typography: { fontSize: '16px', fontFamily: 'Inter, sans-serif' },
+ color: {},
+ } ) ),
+ getFontFamilyFaces: jest.requireActual( '../../utils' ).getFontFamilyFaces,
+ getFontAppearanceValue:
+ jest.requireActual( '../../utils' ).getFontAppearanceValue,
+ withoutInherit: jest.requireActual( '../../utils' ).withoutInherit,
} ) );
jest.mock( '../../hooks', () => ( {
@@ -90,6 +102,28 @@ jest.mock( '../../../../events', () => ( {
const useSettingsMock = useSettings as jest.Mock;
const fontSizePickerMock = FontSizePicker as unknown as jest.Mock;
+const fontAppearanceControlMock = FontAppearanceControl as unknown as jest.Mock;
+const useEmailStylesMock = useEmailStyles as jest.Mock;
+const getElementStylesMock = getElementStyles as jest.Mock;
+const defaultGetElementStyles = getElementStylesMock.getMockImplementation();
+const defaultUseEmailStyles = useEmailStylesMock.getMockImplementation();
+
+const mockFontSettings = () =>
+ useSettingsMock.mockImplementation( ( ...paths: string[] ) => {
+ if ( paths[ 0 ] === 'spacing.units' ) {
+ return [ [ 'px' ] ];
+ }
+ if ( paths[ 0 ] === 'custom.fontWeights' ) {
+ return [ { inter: '300, 700' } ];
+ }
+ return [
+ [],
+ {
+ default: [],
+ theme: [ { slug: 'inter', fontFamily: 'Inter, sans-serif' } ],
+ },
+ ];
+ } );
const selectAppearance = ( value: {
fontStyle?: string;
@@ -102,6 +136,9 @@ const selectAppearance = ( value: {
describe( 'TypographyElementPanel', () => {
beforeEach( () => {
fontSizePickerMock.mockClear();
+ fontAppearanceControlMock.mockClear();
+ getElementStylesMock.mockImplementation( defaultGetElementStyles );
+ useEmailStylesMock.mockImplementation( defaultUseEmailStyles );
emailStyles.updateStyleProp.mockClear();
emailStyles.updateStyles.mockClear();
emailStyles.userStyles = {};
@@ -205,4 +242,117 @@ describe( 'TypographyElementPanel', () => {
},
} );
} );
+
+ it( 'passes the weights the theme lists for the font to the appearance control', () => {
+ mockFontSettings();
+
+ render( <TypographyElementPanel element="text" headingLevel="" /> );
+
+ expect(
+ fontAppearanceControlMock.mock.calls[ 0 ][ 0 ].fontFamilyFaces
+ ).toEqual( [
+ { fontStyle: 'normal', fontWeight: '300' },
+ { fontStyle: 'italic', fontWeight: '300' },
+ { fontStyle: 'normal', fontWeight: '700' },
+ { fontStyle: 'italic', fontWeight: '700' },
+ ] );
+ } );
+
+ it( 'uses the inherited font when the element has no font of its own', () => {
+ mockFontSettings();
+ getElementStylesMock.mockImplementation(
+ jest.requireActual( '../../utils' ).getElementStyles
+ );
+ useEmailStylesMock.mockReturnValue( {
+ ...emailStyles,
+ styles: {
+ typography: { fontFamily: 'Inter, sans-serif' },
+ elements: { h2: { typography: { fontSize: '28px' } } },
+ },
+ } );
+
+ render(
+ <TypographyElementPanel element="heading" headingLevel="h2" />
+ );
+
+ expect(
+ fontAppearanceControlMock.mock.calls[ 0 ][ 0 ].fontFamilyFaces
+ ).toHaveLength( 4 );
+ } );
+
+ it( 'treats an inherited font and appearance as not set', () => {
+ mockFontSettings();
+ getElementStylesMock.mockImplementation(
+ jest.requireActual( '../../utils' ).getElementStyles
+ );
+ useEmailStylesMock.mockReturnValue( {
+ ...emailStyles,
+ styles: {
+ typography: { fontFamily: 'Inter, sans-serif' },
+ elements: {
+ button: {
+ typography: {
+ fontFamily: 'inherit',
+ fontStyle: 'inherit',
+ fontWeight: 'inherit',
+ },
+ },
+ },
+ },
+ } );
+
+ render( <TypographyElementPanel element="button" headingLevel="" /> );
+
+ const props = fontAppearanceControlMock.mock.calls[ 0 ][ 0 ];
+ expect( props.fontFamilyFaces ).toHaveLength( 4 );
+ expect( props.value ).toEqual( {
+ fontStyle: undefined,
+ fontWeight: undefined,
+ } );
+ } );
+
+ it( 'prefers a theme font over a default font with the same slug', () => {
+ const fontFace = [
+ { fontFamily: 'Inter', fontStyle: 'normal', fontWeight: '600' },
+ ];
+ useSettingsMock.mockImplementation( ( ...paths: string[] ) => {
+ if ( paths[ 0 ] === 'custom.fontWeights' ) {
+ return [ { inter: '300, 700' } ];
+ }
+ return [
+ [],
+ {
+ default: [
+ { slug: 'inter', fontFamily: 'Inter, sans-serif' },
+ ],
+ theme: [
+ {
+ slug: 'inter',
+ fontFamily: 'Inter, sans-serif',
+ fontFace,
+ },
+ ],
+ },
+ ];
+ } );
+
+ render( <TypographyElementPanel element="text" headingLevel="" /> );
+
+ expect(
+ fontAppearanceControlMock.mock.calls[ 0 ][ 0 ].fontFamilyFaces
+ ).toEqual( fontFace );
+ } );
+
+ it( 'shows a saved weight that has no saved style', () => {
+ getElementStylesMock.mockReturnValue( {
+ typography: { fontWeight: 700 },
+ color: {},
+ } );
+
+ render( <TypographyElementPanel element="text" headingLevel="" /> );
+
+ expect( fontAppearanceControlMock.mock.calls[ 0 ][ 0 ].value ).toEqual(
+ { fontStyle: 'normal', 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 da05489cc93..5875deff79d 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
@@ -39,7 +39,12 @@ import {
*/
import { useEmailStyles, setImmutably } from '../../../hooks';
import { storeName } from '../../../store';
-import { getElementStyles } from '../utils';
+import {
+ getElementStyles,
+ getFontAppearanceValue,
+ getFontFamilyFaces,
+ withoutInherit,
+} from '../utils';
import { useHasTextColorInTypographyPanel } from '../hooks';
import { ColorDropdownItem } from './color-dropdown-item';
import { recordEvent, debouncedRecordEvent } from '../../../events';
@@ -71,9 +76,15 @@ export function TypographyElementPanel( {
'typography.fontFamilies'
);
const [ availableUnits ] = useSettings( 'spacing.units' ) as [ string[] ];
+ const [ fontWeights ] = useSettings( 'custom.fontWeights' ) as [
+ Record< string, string > | undefined,
+ ];
// Ref: https://github.com/WordPress/gutenberg/issues/59778
const fontFamilies = blockLevelFontFamilies?.default || [];
+ const allFontFamilies = [ 'custom', 'theme', 'default' ].flatMap(
+ ( origin ) => blockLevelFontFamilies?.[ origin ] ?? []
+ );
const theme = useSelect( ( select ) => select( storeName ).getTheme(), [] );
const colorGradientSettings = useMultipleOriginColorsAndGradients();
// Text color renders here only when the running WordPress no longer offers
@@ -88,6 +99,11 @@ export function TypographyElementPanel( {
element,
headingLevel
);
+ const renderedFontFamily =
+ withoutInherit(
+ getElementStyles( styles, element, headingLevel, true ).typography
+ .fontFamily
+ ) ?? styles?.typography?.fontFamily;
const {
fontFamily,
fontSize,
@@ -379,11 +395,17 @@ export function TypographyElementPanel( {
isShownByDefault={ defaultControls.fontAppearance }
>
<FontAppearanceControl
- value={ {
+ value={ getFontAppearanceValue( {
fontStyle,
fontWeight,
- } }
+ } ) }
onChange={ setFontAppearance }
+ fontFamilyFaces={ getFontFamilyFaces(
+ allFontFamilies,
+ fontWeights,
+ renderedFontFamily,
+ { fontStyle, fontWeight }
+ ) }
hasFontStyles
hasFontWeights
size="__unstable-large"
diff --git a/packages/js/email-editor/src/components/styles-sidebar/test/utils.spec.ts b/packages/js/email-editor/src/components/styles-sidebar/test/utils.spec.ts
index 140d8a3cfad..0aeb117cad7 100644
--- a/packages/js/email-editor/src/components/styles-sidebar/test/utils.spec.ts
+++ b/packages/js/email-editor/src/components/styles-sidebar/test/utils.spec.ts
@@ -2,7 +2,12 @@
* Internal dependencies
*/
import { EmailStyles } from '../../../store';
-import { getElementStyles, getHeadingElementStyles } from '../utils';
+import {
+ getElementStyles,
+ getFontAppearanceValue,
+ getFontFamilyFaces,
+ getHeadingElementStyles,
+} from '../utils';
describe( 'getElementStyles', () => {
it( 'always returns typography and color objects, even when missing', () => {
@@ -78,3 +83,176 @@ describe( 'getHeadingElementStyles', () => {
} );
} );
} );
+
+describe( 'getFontAppearanceValue', () => {
+ it.each( [
+ [ {}, { fontStyle: undefined, fontWeight: undefined } ],
+ [ { fontWeight: 700 }, { fontStyle: 'normal', fontWeight: '700' } ],
+ [ { fontStyle: 'italic' }, { fontStyle: 'italic', fontWeight: '400' } ],
+ [
+ { fontStyle: 'italic', fontWeight: '300' },
+ { fontStyle: 'italic', fontWeight: '300' },
+ ],
+ [
+ { fontStyle: 'inherit', fontWeight: 'inherit' },
+ { fontStyle: undefined, fontWeight: undefined },
+ ],
+ [
+ { fontStyle: 'inherit', fontWeight: '700' },
+ { fontStyle: 'normal', fontWeight: '700' },
+ ],
+ ] )( 'turns %j into %j', ( current, expected ) => {
+ expect( getFontAppearanceValue( current ) ).toEqual( expected );
+ } );
+} );
+
+describe( 'getFontFamilyFaces', () => {
+ const arial = {
+ name: 'Arial',
+ slug: 'arial',
+ fontFamily: "Arial, 'Helvetica Neue', Helvetica, sans-serif",
+ };
+ const openSans = {
+ name: 'Open Sans',
+ slug: 'open-sans',
+ fontFamily:
+ "'open sans', 'helvetica neue', helvetica, arial, sans-serif",
+ };
+ const inter = {
+ name: 'Inter',
+ slug: 'inter',
+ fontFamily: 'Inter, sans-serif',
+ fontFace: [
+ { fontFamily: 'Inter', fontStyle: 'normal', fontWeight: '300' },
+ { fontFamily: 'Inter', fontStyle: 'normal', fontWeight: '600' },
+ ],
+ };
+ const fontFamilies = [ arial, openSans, inter ];
+ const fontWeights = {
+ arial: '400, 700',
+ 'open-sans': '300,400, 500 ,700',
+ inter: '400, 700',
+ };
+ const regularAndBold = [
+ { fontStyle: 'normal', fontWeight: '400' },
+ { fontStyle: 'italic', fontWeight: '400' },
+ { fontStyle: 'normal', fontWeight: '700' },
+ { fontStyle: 'italic', fontWeight: '700' },
+ ];
+
+ it( 'offers the weights the theme lists for the font, each with italic', () => {
+ expect(
+ getFontFamilyFaces(
+ fontFamilies,
+ fontWeights,
+ arial.fontFamily,
+ {}
+ )
+ ).toEqual( regularAndBold );
+ expect(
+ getFontFamilyFaces(
+ fontFamilies,
+ fontWeights,
+ openSans.fontFamily,
+ {}
+ )?.map( ( face ) => face.fontWeight )
+ ).toEqual( [ '300', '300', '400', '400', '500', '500', '700', '700' ] );
+ } );
+
+ it( 'prefers the font faces of a font that has them', () => {
+ expect(
+ getFontFamilyFaces(
+ fontFamilies,
+ fontWeights,
+ inter.fontFamily,
+ {}
+ )
+ ).toEqual( inter.fontFace );
+ } );
+
+ it.each( [
+ 'var:preset|font-family|open-sans',
+ 'var(--wp--preset--font-family--open-sans)',
+ ] )( 'finds the font from a preset reference: %s', ( fontFamily ) => {
+ expect(
+ getFontFamilyFaces( fontFamilies, fontWeights, fontFamily, {} )
+ ).toHaveLength( 8 );
+ } );
+
+ it( 'matches the font value before reading a preset slug from it', () => {
+ const custom = {
+ slug: 'custom',
+ fontFamily: 'my-font-family--arial',
+ };
+ expect(
+ getFontFamilyFaces(
+ [ ...fontFamilies, custom ],
+ { ...fontWeights, custom: '300' },
+ custom.fontFamily,
+ {}
+ )
+ ).toEqual( [
+ { fontStyle: 'normal', fontWeight: '300' },
+ { fontStyle: 'italic', fontWeight: '300' },
+ ] );
+ } );
+
+ it.each( [
+ [ 'the font is not in the list', undefined, fontWeights ],
+ [ 'the theme lists no weights for the font', arial.fontFamily, {} ],
+ [ 'the theme lists no weights at all', arial.fontFamily, undefined ],
+ ] )( 'leaves the list to core when %s', ( _, fontFamily, weights ) => {
+ expect(
+ getFontFamilyFaces( fontFamilies, weights, fontFamily, {
+ fontWeight: '300',
+ } )
+ ).toBeUndefined();
+ } );
+
+ it( 'keeps a saved weight that the font does not offer', () => {
+ expect(
+ getFontFamilyFaces( fontFamilies, fontWeights, arial.fontFamily, {
+ fontStyle: 'normal',
+ fontWeight: '300',
+ } )
+ ).toEqual( [
+ { fontStyle: 'normal', fontWeight: '300' },
+ ...regularAndBold,
+ ] );
+ } );
+
+ it( 'puts a saved weight before a variable weight range', () => {
+ const variable = {
+ slug: 'variable',
+ fontFamily: 'Variable, sans-serif',
+ fontFace: [ { fontStyle: 'normal', fontWeight: '300 700' } ],
+ };
+ expect(
+ getFontFamilyFaces( [ variable ], {}, variable.fontFamily, {
+ fontStyle: 'normal',
+ fontWeight: '900',
+ } )
+ ).toEqual( [
+ { fontStyle: 'normal', fontWeight: '900' },
+ ...variable.fontFace,
+ ] );
+ } );
+
+ it( 'does not add an inherited style and weight', () => {
+ expect(
+ getFontFamilyFaces( fontFamilies, fontWeights, arial.fontFamily, {
+ fontStyle: 'inherit',
+ fontWeight: 'inherit',
+ } )
+ ).toEqual( regularAndBold );
+ } );
+
+ it( 'does not add a saved value the font already offers', () => {
+ expect(
+ getFontFamilyFaces( fontFamilies, fontWeights, arial.fontFamily, {
+ fontStyle: 'italic',
+ fontWeight: 700,
+ } )
+ ).toEqual( regularAndBold );
+ } );
+} );
diff --git a/packages/js/email-editor/src/components/styles-sidebar/utils.ts b/packages/js/email-editor/src/components/styles-sidebar/utils.ts
index e67b4334435..1fe88160c25 100644
--- a/packages/js/email-editor/src/components/styles-sidebar/utils.ts
+++ b/packages/js/email-editor/src/components/styles-sidebar/utils.ts
@@ -75,3 +75,124 @@ export const getElementStyles = (
color: elementStyles.color ?? {},
} as EmailStyles;
};
+
+type FontFace = {
+ fontStyle?: string;
+ fontWeight?: string | number;
+};
+
+type FontFamily = {
+ slug?: string;
+ fontFamily?: string;
+ fontFace?: FontFace[];
+};
+
+const PRESET_PREFIX = 'var:preset|font-family|';
+const CSS_VAR_PREFIX = 'var(--wp--preset--font-family--';
+
+/**
+ * Gets the font family slug from a preset reference.
+ *
+ * @param fontFamily The font family value.
+ * @return The slug, or undefined when the value is not a preset reference.
+ */
+const getFontFamilySlug = ( fontFamily: string | undefined ) => {
+ if ( fontFamily?.startsWith( PRESET_PREFIX ) ) {
+ return fontFamily.slice( PRESET_PREFIX.length );
+ }
+ if (
+ fontFamily?.startsWith( CSS_VAR_PREFIX ) &&
+ fontFamily.endsWith( ')' )
+ ) {
+ return fontFamily.slice( CSS_VAR_PREFIX.length, -1 );
+ }
+ return undefined;
+};
+
+/**
+ * Treats a CSS `inherit` value as not set.
+ *
+ * Core's theme.json gives buttons `inherit` for their font family, style and weight.
+ *
+ * @param value The style value.
+ * @return The value, or undefined when it is `inherit`.
+ */
+export const withoutInherit = < T >( value: T ): T | undefined =>
+ value === 'inherit' ? undefined : value;
+
+/**
+ * Fills in the missing half of a saved font style and weight.
+ *
+ * The Appearance control only shows an option when both values match it exactly.
+ *
+ * @param current The saved font style and weight.
+ * @param current.fontStyle The saved font style.
+ * @param current.fontWeight The saved font weight.
+ * @return The style and weight to show, or both undefined when nothing is saved.
+ */
+export const getFontAppearanceValue = ( current: FontFace ) => {
+ const fontStyle = withoutInherit( current.fontStyle );
+ const fontWeight = withoutInherit( current.fontWeight );
+ if ( ! fontStyle && ! fontWeight ) {
+ return { fontStyle: undefined, fontWeight: undefined };
+ }
+ return {
+ fontStyle: fontStyle ?? 'normal',
+ fontWeight: String( fontWeight ?? '400' ),
+ };
+};
+
+/**
+ * Gets the font faces the Appearance control should offer for a font.
+ *
+ * Email fonts have no font faces, so core would offer every weight. The email theme lists the
+ * weights each font can show, and a saved style or weight is kept so the control still shows it.
+ *
+ * @param fontFamilies The font families from the theme settings.
+ * @param fontWeights The comma-separated weights each font family can show, keyed by slug.
+ * @param fontFamily The selected font family value or preset reference.
+ * @param current The saved font style and weight.
+ * @return The font faces, or undefined to let core offer every weight.
+ */
+export const getFontFamilyFaces = (
+ fontFamilies: FontFamily[],
+ fontWeights: Record< string, string > | undefined,
+ fontFamily: string | undefined,
+ current: FontFace
+): FontFace[] | undefined => {
+ const slug = getFontFamilySlug( fontFamily );
+ const family =
+ fontFamilies.find( ( item ) => item.fontFamily === fontFamily ) ??
+ ( slug
+ ? fontFamilies.find( ( item ) => item.slug === slug )
+ : undefined );
+ const weights = ( family?.slug ? fontWeights?.[ family.slug ] : '' )
+ ?.split( ',' )
+ .map( ( weight ) => weight.trim() )
+ .filter( Boolean );
+ let faces: FontFace[];
+ if ( family?.fontFace?.length ) {
+ faces = family.fontFace;
+ } else if ( weights?.length ) {
+ faces = weights.flatMap( ( fontWeight ) => [
+ { fontStyle: 'normal', fontWeight },
+ { fontStyle: 'italic', fontWeight },
+ ] );
+ } else {
+ return undefined;
+ }
+
+ const { fontStyle, fontWeight } = getFontAppearanceValue( current );
+ if ( ! fontStyle || ! fontWeight ) {
+ return faces;
+ }
+
+ const isOffered = faces.some(
+ ( face ) =>
+ ( face.fontStyle ?? 'normal' ) === fontStyle &&
+ String( face.fontWeight ) === fontWeight
+ );
+
+ // First, because core skips single weights listed after a variable weight range.
+ return isOffered ? faces : [ { fontStyle, fontWeight }, ...faces ];
+};
diff --git a/packages/php/email-editor/changelog/fix-email-editor-appearance-font-faces b/packages/php/email-editor/changelog/fix-email-editor-appearance-font-faces
new file mode 100644
index 00000000000..cc4e4c1aa84
--- /dev/null
+++ b/packages/php/email-editor/changelog/fix-email-editor-appearance-font-faces
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+List the font weights each email font can show in the email theme.json, under `settings.custom.fontWeights`.
diff --git a/packages/php/email-editor/src/Engine/theme.json b/packages/php/email-editor/src/Engine/theme.json
index bd4e1135ee6..88c56c7da41 100644
--- a/packages/php/email-editor/src/Engine/theme.json
+++ b/packages/php/email-editor/src/Engine/theme.json
@@ -219,6 +219,34 @@
}
]
},
+ "custom": {
+ "fontWeights": {
+ "arial": "400, 700",
+ "comic-sans-ms": "400, 700",
+ "courier-new": "400, 700",
+ "georgia": "400, 700",
+ "lucida": "400, 700",
+ "tahoma": "400, 700",
+ "times-new-roman": "400, 700",
+ "trebuchet-ms": "400, 700",
+ "verdana": "400, 700",
+ "arvo": "400, 700",
+ "lato": "300, 400, 500, 700",
+ "lora": "400, 700",
+ "merriweather": "400, 700",
+ "merriweather-sans": "300, 400, 500, 700",
+ "noticia-text": "400, 700",
+ "open-sans": "300, 400, 500, 700",
+ "playfair-display": "400, 700",
+ "roboto": "300, 400, 500, 700",
+ "source-sans-pro": "300, 400, 500, 700",
+ "oswald": "400, 700",
+ "raleway": "400, 700",
+ "permanent-marker": "400, 700",
+ "pacifico": "400, 700",
+ "inter": "300, 400, 500, 700"
+ }
+ },
"useRootPaddingAwareAlignments": true
},
"styles": {
diff --git a/packages/php/email-editor/src/Engine/theme.md b/packages/php/email-editor/src/Engine/theme.md
index 7d289746698..98b01f2248c 100644
--- a/packages/php/email-editor/src/Engine/theme.md
+++ b/packages/php/email-editor/src/Engine/theme.md
@@ -12,6 +12,7 @@ In this file we want to document settings and styles that are specific to the em
- **spacing**: We allow only px units, because they are the most reliable in email clients. We may add the support for other units later with some sort of conversion to px. We also disable margins because they are not supported in our renderer (margin collapsing might be tricky).
- **border**: We want to allow all types of borders and border styles.
- **typography**: We enable fontWeight, fontStyle, and letterSpacing because they are supported by our renderer. We disable the dropCap appearance setting because it is not supported. We also define a set of basic font families that are safe to use with emails. The list was copied from the battle-tested legacy editor.
+- **custom.fontWeights**: The font weights the editor offers for each font family, keyed by the font family slug, as a comma-separated string (values in `custom` must be strings, numbers or objects). The email does not load web fonts, so the reader sees the first font in the stack that their device has. Most of those fonts only come in regular and bold. Stacks that fall back to Helvetica Neue also have light and medium on Apple devices. Italic is offered for every weight. A font family with its own `fontFace` list uses that list instead. A font family with neither shows every weight.
## Styles
diff --git a/packages/php/email-editor/tests/integration/Engine/Theme_Controller_Test.php b/packages/php/email-editor/tests/integration/Engine/Theme_Controller_Test.php
index 8c38c543796..e5884806d4d 100644
--- a/packages/php/email-editor/tests/integration/Engine/Theme_Controller_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Theme_Controller_Test.php
@@ -255,6 +255,20 @@ class Theme_Controller_Test extends \Email_Editor_Integration_Test_Case {
$this->assertNotEmpty( $settings['color']['palette']['theme'] );
}
+ /**
+ * Test that every email font family has a list of font weights
+ */
+ public function testItProvidesFontWeightsForEveryFontFamily(): void {
+ $settings = $this->theme_controller->get_settings();
+ $font_weights = $settings['custom']['fontWeights'] ?? array();
+
+ foreach ( $settings['typography']['fontFamilies']['default'] as $font_family ) {
+ $this->assertNotEmpty( $font_weights[ $font_family['slug'] ] ?? array(), $font_family['slug'] );
+ }
+ $this->assertSame( '400, 700', $font_weights['arial'] );
+ $this->assertSame( '300, 400, 500, 700', $font_weights['open-sans'] );
+ }
+
/**
* Test if the theme controller returns correct preset variables map
*/