From d80d801ef628d22ec0a37df90a684cbef6908284 Mon Sep 17 00:00:00 2001 From: Sarah Norris <1645628+mikachan@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:33:52 +0100 Subject: [PATCH] Font Appearance Control: Refactor font appearance fallbacks (#63215) * Refactor font appearance fallbacks * Create new findNearestStyleAndWeight function * Add tests for findNearestStyleAndWeight * Refactor findNearestStyleAndWeight * Add a test for normal/100 * Add comments * Tidy up comments * Fix test description * Update test descriptions * Update deps and wrap setFontAppearance and resetFontAppearance in useCallback * Add e2e test for appearance control dropdown menu * Add periods to end of comments * Use better test data for font appearance e2e default test * Add findNearestFontStyle function with tests * Limit the dependency array to just fontFamily * Add normal font style test * Add invalid font style test * Try toHaveText instead of toContainText * Try using TT4 for e2e test rather than TT3 * Use combobox role rather than button in e2e test * Set nearestFontStyle to an empty string by default * Refactor findNearestFontStyle * Do not activate a theme in font appearance e2e test * Run findNearestStyleAndWeight only when fontFamily has changed * Only trigger appearance onChange if values are different Co-authored-by: mikachan Co-authored-by: talldan Co-authored-by: creativecoder Co-authored-by: noisysocks Co-authored-by: t-hamano Co-authored-by: Mamaduka --- .../global-styles/test/typography-utils.js | 325 ++++++++++++++++++ .../global-styles/typography-panel.js | 80 ++--- .../global-styles/typography-utils.js | 110 +++++- .../various/font-appearance-control.spec.js | 90 +++++ 4 files changed, 554 insertions(+), 51 deletions(-) create mode 100644 test/e2e/specs/editor/various/font-appearance-control.spec.js diff --git a/packages/block-editor/src/components/global-styles/test/typography-utils.js b/packages/block-editor/src/components/global-styles/test/typography-utils.js index 115de8cdf2563d..41a7d6b5e37b8b 100644 --- a/packages/block-editor/src/components/global-styles/test/typography-utils.js +++ b/packages/block-editor/src/components/global-styles/test/typography-utils.js @@ -6,6 +6,8 @@ import { getFluidTypographyOptionsFromSettings, getMergedFontFamiliesAndFontFamilyFaces, findNearestFontWeight, + findNearestFontStyle, + findNearestStyleAndWeight, } from '../typography-utils'; describe( 'typography utils', () => { @@ -951,6 +953,329 @@ describe( 'typography utils', () => { ); } ); + describe( 'findNearestFontStyle', () => { + [ + { + message: + 'should return empty string when newFontStyleValue is `undefined`', + availableFontStyles: undefined, + newFontStyleValue: undefined, + expected: '', + }, + { + message: + 'should return newFontStyleValue value when availableFontStyles is empty', + availableFontStyles: [], + newFontStyleValue: 'italic', + expected: 'italic', + }, + { + message: + 'should return empty string if there is no new font style available', + availableFontStyles: [ { name: 'Normal', value: 'normal' } ], + newFontStyleValue: 'italic', + expected: '', + }, + { + message: + 'should return empty string if the new font style is invalid', + availableFontStyles: [ + { name: 'Regular', value: 'normal' }, + { name: 'Italic', value: 'italic' }, + ], + newFontStyleValue: 'not-valid', + expected: '', + }, + { + message: 'should return italic if oblique is not available', + availableFontStyles: [ + { name: 'Regular', value: 'normal' }, + { name: 'Italic', value: 'italic' }, + ], + newFontStyleValue: 'oblique', + expected: 'italic', + }, + { + message: 'should return normal if normal is available', + availableFontStyles: [ + { name: 'Regular', value: 'normal' }, + { name: 'Italic', value: 'italic' }, + ], + newFontStyleValue: 'normal', + expected: 'normal', + }, + ].forEach( + ( { + message, + availableFontStyles, + newFontStyleValue, + expected, + } ) => { + it( `${ message }`, () => { + expect( + findNearestFontStyle( + availableFontStyles, + newFontStyleValue + ) + ).toEqual( expected ); + } ); + } + ); + } ); + + describe( 'findNearestStyleAndWeight', () => { + [ + { + message: 'should return empty object when all values are empty', + fontFamilyFaces: [], + fontStyle: undefined, + fontWeight: undefined, + expected: {}, + }, + { + message: + 'should return original fontStyle and fontWeight when fontFamilyFaces is empty', + fontFamilyFaces: [], + fontStyle: 'italic', + fontWeight: '700', + expected: { + nearestFontStyle: 'italic', + nearestFontWeight: '700', + }, + }, + { + message: + 'should return undefined values if both fontStyle and fontWeight are not available', + fontFamilyFaces: [ + { + fontFamily: 'ABeeZee', + fontStyle: 'italic', + fontWeight: '400', + src: [ + 'file:./assets/fonts/esDT31xSG-6AGleN2tCkkJUCGpG-GQ.woff2', + ], + }, + ], + fontStyle: undefined, + fontWeight: undefined, + expected: { + nearestFontStyle: undefined, + nearestFontWeight: undefined, + }, + }, + { + message: + 'should return nearest fontStyle and fontWeight for normal/400', + fontFamilyFaces: [ + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'normal', + fontWeight: '400', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Regular.woff2', + ], + }, + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'italic', + fontWeight: '400', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Italic.woff2', + ], + }, + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'normal', + fontWeight: '700', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Bold.woff2', + ], + }, + ], + fontStyle: 'normal', + fontWeight: '400', + expected: { + nearestFontStyle: 'normal', + nearestFontWeight: '400', + }, + }, + { + message: + 'should return nearest fontStyle and fontWeight for normal/100', + fontFamilyFaces: [ + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'normal', + fontWeight: '400', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Regular.woff2', + ], + }, + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'italic', + fontWeight: '400', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Italic.woff2', + ], + }, + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'normal', + fontWeight: '700', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Bold.woff2', + ], + }, + ], + fontStyle: 'normal', + fontWeight: '100', + expected: { + nearestFontStyle: 'normal', + nearestFontWeight: '400', + }, + }, + { + message: + 'should return nearest fontStyle and fontWeight for italic/900', + fontFamilyFaces: [ + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'normal', + fontWeight: '400', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Regular.woff2', + ], + }, + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'italic', + fontWeight: '400', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Italic.woff2', + ], + }, + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'normal', + fontWeight: '700', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Bold.woff2', + ], + }, + ], + fontStyle: 'italic', + fontWeight: '900', + expected: { + nearestFontStyle: 'italic', + nearestFontWeight: '700', + }, + }, + { + message: + 'should return nearest fontStyle and fontWeight for oblique/600', + fontFamilyFaces: [ + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'normal', + fontWeight: '400', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Regular.woff2', + ], + }, + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'italic', + fontWeight: '700', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Bold.woff2', + ], + }, + ], + fontStyle: 'oblique', + fontWeight: '600', + expected: { + nearestFontStyle: 'italic', + nearestFontWeight: '700', + }, + }, + { + message: + 'should return nearest fontStyle and fontWeight for 300 font weight and empty font style', + fontFamilyFaces: [ + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'normal', + fontWeight: '400', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Regular.woff2', + ], + }, + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'italic', + fontWeight: '700', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Bold.woff2', + ], + }, + ], + fontStyle: undefined, + fontWeight: '300', + expected: { + nearestFontStyle: 'normal', + nearestFontWeight: '400', + }, + }, + { + message: + 'should return nearest fontStyle and fontWeight for oblique font style and empty font weight', + fontFamilyFaces: [ + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'normal', + fontWeight: '400', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Regular.woff2', + ], + }, + { + fontFamily: 'IBM Plex Mono', + fontStyle: 'italic', + fontWeight: '700', + src: [ + 'file:./assets/fonts/ibm-plex-mono/IBMPlexMono-Bold.woff2', + ], + }, + ], + fontStyle: 'oblique', + fontWeight: undefined, + expected: { + nearestFontStyle: 'italic', + nearestFontWeight: '400', + }, + }, + ].forEach( + ( { + message, + fontFamilyFaces, + fontStyle, + fontWeight, + expected, + } ) => { + it( `${ message }`, () => { + expect( + findNearestStyleAndWeight( + fontFamilyFaces, + fontStyle, + fontWeight + ) + ).toEqual( expected ); + } ); + } + ); + } ); + describe( 'typography utils', () => { [ { diff --git a/packages/block-editor/src/components/global-styles/typography-panel.js b/packages/block-editor/src/components/global-styles/typography-panel.js index c497ea46833910..f6a389a5bc96d9 100644 --- a/packages/block-editor/src/components/global-styles/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/typography-panel.js @@ -25,9 +25,8 @@ import { getValueFromVariable, useToolsPanelDropdownMenuProps } from './utils'; import { setImmutably } from '../../utils/object'; import { getMergedFontFamiliesAndFontFamilyFaces, - findNearestFontWeight, + findNearestStyleAndWeight, } from './typography-utils'; -import { getFontStylesAndWeights } from '../../utils/get-font-styles-and-weights'; const MIN_TEXT_COLUMNS = 1; const MAX_TEXT_COLUMNS = 6; @@ -236,57 +235,50 @@ export default function TypographyPanel( { const hasFontWeights = settings?.typography?.fontWeight; const fontStyle = decodeValue( inheritedValue?.typography?.fontStyle ); const fontWeight = decodeValue( inheritedValue?.typography?.fontWeight ); - const setFontAppearance = ( { - fontStyle: newFontStyle, - fontWeight: newFontWeight, - } ) => { - onChange( { - ...value, - typography: { - ...value?.typography, - fontStyle: newFontStyle || undefined, - fontWeight: newFontWeight || undefined, - }, - } ); - }; + const { nearestFontStyle, nearestFontWeight } = findNearestStyleAndWeight( + fontFamilyFaces, + fontStyle, + fontWeight + ); + const setFontAppearance = useCallback( + ( { fontStyle: newFontStyle, fontWeight: newFontWeight } ) => { + // Only update the font style and weight if they have changed. + if ( newFontStyle !== fontStyle || newFontWeight !== fontWeight ) { + onChange( { + ...value, + typography: { + ...value?.typography, + fontStyle: newFontStyle || undefined, + fontWeight: newFontWeight || undefined, + }, + } ); + } + }, + [ fontStyle, fontWeight, onChange, value ] + ); const hasFontAppearance = () => !! value?.typography?.fontStyle || !! value?.typography?.fontWeight; - const resetFontAppearance = () => { + const resetFontAppearance = useCallback( () => { setFontAppearance( {} ); - }; + }, [ setFontAppearance ] ); - // Check if previous font style and weight values are available in the new font family + // Check if previous font style and weight values are available in the new font family. useEffect( () => { - const { fontStyles, fontWeights, isSystemFont } = - getFontStylesAndWeights( fontFamilyFaces ); - const hasFontStyle = fontStyles?.some( - ( { value: fs } ) => fs === fontStyle - ); - const hasFontWeight = fontWeights?.some( - ( { value: fw } ) => fw === fontWeight - ); - - // Try to set nearest available font weight - if ( ! hasFontWeight && fontWeight ) { + if ( nearestFontStyle && nearestFontWeight ) { setFontAppearance( { - fontStyle, - fontWeight: findNearestFontWeight( fontWeights, fontWeight ), + fontStyle: nearestFontStyle, + fontWeight: nearestFontWeight, } ); - } - - // Set the same weight and style values if the font family is a system font or if both are the same - if ( isSystemFont || ( hasFontStyle && hasFontWeight ) ) { - setFontAppearance( { - fontStyle, - fontWeight, - } ); - } - - // Reset font appearance if the font family does not have the selected font style - if ( ! hasFontStyle ) { + } else { + // Reset font appearance if there are no available styles or weights. resetFontAppearance(); } - }, [ fontFamily ] ); + }, [ + nearestFontStyle, + nearestFontWeight, + resetFontAppearance, + setFontAppearance, + ] ); // Line Height const hasLineHeightEnabled = useHasLineHeightControl( settings ); diff --git a/packages/block-editor/src/components/global-styles/typography-utils.js b/packages/block-editor/src/components/global-styles/typography-utils.js index 0d4c1b29b8b662..59ff04bf21ebcb 100644 --- a/packages/block-editor/src/components/global-styles/typography-utils.js +++ b/packages/block-editor/src/components/global-styles/typography-utils.js @@ -11,6 +11,7 @@ import { getComputedFluidTypographyValue, getTypographyValueAndUnit, } from '../font-sizes/fluid-utils'; +import { getFontStylesAndWeights } from '../../utils/get-font-styles-and-weights'; /** * @typedef {Object} FluidPreset @@ -127,9 +128,9 @@ export function getFluidTypographyOptionsFromSettings( settings ) { * Returns an object of merged font families and the font faces from the selected font family * based on the theme.json settings object and the currently selected font family. * - * @param {Object} settings Theme.json settings - * @param {string} selectedFontFamily Decoded font family string - * @return {Object} Merged font families and font faces from the selected font family + * @param {Object} settings Theme.json settings. + * @param {string} selectedFontFamily Decoded font family string. + * @return {Object} Merged font families and font faces from the selected font family. */ export function getMergedFontFamiliesAndFontFamilyFaces( settings, @@ -153,11 +154,10 @@ export function getMergedFontFamiliesAndFontFamilyFaces( * Returns the nearest font weight value from the available font weight list based on the new font weight. * The nearest font weight is the one with the smallest difference from the new font weight. * - * @param {Array} availableFontWeights Array of available font weights - * @param {string} newFontWeightValue New font weight value - * @return {string} Nearest font weight + * @param {Array} availableFontWeights Array of available font weights. + * @param {string} newFontWeightValue New font weight value. + * @return {string} Nearest font weight. */ - export function findNearestFontWeight( availableFontWeights, newFontWeightValue @@ -185,3 +185,99 @@ export function findNearestFontWeight( return nearestFontWeight; } + +/** + * Returns the nearest font style based on the new font style. + * Defaults to an empty string if the new font style is not valid or available. + * + * @param {Array} availableFontStyles Array of available font weights. + * @param {string} newFontStyleValue New font style value. + * @return {string} Nearest font style or an empty string. + */ +export function findNearestFontStyle( availableFontStyles, newFontStyleValue ) { + if ( typeof newFontStyleValue !== 'string' || ! newFontStyleValue ) { + return ''; + } + + const validStyles = [ 'normal', 'italic', 'oblique' ]; + if ( ! validStyles.includes( newFontStyleValue ) ) { + return ''; + } + + if ( + ! availableFontStyles || + availableFontStyles.length === 0 || + availableFontStyles.find( + ( style ) => style.value === newFontStyleValue + ) + ) { + return newFontStyleValue; + } + + if ( + newFontStyleValue === 'oblique' && + ! availableFontStyles.find( ( style ) => style.value === 'oblique' ) + ) { + return 'italic'; + } + + return ''; +} + +/** + * Returns the nearest font style and weight based on the available font family faces and the new font style and weight. + * + * @param {Array} fontFamilyFaces Array of available font family faces. + * @param {string} fontStyle New font style. Defaults to previous value. + * @param {string} fontWeight New font weight. Defaults to previous value. + * @return {Object} Nearest font style and font weight. + */ +export function findNearestStyleAndWeight( + fontFamilyFaces, + fontStyle, + fontWeight +) { + let nearestFontStyle = fontStyle; + let nearestFontWeight = fontWeight; + + const { fontStyles, fontWeights, combinedStyleAndWeightOptions } = + getFontStylesAndWeights( fontFamilyFaces ); + + // Check if the new font style and weight are available in the font family faces. + const hasFontStyle = fontStyles?.some( + ( { value: fs } ) => fs === fontStyle + ); + const hasFontWeight = fontWeights?.some( + ( { value: fw } ) => fw === fontWeight + ); + + if ( ! hasFontStyle ) { + /* + * Default to italic if oblique is not available. + * Or find the nearest font style based on the nearest font weight. + */ + nearestFontStyle = fontStyle + ? findNearestFontStyle( fontStyles, fontStyle ) + : combinedStyleAndWeightOptions?.find( + ( option ) => + option.style.fontWeight === + findNearestFontWeight( fontWeights, fontWeight ) + )?.style?.fontStyle; + } + + if ( ! hasFontWeight ) { + /* + * Find the nearest font weight based on available weights. + * Or find the nearest font weight based on the nearest font style. + */ + nearestFontWeight = fontWeight + ? findNearestFontWeight( fontWeights, fontWeight ) + : combinedStyleAndWeightOptions?.find( + ( option ) => + option.style.fontStyle === + ( nearestFontStyle || fontStyle ) + )?.style?.fontWeight; + } + + return { nearestFontStyle, nearestFontWeight }; +} diff --git a/test/e2e/specs/editor/various/font-appearance-control.spec.js b/test/e2e/specs/editor/various/font-appearance-control.spec.js new file mode 100644 index 00000000000000..4b148b6dd8587c --- /dev/null +++ b/test/e2e/specs/editor/various/font-appearance-control.spec.js @@ -0,0 +1,90 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Font Appearance Control dropdown menu', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'should apply available font weight and styles from active font family', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'Regular', + style: { + typography: { fontWeight: '400', fontStyle: 'normal' }, + }, + }, + } ); + await page + .getByRole( 'button', { name: 'Typography options' } ) + .click(); + await expect( + page.getByRole( 'combobox', { name: 'Appearance' } ) + ).toHaveText( 'Regular' ); + + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'Extra Light Italic', + style: { + typography: { fontWeight: '200', fontStyle: 'italic' }, + }, + }, + } ); + await page + .getByRole( 'button', { name: 'Typography options' } ) + .click(); + await expect( + page.getByRole( 'combobox', { name: 'Appearance' } ) + ).toHaveText( 'Extra Light Italic' ); + + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'Bold Italic', + style: { + typography: { fontWeight: '700', fontStyle: 'italic' }, + }, + }, + } ); + await page + .getByRole( 'button', { name: 'Typography options' } ) + .click(); + await expect( + page.getByRole( 'combobox', { name: 'Appearance' } ) + ).toHaveText( 'Bold Italic' ); + } ); + + test( 'should apply Default appearance if weight and style are invalid', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'Default', + style: { + typography: { + fontWeight: '', + fontStyle: 'invalid-style', + }, + }, + }, + } ); + await page + .getByRole( 'button', { name: 'Typography options' } ) + .click(); + await page + .getByRole( 'menuitemcheckbox', { name: 'Show Appearance' } ) + .click(); + await expect( + page.getByRole( 'combobox', { name: 'Appearance' } ) + ).toHaveText( 'Default' ); + } ); +} );