Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fluid typography: migrate fluid typography algorithm to JS for site editor #42688

Merged
merged 4 commits into from
Jul 27, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 an object consisting of `'value'` and `'unit'` properties.
ramonjd marked this conversation as resolved.
Show resolved Hide resolved
*/
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,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<Object>} An array of style declarations.
*/
function getPresetsDeclarations( blockPresets = {} ) {
function getPresetsDeclarations( blockPresets = {}, mergedSettings ) {
return reduce(
PRESET_METADATA,
( declarations, { path, valueKey, valueFunc, cssVarInfix } ) => {
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
8 changes: 7 additions & 1 deletion packages/edit-site/src/components/global-styles/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -58,7 +63,8 @@ export const PRESET_METADATA = [
},
{
path: [ 'typography', 'fontSizes' ],
valueKey: 'size',
valueFunc: ( preset, { typography: typographySettings } ) =>
getTypographyFontSizeValue( preset, typographySettings ),
cssVarInfix: 'font-size',
classes: [ { classSuffix: 'font-size', propertyName: 'font-size' } ],
},
Expand Down
Loading