Skip to content

Commit

Permalink
Fluid typography: migrate fluid typography algorithm to JS for site e…
Browse files Browse the repository at this point in the history
…ditor (#42688)

* Migrate fluid typography algorithm from PHP version of `gutenberg_get_typography_font_size_value` to JS.

We have to do this because the site editor is contained within an iframe and cannot, therefore, access the global CSS presets in the parent window.

* Remove var_dump. YOLO!

* Every preset metadata object needs a `valueKey` property and value so that the site editor can fetch values from the preset objects. Most notably these operations takes place in getValueFromPresetVariable() and getValueFromVariable()

* Update packages/edit-site/src/components/global-styles/typography-utils.js

My blod is typo

Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com>

Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
  • Loading branch information
ramonjd and andrewserong authored Jul 27, 2022
1 parent 6da2f27 commit c6c4dd4
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 8 deletions.
2 changes: 1 addition & 1 deletion lib/block-supports/typography.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 );
} );
} );
} );
} );
223 changes: 223 additions & 0 deletions packages/edit-site/src/components/global-styles/typography-utils.js
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,19 @@ 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<Object>} An array of style declarations.
*/
function getPresetsDeclarations( blockPresets = {} ) {
function getPresetsDeclarations( blockPresets = {}, mergedSettings ) {
return reduce(
PRESET_METADATA,
( declarations, { path, valueKey, valueFunc, cssVarInfix } ) => {
const presetByOrigin = get( blockPresets, path, [] );
[ '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
Expand All @@ -86,7 +87,7 @@ function getPresetsDeclarations( blockPresets = {} ) {
declarations.push(
`--wp--preset--${ cssVarInfix }--${ kebabCase(
value.slug
) }: ${ valueFunc( value ) }`
) }: ${ valueFunc( value, mergedSettings ) }`
);
}
} );
Expand Down Expand Up @@ -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 );
Expand Down
Loading

0 comments on commit c6c4dd4

Please sign in to comment.