diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index 382b897fefcf87..6e106ac7fcc09f 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -233,7 +233,7 @@ function gutenberg_typography_get_css_variable_inline_style( $attributes, $featu * 'root_size_value' => (number) Value of root font size for rem|em <-> px conversion. Default `16`. * 'acceptable_units' => (array) An array of font size units. Default `[ 'rem', 'px', 'em' ]`; * );. - * @return array An array consisting of `'value'` and `'unit'`, e.g., [ '42', 'rem' ] + * @return array An array consisting of `'value'` and `'unit'` properties. */ function gutenberg_get_typography_value_and_unit( $raw_value, $options = array() ) { if ( empty( $raw_value ) ) { diff --git a/packages/edit-site/src/components/global-styles/test/typography-utils.js b/packages/edit-site/src/components/global-styles/test/typography-utils.js new file mode 100644 index 00000000000000..2306e61522f07e --- /dev/null +++ b/packages/edit-site/src/components/global-styles/test/typography-utils.js @@ -0,0 +1,105 @@ +/** + * Internal dependencies + */ +import { getTypographyFontSizeValue } from '../typography-utils'; + +describe( 'typography utils', () => { + describe( 'getTypographyFontSizeValue', () => { + it( 'should return the expected values', () => { + [ + // Default return non-fluid value. + { + preset: { + size: '28px', + }, + typographySettings: undefined, + expected: '28px', + }, + // Should return fluid value. + { + preset: { + size: '1.75rem', + }, + typographySettings: { + fluid: true, + }, + expected: + 'clamp(1.3125rem, 1.3125rem + ((1vw - 0.48rem) * 2.524), 2.625rem)', + }, + // Should return default fluid values with empty fluid array. + { + preset: { + size: '28px', + fluid: [], + }, + typographySettings: { + fluid: true, + }, + expected: + 'clamp(21px, 1.3125rem + ((1vw - 7.68px) * 2.524), 42px)', + }, + // Should return size with invalid fluid units. + { + preset: { + size: '10em', + fluid: { + min: '20vw', + max: '50%', + }, + }, + typographySettings: { + fluid: true, + }, + expected: '10em', + }, + // Should return fluid clamp value. + { + preset: { + size: '28px', + fluid: { + min: '20px', + max: '50rem', + }, + }, + typographySettings: { + fluid: true, + }, + expected: + 'clamp(20px, 1.25rem + ((1vw - 7.68px) * 93.75), 50rem)', + }, + // Should return clamp value with default fluid max value. + { + preset: { + size: '28px', + fluid: { + min: '2.6rem', + }, + }, + typographySettings: { + fluid: true, + }, + expected: + 'clamp(2.6rem, 2.6rem + ((1vw - 0.48rem) * 0.048), 42px)', + }, + // Should return clamp value with default fluid min value. + { + preset: { + size: '28px', + fluid: { + max: '80px', + }, + }, + typographySettings: { + fluid: true, + }, + expected: + 'clamp(21px, 1.3125rem + ((1vw - 7.68px) * 7.091), 80px)', + }, + ].forEach( ( { preset, typographySettings, expected } ) => { + expect( + getTypographyFontSizeValue( preset, typographySettings ) + ).toBe( expected ); + } ); + } ); + } ); +} ); diff --git a/packages/edit-site/src/components/global-styles/typography-utils.js b/packages/edit-site/src/components/global-styles/typography-utils.js new file mode 100644 index 00000000000000..53c782da0697af --- /dev/null +++ b/packages/edit-site/src/components/global-styles/typography-utils.js @@ -0,0 +1,223 @@ +/** + * The fluid utilities must match the backend equivalent. + * See: gutenberg_get_typography_font_size_value() in lib/block-supports/typography.php + * --------------------------------------------------------------- + */ + +/** + * Returns a font-size value based on a given font-size preset. + * Takes into account fluid typography parameters and attempts to return a css formula depending on available, valid values. + * + * @param {Object} preset + * @param {string} preset.size A default font size. + * @param {string} preset.name A font size name, displayed in the UI. + * @param {string} preset.slug A font size slug. + * @param {Object} preset.fluid + * @param {string|undefined} preset.fluid.max A maximum font size value. + * @param {string|undefined} preset.fluid.min A minimum font size value. + * @param {Object} typographySettings + * @param {boolean} typographySettings.fluid Whether fluid typography is enabled. + * + * @return {string} An font-size value + */ +export function getTypographyFontSizeValue( preset, typographySettings ) { + const { size: defaultSize } = preset; + + if ( true !== typographySettings?.fluid ) { + return defaultSize; + } + + // Defaults. + const DEFAULT_MAXIMUM_VIEWPORT_WIDTH = '1600px'; + const DEFAULT_MINIMUM_VIEWPORT_WIDTH = '768px'; + const DEFAULT_MINIMUM_FONT_SIZE_FACTOR = 0.75; + const DEFAULT_MAXIMUM_FONT_SIZE_FACTOR = 1.5; + const DEFAULT_SCALE_FACTOR = 1; + + // Font sizes. + const fluidFontSizeSettings = preset?.fluid || {}; + + // Try to grab explicit min and max fluid font sizes. + let minimumFontSizeRaw = fluidFontSizeSettings?.min; + let maximumFontSizeRaw = fluidFontSizeSettings?.max; + const preferredSize = getTypographyValueAndUnit( defaultSize ); + + // Protect against unsupported units. + if ( ! preferredSize?.unit ) { + return defaultSize; + } + + // If no fluid min or max font sizes are available, create some using min/max font size factors. + if ( ! minimumFontSizeRaw ) { + minimumFontSizeRaw = + preferredSize.value * DEFAULT_MINIMUM_FONT_SIZE_FACTOR + + preferredSize.unit; + } + + if ( ! maximumFontSizeRaw ) { + maximumFontSizeRaw = + preferredSize.value * DEFAULT_MAXIMUM_FONT_SIZE_FACTOR + + preferredSize.unit; + } + + const fluidFontSizeValue = getComputedFluidTypographyValue( { + maximumViewPortWidth: DEFAULT_MAXIMUM_VIEWPORT_WIDTH, + minimumViewPortWidth: DEFAULT_MINIMUM_VIEWPORT_WIDTH, + maximumFontSize: maximumFontSizeRaw, + minimumFontSize: minimumFontSizeRaw, + scaleFactor: DEFAULT_SCALE_FACTOR, + } ); + + if ( !! fluidFontSizeValue ) { + return fluidFontSizeValue; + } + + return defaultSize; +} + +/** + * Internal implementation of clamp() based on available min/max viewport width, and min/max font sizes. + * + * @param {Object} args + * @param {string} args.maximumViewPortWidth Maximum size up to which type will have fluidity. + * @param {string} args.minimumViewPortWidth Minimum viewport size from which type will have fluidity. + * @param {string} args.maximumFontSize Maximum font size for any clamp() calculation. + * @param {string} args.minimumFontSize Minimum font size for any clamp() calculation. + * @param {number} args.scaleFactor A scale factor to determine how fast a font scales within boundaries. + * + * @return {string|null} A font-size value using clamp(). + */ +export function getComputedFluidTypographyValue( { + maximumViewPortWidth, + minimumViewPortWidth, + maximumFontSize, + minimumFontSize, + scaleFactor, +} ) { + // Grab the minimum font size and normalize it in order to use the value for calculations. + const minimumFontSizeParsed = getTypographyValueAndUnit( minimumFontSize ); + + // We get a 'preferred' unit to keep units consistent when calculating, + // otherwise the result will not be accurate. + const fontSizeUnit = minimumFontSizeParsed?.unit || 'rem'; + + // Grab the maximum font size and normalize it in order to use the value for calculations. + const maximumFontSizeParsed = getTypographyValueAndUnit( maximumFontSize, { + coerceTo: fontSizeUnit, + } ); + + // Protect against unsupported units. + if ( ! minimumFontSizeParsed || ! maximumFontSizeParsed ) { + return null; + } + + // Use rem for accessible fluid target font scaling. + const minimumFontSizeRem = getTypographyValueAndUnit( minimumFontSize, { + coerceTo: 'rem', + } ); + + // Viewport widths defined for fluid typography. Normalize units + const maximumViewPortWidthParsed = getTypographyValueAndUnit( + maximumViewPortWidth, + { coerceTo: fontSizeUnit } + ); + const minumumViewPortWidthParsed = getTypographyValueAndUnit( + minimumViewPortWidth, + { coerceTo: fontSizeUnit } + ); + + // Protect against unsupported units. + if ( + ! maximumViewPortWidthParsed || + ! minumumViewPortWidthParsed || + ! minimumFontSizeRem + ) { + return null; + } + + // Build CSS rule. + // Borrowed from https://websemantics.uk/tools/responsive-font-calculator/. + const minViewPortWidthOffsetValue = roundToPrecision( + minumumViewPortWidthParsed.value / 100, + 3 + ); + + const viewPortWidthOffset = minViewPortWidthOffsetValue + fontSizeUnit; + let linearFactor = + 100 * + ( ( maximumFontSizeParsed.value - minimumFontSizeParsed.value ) / + ( maximumViewPortWidthParsed.value - + minumumViewPortWidthParsed.value ) ); + linearFactor = roundToPrecision( linearFactor, 3 ) || 1; + const linearFactorScaled = linearFactor * scaleFactor; + const fluidTargetFontSize = `${ minimumFontSizeRem.value }${ minimumFontSizeRem.unit } + ((1vw - ${ viewPortWidthOffset }) * ${ linearFactorScaled })`; + + return `clamp(${ minimumFontSize }, ${ fluidTargetFontSize }, ${ maximumFontSize })`; +} + +/** + * + * @param {string} rawValue Raw size value from theme.json. + * @param {Object|undefined} options Calculation options. + * + * @return {{ unit: string, value: number }|null} An object consisting of `'value'` and `'unit'` properties. + */ +export function getTypographyValueAndUnit( rawValue, options = {} ) { + if ( ! rawValue ) { + return null; + } + + const { coerceTo, rootSizeValue, acceptableUnits } = { + coerceTo: '', + // Default browser font size. Later we could inject some JS to compute this `getComputedStyle( document.querySelector( "html" ) ).fontSize`. + rootSizeValue: 16, + acceptableUnits: [ 'rem', 'px', 'em' ], + ...options, + }; + + const acceptableUnitsGroup = acceptableUnits?.join( '|' ); + const regexUnits = new RegExp( + `^(\\d*\\.?\\d+)(${ acceptableUnitsGroup }){1,1}$` + ); + + const matches = rawValue.match( regexUnits ); + + // We need a number value and a unit. + if ( ! matches || matches.length < 3 ) { + return null; + } + + let [ , value, unit ] = matches; + + let returnValue = parseFloat( value ); + + if ( 'px' === coerceTo && ( 'em' === unit || 'rem' === unit ) ) { + returnValue = returnValue * rootSizeValue; + unit = coerceTo; + } + + if ( 'px' === unit && ( 'em' === coerceTo || 'rem' === coerceTo ) ) { + returnValue = returnValue / rootSizeValue; + unit = coerceTo; + } + + return { + value: returnValue, + unit, + }; +} + +/** + * Returns a value rounded to defined precision. + * Returns `undefined` if the value is not a valid finite number. + * + * @param {number} value Raw value. + * @param {number} digits The number of digits to appear after the decimal point + * + * @return {number|undefined} Value rounded to standard precision. + */ +export function roundToPrecision( value, digits = 3 ) { + return Number.isFinite( value ) + ? parseFloat( value.toFixed( digits ) ) + : undefined; +} diff --git a/packages/edit-site/src/components/global-styles/use-global-styles-output.js b/packages/edit-site/src/components/global-styles/use-global-styles-output.js index 431e8a420f79b0..d5f2f213ad839d 100644 --- a/packages/edit-site/src/components/global-styles/use-global-styles-output.js +++ b/packages/edit-site/src/components/global-styles/use-global-styles-output.js @@ -62,10 +62,11 @@ function compileStyleValue( uncompiledValue ) { * Transform given preset tree into a set of style declarations. * * @param {Object} blockPresets + * @param {Object} mergedSettings Merged theme.json settings. * - * @return {Array} An array of style declarations. + * @return {Array} An array of style declarations. */ -function getPresetsDeclarations( blockPresets = {} ) { +function getPresetsDeclarations( blockPresets = {}, mergedSettings ) { return reduce( PRESET_METADATA, ( declarations, { path, valueKey, valueFunc, cssVarInfix } ) => { @@ -73,7 +74,7 @@ function getPresetsDeclarations( blockPresets = {} ) { [ 'default', 'theme', 'custom' ].forEach( ( origin ) => { if ( presetByOrigin[ origin ] ) { presetByOrigin[ origin ].forEach( ( value ) => { - if ( valueKey ) { + if ( valueKey && ! valueFunc ) { declarations.push( `--wp--preset--${ cssVarInfix }--${ kebabCase( value.slug @@ -86,7 +87,7 @@ function getPresetsDeclarations( blockPresets = {} ) { declarations.push( `--wp--preset--${ cssVarInfix }--${ kebabCase( value.slug - ) }: ${ valueFunc( value ) }` + ) }: ${ valueFunc( value, mergedSettings ) }` ); } } ); @@ -515,10 +516,9 @@ export const getNodesWithSettings = ( tree, blockSelectors ) => { export const toCustomProperties = ( tree, blockSelectors ) => { const settings = getNodesWithSettings( tree, blockSelectors ); - let ruleset = ''; settings.forEach( ( { presets, custom, selector } ) => { - const declarations = getPresetsDeclarations( presets ); + const declarations = getPresetsDeclarations( presets, tree?.settings ); const customProps = flattenTree( custom, '--wp--custom--', '--' ); if ( customProps.length > 0 ) { declarations.push( ...customProps ); diff --git a/packages/edit-site/src/components/global-styles/utils.js b/packages/edit-site/src/components/global-styles/utils.js index e4818d77d1df43..4c4aef887c6e0d 100644 --- a/packages/edit-site/src/components/global-styles/utils.js +++ b/packages/edit-site/src/components/global-styles/utils.js @@ -3,6 +3,11 @@ */ import { get, find } from 'lodash'; +/** + * Internal dependencies + */ +import { getTypographyFontSizeValue } from './typography-utils'; + /* Supporting data. */ export const ROOT_BLOCK_NAME = 'root'; export const ROOT_BLOCK_SELECTOR = 'body'; @@ -58,6 +63,8 @@ export const PRESET_METADATA = [ }, { path: [ 'typography', 'fontSizes' ], + valueFunc: ( preset, { typography: typographySettings } ) => + getTypographyFontSizeValue( preset, typographySettings ), valueKey: 'size', cssVarInfix: 'font-size', classes: [ { classSuffix: 'font-size', propertyName: 'font-size' } ], diff --git a/phpunit/block-supports/typography-test.php b/phpunit/block-supports/typography-test.php index 956acd40f3c308..48f4e4ccbe5b96 100644 --- a/phpunit/block-supports/typography-test.php +++ b/phpunit/block-supports/typography-test.php @@ -242,7 +242,7 @@ public function data_generate_font_size_preset_fixtures() { 'expected_output' => 'clamp(1.3125rem, 1.3125rem + ((1vw - 0.48rem) * 2.524), 2.625rem)', ), - 'return_default_fluid_values_with_empty_fluidSize' => array( + 'return_default_fluid_values_with_empty_fluid_array' => array( 'font_size_preset' => array( 'size' => '28px', 'fluid' => array(),