Skip to content

Commit

Permalink
Block supports: add fluid typography (WordPress#39529)
Browse files Browse the repository at this point in the history
* Initial commit

* Enabling fluid type in theme.json
Only allowing rem and px

* Alternative calculation for fluid type

* Implementing new algorithm using max and mix viewport widths

* Refactoring the method to accept a `maxSize`

* The linter of doom!

Testing out a new model that allows all values to be custom, with a fallback clamp implementation.

* Extracting internal implementation.

* Added missing doc comment for fluid prop in theme.json

* Remove dupe presets

* Remove dupe settings

* Creating new compat file for 6.1

* Created fallback for min and max viewport widths
Returning `min()` CSS rule where there's only a maximum font size
Returning `max()` CSS rule where there's only a minimum font size
Removing `fluidFormula` as a property and related logic

* Checking for valid units
Adding tests
Docs

* Looking in layout settings for viewport width fallbacks

* Post-rebase file shuffling.
Renaming properties to `__experimental*`

* Update CHANGELOG.md

* min(), max() and clamp() automatically parse mathematic expressions so removing calc()
props @wongjn

* We're now supporting passing single values to the fluid type calculator, and returning clamp values based on a set of defaults: minFontSizeFactor, maxFontSizeFactor and scaleFactor.

So that themes can opt-in to the system, typography.fluid should be true, in which case default values will be used, or an object of viewport width, minFontSizeFactor, maxFontSizeFactor and scaleFactor.

The values in fontSizes will take precedence, then, if they're not there, we'll calculate a min and max font size based on the scale value.

The calculations for min() and max() have been removed since we'll always have a min and max font size value. This means we're returning a clamp() value always. I think that's okay as it also removes any unexpected side effects.

* Refactoring function signatures to pass options array instead of multiple args.

* Merge default args in gutenberg_get_typography_value_and_unit using wp_parse_args

* Fix merge conflicts with trunk.
Updated JSON schema.

* - add to settings > fluid would be a boolean true to enable the feature.
- remove content settings as fallbacks
- rename fluidSize to fluid in the typography settings for each font.
- unit tests

* - update docs

* reverse min/max viewport width

Updated tests

Co-authored-by: Ben Dwyer <[email protected]>
  • Loading branch information
ramonjd and scruffian authored Jul 18, 2022
1 parent dfc4b8b commit d2418af
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,13 @@ Settings related to typography.
| customFontSize | boolean | true | |
| fontStyle | boolean | true | |
| fontWeight | boolean | true | |
| fluid | boolean | | |
| letterSpacing | boolean | true | |
| lineHeight | boolean | false | |
| textDecoration | boolean | true | |
| textTransform | boolean | true | |
| dropCap | boolean | true | |
| fontSizes | array | | name, size, slug |
| fontSizes | array | | fluid, name, size, slug |
| fontFamilies | array | | fontFace, fontFamily, name, slug |

---
Expand Down
194 changes: 194 additions & 0 deletions lib/block-supports/typography.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,200 @@ function gutenberg_typography_get_css_variable_inline_style( $attributes, $featu
return sprintf( '%s:var(--wp--preset--%s--%s);', $css_property, $css_property, $slug );
}

/**
* Internal method that checks a string for a unit and value and returns an array consisting of `'value'` and `'unit'`, e.g., [ '42', 'rem' ].
*
* @access private
*
* @param string $raw_value Raw size value from theme.json.
* @param array $options array(
* 'coerce_to' => (string) Coerce the value to rem or px. Default `'rem'`.
* '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' ]
*/
function gutenberg_get_typography_value_and_unit( $raw_value, $options = array() ) {
if ( empty( $raw_value ) ) {
return null;
}

$defaults = array(
'coerce_to' => '',
'root_size_value' => 16,
'acceptable_units' => array( 'rem', 'px', 'em' ),
);

$options = wp_parse_args( $options, $defaults );

$acceptable_units_group = implode( '|', $options['acceptable_units'] );
$pattern = '/^(\d*\.?\d+)(' . $acceptable_units_group . '){1,1}$/';

preg_match( $pattern, $raw_value, $matches );

// We need a number value and a px or rem unit.
if ( ! isset( $matches[1] ) || ! isset( $matches[2] ) ) {
return null;
}

$value = $matches[1];
$unit = $matches[2];

// Default browser font size. Later we could inject some JS to compute this `getComputedStyle( document.querySelector( "html" ) ).fontSize`.
if ( 'px' === $options['coerce_to'] && ( 'em' === $unit || 'rem' === $unit ) ) {
$value = $value * $options['root_size_value'];
$unit = $options['coerce_to'];
}

if ( 'px' === $unit && ( 'em' === $options['coerce_to'] || 'rem' === $options['coerce_to'] ) ) {
$value = $value / $options['root_size_value'];
$unit = $options['coerce_to'];
}

return array(
'value' => $value,
'unit' => $unit,
);
}

/**
* Internal implementation of clamp() based on available min/max viewport width, and min/max font sizes.
*
* @access private
*
* @param array $args array(
* 'maximum_viewport_width' => (string) Maximum size up to which type will have fluidity.
* 'minimum_viewport_width' => (string) Minimum viewport size from which type will have fluidity.
* 'maximum_font_size' => (string) Maximum font size for any clamp() calculation.
* 'minimum_font_size' => (string) Minimum font size for any clamp() calculation.
* 'scale_factor' => (number) A scale factor to determine how fast a font scales within boundaries.
* );.
* @return string|null A font-size value using clamp().
*/
function gutenberg_get_computed_fluid_typography_value( $args = array() ) {
$maximum_viewport_width_raw = isset( $args['maximum_viewport_width'] ) ? $args['maximum_viewport_width'] : null;
$minimum_viewport_width_raw = isset( $args['minimum_viewport_width'] ) ? $args['minimum_viewport_width'] : null;
$maximum_font_size_raw = isset( $args['maximum_font_size'] ) ? $args['maximum_font_size'] : null;
$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.
$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.
$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.
$maximum_font_size = gutenberg_get_typography_value_and_unit(
$maximum_font_size_raw,
array(
'coerce_to' => $font_size_unit,
)
);

// Protect against unsupported units.
if ( ! $maximum_font_size || ! $minimum_font_size ) {
return null;
}

// Use rem for accessible fluid target font scaling.
$minimum_font_size_rem = gutenberg_get_typography_value_and_unit(
$minimum_font_size_raw,
array(
'coerce_to' => 'rem',
)
);

// Viewport widths defined for fluid typography. Normalize units.
$maximum_viewport_width = gutenberg_get_typography_value_and_unit(
$maximum_viewport_width_raw,
array(
'coerce_to' => $font_size_unit,
)
);
$minimum_viewport_width = gutenberg_get_typography_value_and_unit(
$minimum_viewport_width_raw,
array(
'coerce_to' => $font_size_unit,
)
);

// Build CSS rule.
// 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)";

return "clamp($minimum_font_size_raw, $fluid_target_font_size, $maximum_font_size_raw)";
}

/**
* 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 array $preset fontSizes preset value as seen in theme.json.
* @param boolean $should_use_fluid_typography An override to switch fluid typography "on". Can be used for unit testing.
* @return string Font-size value.
*/
function gutenberg_get_typography_font_size_value( $preset, $should_use_fluid_typography = false ) {
// Check 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;

if ( ! $should_use_fluid_typography ) {
return $preset['size'];
}

// Defaults.
$default_maximum_viewport_width = '1600px';
$default_minimum_viewport_width = '768px';
$default_minimum_font_size_factor = 0.75;
$default_maximum_font_size_factor = 1.5;
$default_scale_factor = 1;

// Font sizes.
$fluid_font_size_settings = isset( $preset['fluid'] ) ? $preset['fluid'] : null;

// Try to grab explicit min and max fluid font sizes.
$minimum_font_size_raw = isset( $fluid_font_size_settings['min'] ) ? $fluid_font_size_settings['min'] : null;
$maximum_font_size_raw = isset( $fluid_font_size_settings['max'] ) ? $fluid_font_size_settings['max'] : null;

// Font sizes.
$preferred_size = gutenberg_get_typography_value_and_unit( $preset['size'] );

// Protect against unsupported units.
if ( empty( $preferred_size['unit'] ) ) {
return $preset['size'];
}

// If no fluid min or max font sizes are available, create some using min/max font size factors.
if ( ! $minimum_font_size_raw ) {
$minimum_font_size_raw = ( $preferred_size['value'] * $default_minimum_font_size_factor ) . $preferred_size['unit'];
}

if ( ! $maximum_font_size_raw ) {
$maximum_font_size_raw = ( $preferred_size['value'] * $default_maximum_font_size_factor ) . $preferred_size['unit'];
}

$fluid_font_size_value = gutenberg_get_computed_fluid_typography_value(
array(
'minimum_viewport_width' => $default_minimum_viewport_width,
'maximum_viewport_width' => $default_maximum_viewport_width,
'minimum_font_size' => $minimum_font_size_raw,
'maximum_font_size' => $maximum_font_size_raw,
'scale_factor' => $default_scale_factor,
)
);

if ( ! empty( $fluid_font_size_value ) ) {
return $fluid_font_size_value;
}

return $preset['size'];
}

// Register the block support.
WP_Block_Supports::get_instance()->register(
'typography',
Expand Down
3 changes: 2 additions & 1 deletion lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,7 @@ protected static function get_property_value( $styles, $path, $theme_json = null
'path' => array( 'typography', 'fontSizes' ),
'prevent_override' => false,
'use_default_names' => true,
'value_key' => 'size',
'value_func' => 'gutenberg_get_typography_font_size_value',
'css_vars' => '--wp--preset--font-size--$slug',
'classes' => array( '.has-$slug-font-size' => 'font-size' ),
'properties' => array( 'font-size' ),
Expand Down Expand Up @@ -1129,6 +1129,7 @@ protected static function get_property_value( $styles, $path, $theme_json = null
'units' => null,
),
'typography' => array(
'fluid' => null,
'customFontSize' => null,
'dropCap' => null,
'fontFamilies' => null,
Expand Down
93 changes: 92 additions & 1 deletion phpunit/block-supports/typography-test.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function test_typography_with_skipped_serialization_block_supports() {
}

function test_letter_spacing_with_individual_skipped_serialization_block_supports() {
$this->test_block_name = 'test/letter-spacing-with-individua-skipped-serialization-block-supports';
$this->test_block_name = 'test/letter-spacing-with-individual-skipped-serialization-block-supports';
register_block_type(
$this->test_block_name,
array(
Expand Down Expand Up @@ -207,4 +207,95 @@ function test_font_family_with_class() {

$this->assertSame( $expected, $actual );
}

/**
* Tests generating font size values, including fluid formulae, from fontSizes preset.
*
* @dataProvider data_generate_font_size_preset_fixtures
*/
function test_gutenberg_get_typography_font_size_value( $font_size_preset, $should_use_fluid_typography, $expected_output ) {
$actual = gutenberg_get_typography_font_size_value( $font_size_preset, $should_use_fluid_typography );

$this->assertSame( $expected_output, $actual );
}

/**
* Data provider.
*
* @return array
*/
public function data_generate_font_size_preset_fixtures() {
return array(
'default_return_value' => array(
'font_size_preset' => array(
'size' => '28px',
),
'should_use_fluid_typography' => false,
'expected_output' => '28px',
),

'return_fluid_value' => array(
'font_size_preset' => array(
'size' => '1.75rem',
),
'should_use_fluid_typography' => true,
'expected_output' => 'clamp(1.3125rem, 1.3125rem + ((1vw - 0.48rem) * 2.524), 2.625rem)',
),

'return_default_fluid_values_with_empty_fluidSize' => array(
'font_size_preset' => array(
'size' => '28px',
'fluid' => array(),
),
'should_use_fluid_typography' => true,
'expected_output' => 'clamp(21px, 1.3125rem + ((1vw - 7.68px) * 2.524), 42px)',
),

'return_size_with_invalid_fluid_units' => array(
'font_size_preset' => array(
'size' => '10em',
'fluid' => array(
'min' => '20vw',
'max' => '50%',
),
),
'should_use_fluid_typography' => true,
'expected_output' => '10em',
),

'return_fluid_clamp_value' => array(
'font_size_preset' => array(
'size' => '28px',
'fluid' => array(
'min' => '20px',
'max' => '50rem',
),
),
'should_use_fluid_typography' => true,
'expected_output' => 'clamp(20px, 1.25rem + ((1vw - 7.68px) * 93.75), 50rem)',
),

'return_clamp_value_with_default_fluid_max_value' => array(
'font_size_preset' => array(
'size' => '28px',
'fluid' => array(
'min' => '2.6rem',
),
),
'should_use_fluid_typography' => true,
'expected_output' => 'clamp(2.6rem, 2.6rem + ((1vw - 0.48rem) * 0.048), 42px)',
),

'default_return_clamp_value_with_default_fluid_min_value' => array(
'font_size_preset' => array(
'size' => '28px',
'fluid' => array(
'max' => '80px',
),
),
'should_use_fluid_typography' => true,
'expected_output' => 'clamp(21px, 1.3125rem + ((1vw - 7.68px) * 7.091), 80px)',
),
);
}
}
3 changes: 3 additions & 0 deletions schemas/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@

## Unreleased

- Add new properties `settings.typography.fluid` and `settings.typography.fontSizes[n].fluidSize` to theme.json to enable fluid typography ([#39529](https://github.com/WordPress/gutenberg/pull/39529)).


Initial release.
18 changes: 18 additions & 0 deletions schemas/json/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,10 @@
"type": "boolean",
"default": true
},
"fluid": {
"description": "Opts into fluid typography.",
"type": "boolean"
},
"letterSpacing": {
"description": "Allow users to set custom letter spacing.",
"type": "boolean",
Expand Down Expand Up @@ -358,6 +362,20 @@
"size": {
"description": "CSS font-size value, including units.",
"type": "string"
},
"fluid": {
"type": "object",
"properties": {
"min": {
"description": "A min font size for fluid font size calculations in px, rem or em.",
"type": "string"
},
"max": {
"description": "A max font size for fluid font size calculations in px, rem or em.",
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
Expand Down

0 comments on commit d2418af

Please sign in to comment.