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: add font size constraints #44993

Merged
76 changes: 62 additions & 14 deletions lib/block-supports/typography.php
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,17 @@ function gutenberg_get_typography_value_and_unit( $raw_value, $options = array()
$unit = $options['coerce_to'];
}

/*
* No calculation is required if swapping between em and rem yet,
* since we assume a root size value. Later we might like to differentiate between
* :root font size (rem) and parent element font size (em) relativity.
*/
if ( ( 'em' === $options['coerce_to'] || 'rem' === $options['coerce_to'] ) && ( 'em' === $unit || 'rem' === $unit ) ) {
$unit = $options['coerce_to'];
}

return array(
'value' => $value,
'value' => round( $value, 3 ),
'unit' => $unit,
);
}
Expand Down Expand Up @@ -357,27 +366,29 @@ function gutenberg_get_computed_fluid_typography_value( $args = array() ) {
$minimum_font_size_raw = isset( $args['minimum_font_size'] ) ? $args['minimum_font_size'] : null;
$scale_factor = isset( $args['scale_factor'] ) ? $args['scale_factor'] : null;

// Grab the minimum font size and normalize it in order to use the value for calculations.
// Normalizes the minimum font size in order to use the value for calculations.
$minimum_font_size = gutenberg_get_typography_value_and_unit( $minimum_font_size_raw );

// We get a 'preferred' unit to keep units consistent when calculating,
// otherwise the result will not be accurate.
/*
* We get a 'preferred' unit to keep units consistent when calculating,
* otherwise the result will not be accurate.
*/
$font_size_unit = isset( $minimum_font_size['unit'] ) ? $minimum_font_size['unit'] : 'rem';

// Grab the maximum font size and normalize it in order to use the value for calculations.
// Grabs the maximum font size and normalize it in order to use the value for calculations.
$maximum_font_size = gutenberg_get_typography_value_and_unit(
$maximum_font_size_raw,
array(
'coerce_to' => $font_size_unit,
)
);

// Protect against unsupported units.
// Checks for mandatory min and max sizes, and protects against unsupported units.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the (unchanged) lines above, we're coercing the max size to $font_size_unit but not the min size, is that deliberate?

Copy link
Member Author

@ramonjd ramonjd Oct 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, and here's the logic (mine at least 😄)

The min and max font size units can theoretically differ, so one can be px and the other rem. This is fine in
the resulting clamp value's first and third args, e.g., clamp(20px, 1.25rem + ((1vw - 7.68px) * 93.75), 50rem)

But in order to calculate a linear factor between viewport boundaries we have to do the math on similar values, that is, values whose units are the same. So we coerce the max value unit to whatever the min value unit is.

I use the min value because, if there isn't a min value specified, e.g., it's a custom font size, we use incoming custom font size to calculate a minimum value, and therefore use its unit.

The functions' internals are ripe for refactor by the way, but since it's all neatly housed I think we can do it in a follow up.

if ( ! $maximum_font_size || ! $minimum_font_size ) {
return null;
}

// Use rem for accessible fluid target font scaling.
// Uses rem for accessible fluid target font scaling.
$minimum_font_size_rem = gutenberg_get_typography_value_and_unit(
$minimum_font_size_raw,
array(
Expand All @@ -403,8 +414,9 @@ function gutenberg_get_computed_fluid_typography_value( $args = array() ) {
// Borrowed from https://websemantics.uk/tools/responsive-font-calculator/.
$view_port_width_offset = round( $minimum_viewport_width['value'] / 100, 3 ) . $font_size_unit;
$linear_factor = 100 * ( ( $maximum_font_size['value'] - $minimum_font_size['value'] ) / ( $maximum_viewport_width['value'] - $minimum_viewport_width['value'] ) );
$linear_factor = round( $linear_factor, 3 ) * $scale_factor;
$fluid_target_font_size = implode( '', $minimum_font_size_rem ) . " + ((1vw - $view_port_width_offset) * $linear_factor)";
$linear_factor_scaled = round( $linear_factor * $scale_factor, 3 );
$linear_factor_scaled = empty( $linear_factor_scaled ) ? 1 : $linear_factor_scaled;
$fluid_target_font_size = implode( '', $minimum_font_size_rem ) . " + ((1vw - $view_port_width_offset) * $linear_factor_scaled)";

return "clamp($minimum_font_size_raw, $fluid_target_font_size, $maximum_font_size_raw)";
}
Expand Down Expand Up @@ -437,7 +449,7 @@ function gutenberg_get_typography_font_size_value( $preset, $should_use_fluid_ty
return $preset['size'];
}

// Check if fluid font sizes are activated.
// Checks if fluid font sizes are activated.
$typography_settings = gutenberg_get_global_settings( array( 'typography' ) );
$should_use_fluid_typography = isset( $typography_settings['fluid'] ) && true === $typography_settings['fluid'] ? true : $should_use_fluid_typography;

Expand All @@ -451,6 +463,7 @@ function gutenberg_get_typography_font_size_value( $preset, $should_use_fluid_ty
$default_minimum_font_size_factor = 0.75;
$default_maximum_font_size_factor = 1.5;
$default_scale_factor = 1;
$default_minimum_font_size_limit = '14px';

// Font sizes.
$fluid_font_size_settings = isset( $preset['fluid'] ) ? $preset['fluid'] : null;
Expand All @@ -472,13 +485,48 @@ function gutenberg_get_typography_font_size_value( $preset, $should_use_fluid_ty
return $preset['size'];
}

// If no fluid min or max font sizes are available, create some using min/max font size factors.
// If no fluid max font size is available, create one using max font size factor.
if ( ! $maximum_font_size_raw ) {
$maximum_font_size_raw = round( $preferred_size['value'] * $default_maximum_font_size_factor, 3 ) . $preferred_size['unit'];
}

// If no fluid min font size is available, create one using min font size factor.
if ( ! $minimum_font_size_raw ) {
$minimum_font_size_raw = ( $preferred_size['value'] * $default_minimum_font_size_factor ) . $preferred_size['unit'];
$minimum_font_size_raw = round( $preferred_size['value'] * $default_minimum_font_size_factor, 3 ) . $preferred_size['unit'];
}

if ( ! $maximum_font_size_raw ) {
$maximum_font_size_raw = ( $preferred_size['value'] * $default_maximum_font_size_factor ) . $preferred_size['unit'];
// Parses the minimum font size limit, so we can perform checks using it.
$minimum_font_size_limit = gutenberg_get_typography_value_and_unit(
$default_minimum_font_size_limit,
array(
'coerce_to' => $preferred_size['unit'],
)
);

if ( ! empty( $minimum_font_size_limit ) ) {
/*
* If a minimum size was not passed to this function
* and the user-defined font size is lower than $minimum_font_size_limit,
* then uses the user-defined font size as the minimum font-size.
*/
if ( ! isset( $fluid_font_size_settings['min'] ) && $preferred_size['value'] < $minimum_font_size_limit['value'] ) {
$minimum_font_size_raw = implode( '', $preferred_size );
} else {
$minimum_font_size_parsed = gutenberg_get_typography_value_and_unit(
$minimum_font_size_raw,
array(
'coerce_to' => $preferred_size['unit'],
)
);

/*
* If the passed or calculated minimum font size is lower than $minimum_font_size_limit
* use $minimum_font_size_limit instead.
*/
if ( ! empty( $minimum_font_size_parsed ) && $minimum_font_size_parsed['value'] < $minimum_font_size_limit['value'] ) {
$minimum_font_size_raw = implode( '', $minimum_font_size_limit );
}
}
}

$fluid_font_size_value = gutenberg_get_computed_fluid_typography_value(
Expand Down
125 changes: 100 additions & 25 deletions packages/block-editor/src/components/font-sizes/fluid-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const DEFAULT_MINIMUM_VIEWPORT_WIDTH = '768px';
const DEFAULT_SCALE_FACTOR = 1;
const DEFAULT_MINIMUM_FONT_SIZE_FACTOR = 0.75;
const DEFAULT_MAXIMUM_FONT_SIZE_FACTOR = 1.5;
const DEFAULT_MINIMUM_FONT_SIZE_LIMIT = '14px';

/**
* Computes a fluid font-size value that uses clamp(). A minimum and maxinmum
Expand Down Expand Up @@ -53,11 +54,20 @@ export function getComputedFluidTypographyValue( {
scaleFactor = DEFAULT_SCALE_FACTOR,
minimumFontSizeFactor = DEFAULT_MINIMUM_FONT_SIZE_FACTOR,
maximumFontSizeFactor = DEFAULT_MAXIMUM_FONT_SIZE_FACTOR,
minimumFontSizeLimit = DEFAULT_MINIMUM_FONT_SIZE_LIMIT,
} ) {
// Calculate missing minimumFontSize and maximumFontSize from
// defaultFontSize if provided.
if ( fontSize && ( ! minimumFontSize || ! maximumFontSize ) ) {
// Parse default font size.
/*
* Caches minimumFontSize in minimumFontSizeValue
* so we can check if minimumFontSize exists later.
*/
let minimumFontSizeValue = minimumFontSize;

/*
* Calculates missing minimumFontSize and maximumFontSize from
* defaultFontSize if provided.
*/
if ( fontSize ) {
// Parses default font size.
const fontSizeParsed = getTypographyValueAndUnit( fontSize );

// Protect against invalid units.
Expand All @@ -66,46 +76,95 @@ export function getComputedFluidTypographyValue( {
}

// If no minimumFontSize is provided, derive using min scale factor.
if ( ! minimumFontSize ) {
minimumFontSize =
fontSizeParsed.value * minimumFontSizeFactor +
fontSizeParsed.unit;
if ( ! minimumFontSizeValue ) {
minimumFontSizeValue =
roundToPrecision(
fontSizeParsed.value * minimumFontSizeFactor,
3
) + fontSizeParsed.unit;
}

// Parses the minimum font size limit, so we can perform checks using it.
const minimumFontSizeLimitParsed = getTypographyValueAndUnit(
minimumFontSizeLimit,
{
coerceTo: fontSizeParsed.unit,
}
);

if ( !! minimumFontSizeLimitParsed?.value ) {
/*
* If a minimum size was not passed to this function
* and the user-defined font size is lower than `minimumFontSizeLimit`,
* then uses the user-defined font size as the minimum font-size.
*/
if (
! minimumFontSize &&
fontSizeParsed?.value < minimumFontSizeLimitParsed?.value
) {
minimumFontSizeValue = `${ fontSizeParsed.value }${ fontSizeParsed.unit }`;
} else {
const minimumFontSizeParsed = getTypographyValueAndUnit(
minimumFontSizeValue,
{
coerceTo: fontSizeParsed.unit,
}
);

/*
* Otherwise, if the passed or calculated minimum font size is lower than `minimumFontSizeLimit`
* use `minimumFontSizeLimit` instead.
*/
if (
!! minimumFontSizeParsed?.value &&
minimumFontSizeParsed.value <
minimumFontSizeLimitParsed.value
) {
minimumFontSizeValue = `${ minimumFontSizeLimitParsed.value }${ minimumFontSizeLimitParsed.unit }`;
}
}
}

// If no maximumFontSize is provided, derive using max scale factor.
if ( ! maximumFontSize ) {
maximumFontSize =
fontSizeParsed.value * maximumFontSizeFactor +
fontSizeParsed.unit;
roundToPrecision(
fontSizeParsed.value * maximumFontSizeFactor,
3
) + fontSizeParsed.unit;
}
}

// Return early if one of the provided inputs is not provided.
if ( ! minimumFontSize || ! maximumFontSize ) {
if ( ! minimumFontSizeValue || ! maximumFontSize ) {
return null;
}

// Grab the minimum font size and normalize it in order to use the value for calculations.
const minimumFontSizeParsed = getTypographyValueAndUnit( minimumFontSize );
const minimumFontSizeParsed =
getTypographyValueAndUnit( minimumFontSizeValue );

// 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.
// Grabs 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.
// Checks for mandatory min and max sizes, and protects against unsupported units.
if ( ! minimumFontSizeParsed || ! maximumFontSizeParsed ) {
return null;
}

// Use rem for accessible fluid target font scaling.
const minimumFontSizeRem = getTypographyValueAndUnit( minimumFontSize, {
coerceTo: 'rem',
} );
// Uses rem for accessible fluid target font scaling.
const minimumFontSizeRem = getTypographyValueAndUnit(
minimumFontSizeValue,
{
coerceTo: 'rem',
}
);

// Viewport widths defined for fluid typography. Normalize units
const maximumViewPortWidthParsed = getTypographyValueAndUnit(
Expand Down Expand Up @@ -133,17 +192,20 @@ export function getComputedFluidTypographyValue( {
3
);

const viewPortWidthOffset = minViewPortWidthOffsetValue + fontSizeUnit;
let linearFactor =
const viewPortWidthOffset =
roundToPrecision( minViewPortWidthOffsetValue, 3 ) + fontSizeUnit;
const linearFactor =
100 *
( ( maximumFontSizeParsed.value - minimumFontSizeParsed.value ) /
( maximumViewPortWidthParsed.value -
minumumViewPortWidthParsed.value ) );
linearFactor = roundToPrecision( linearFactor, 3 ) || 1;
ramonjd marked this conversation as resolved.
Show resolved Hide resolved
const linearFactorScaled = linearFactor * scaleFactor;
const linearFactorScaled = roundToPrecision(
( linearFactor || 1 ) * scaleFactor,
3
);
const fluidTargetFontSize = `${ minimumFontSizeRem.value }${ minimumFontSizeRem.unit } + ((1vw - ${ viewPortWidthOffset }) * ${ linearFactorScaled })`;

return `clamp(${ minimumFontSize }, ${ fluidTargetFontSize }, ${ maximumFontSize })`;
return `clamp(${ minimumFontSizeValue }, ${ fluidTargetFontSize }, ${ maximumFontSize })`;
}

/**
Expand Down Expand Up @@ -199,8 +261,20 @@ export function getTypographyValueAndUnit( rawValue, options = {} ) {
unit = coerceTo;
}

/*
* No calculation is required if swapping between em and rem yet,
* since we assume a root size value. Later we might like to differentiate between
* :root font size (rem) and parent element font size (em) relativity.
*/
if (
( 'em' === coerceTo || 'rem' === coerceTo ) &&
( 'em' === unit || 'rem' === unit )
) {
unit = coerceTo;
}

return {
value: returnValue,
value: roundToPrecision( returnValue, 3 ),
unit,
};
}
Expand All @@ -215,7 +289,8 @@ export function getTypographyValueAndUnit( rawValue, options = {} ) {
* @return {number|undefined} Value rounded to standard precision.
*/
export function roundToPrecision( value, digits = 3 ) {
const base = Math.pow( 10, digits );
return Number.isFinite( value )
? parseFloat( value.toFixed( digits ) )
? parseFloat( Math.round( value * base ) / base )
: undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe( 'getComputedFluidTypographyValue()', () => {
fontSize: '30px',
} );
expect( fluidTypographyValues ).toBe(
'clamp(22.5px, 1.40625rem + ((1vw - 7.68px) * 2.704), 45px)'
'clamp(22.5px, 1.406rem + ((1vw - 7.68px) * 2.704), 45px)'
);
} );

Expand All @@ -42,7 +42,7 @@ describe( 'getComputedFluidTypographyValue()', () => {
fontSize: '30px',
} );
expect( fluidTypographyValues ).toBe(
'clamp(22.5px, 1.40625rem + ((1vw - 7.68px) * 2.704), 45px)'
'clamp(22.5px, 1.406rem + ((1vw - 7.68px) * 2.704), 45px)'
);
} );

Expand All @@ -53,7 +53,7 @@ describe( 'getComputedFluidTypographyValue()', () => {
maximumViewPortWidth: '1000px',
} );
expect( fluidTypographyValues ).toBe(
'clamp(22.5px, 1.40625rem + ((1vw - 5px) * 4.5), 45px)'
'clamp(22.5px, 1.406rem + ((1vw - 5px) * 4.5), 45px)'
);
} );

Expand All @@ -63,7 +63,7 @@ describe( 'getComputedFluidTypographyValue()', () => {
scaleFactor: '2',
} );
expect( fluidTypographyValues ).toBe(
'clamp(22.5px, 1.40625rem + ((1vw - 7.68px) * 5.408), 45px)'
'clamp(22.5px, 1.406rem + ((1vw - 7.68px) * 5.409), 45px)'
);
} );

Expand All @@ -74,7 +74,7 @@ describe( 'getComputedFluidTypographyValue()', () => {
maximumFontSizeFactor: '2',
} );
expect( fluidTypographyValues ).toBe(
'clamp(15px, 0.9375rem + ((1vw - 7.68px) * 5.409), 60px)'
'clamp(15px, 0.938rem + ((1vw - 7.68px) * 5.409), 60px)'
);
} );

Expand Down
Loading