diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index 57fac0b7e95432..fdbf14d6b9379b 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -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 | --- diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index 00ea2b17ac178e..382b897fefcf87 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -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', diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php index 5fc0dd1223ba62..1679e87c5a8a92 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php @@ -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' ), @@ -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, diff --git a/phpunit/block-supports/typography-test.php b/phpunit/block-supports/typography-test.php index bc5655ebb69856..956acd40f3c308 100644 --- a/phpunit/block-supports/typography-test.php +++ b/phpunit/block-supports/typography-test.php @@ -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( @@ -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)', + ), + ); + } } diff --git a/schemas/CHANGELOG.md b/schemas/CHANGELOG.md index e04ce921cdfdc4..51d33d49707c27 100644 --- a/schemas/CHANGELOG.md +++ b/schemas/CHANGELOG.md @@ -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. diff --git a/schemas/json/theme.json b/schemas/json/theme.json index c699cd78424b90..a218947b92f4a4 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -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", @@ -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