-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make custom font sizes appear fluid in the block editor when fluid ty…
…pography is enabled (#44765) * Make custom font sizes appear fluid in the block editor when fluid typography is enabled * Add tests for fluid utils * update description * You shall not pass with a number, well, yes, but we'll coerce it to `px` and the tests shall pass nonetheless!!! Co-authored-by: Ben Dwyer <[email protected]> Co-authored-by: ramonjd <[email protected]>
- Loading branch information
1 parent
6a2d304
commit b70b8b8
Showing
7 changed files
with
437 additions
and
185 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
218 changes: 218 additions & 0 deletions
218
packages/block-editor/src/components/font-sizes/fluid-utils.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
/** | ||
* The fluid utilities must match the backend equivalent. | ||
* See: gutenberg_get_typography_font_size_value() in lib/block-supports/typography.php | ||
* --------------------------------------------------------------- | ||
*/ | ||
|
||
// Defaults. | ||
const DEFAULT_MAXIMUM_VIEWPORT_WIDTH = '1600px'; | ||
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; | ||
|
||
/** | ||
* Computes a fluid font-size value that uses clamp(). A minimum and maxinmum | ||
* font size OR a single font size can be specified. | ||
* | ||
* If a single font size is specified, it is scaled up and down by | ||
* minimumFontSizeFactor and maximumFontSizeFactor to arrive at the minimum and | ||
* maximum sizes. | ||
* | ||
* @example | ||
* ```js | ||
* // Calculate fluid font-size value from a minimum and maximum value. | ||
* const fontSize = getComputedFluidTypographyValue( { | ||
* minimumFontSize: '20px', | ||
* maximumFontSize: '45px' | ||
* } ); | ||
* // Calculate fluid font-size value from a single font size. | ||
* const fontSize = getComputedFluidTypographyValue( { | ||
* fontSize: '30px', | ||
* } ); | ||
* ``` | ||
* | ||
* @param {Object} args | ||
* @param {?string} args.minimumViewPortWidth Minimum viewport size from which type will have fluidity. Optional if fontSize is specified. | ||
* @param {?string} args.maximumViewPortWidth Maximum size up to which type will have fluidity. Optional if fontSize is specified. | ||
* @param {?string} args.fontSize Size to derive maximumFontSize and minimumFontSize from, if necessary. Optional if minimumFontSize and maximumFontSize are specified. | ||
* @param {?string} args.maximumFontSize Maximum font size for any clamp() calculation. Optional. | ||
* @param {?string} args.minimumFontSize Minimum font size for any clamp() calculation. Optional. | ||
* @param {?number} args.scaleFactor A scale factor to determine how fast a font scales within boundaries. Optional. | ||
* @param {?number} args.minimumFontSizeFactor How much to scale defaultFontSize by to derive minimumFontSize. Optional. | ||
* @param {?number} args.maximumFontSizeFactor How much to scale defaultFontSize by to derive maximumFontSize. Optional. | ||
* | ||
* @return {string|null} A font-size value using clamp(). | ||
*/ | ||
export function getComputedFluidTypographyValue( { | ||
minimumFontSize, | ||
maximumFontSize, | ||
fontSize, | ||
minimumViewPortWidth = DEFAULT_MINIMUM_VIEWPORT_WIDTH, | ||
maximumViewPortWidth = DEFAULT_MAXIMUM_VIEWPORT_WIDTH, | ||
scaleFactor = DEFAULT_SCALE_FACTOR, | ||
minimumFontSizeFactor = DEFAULT_MINIMUM_FONT_SIZE_FACTOR, | ||
maximumFontSizeFactor = DEFAULT_MAXIMUM_FONT_SIZE_FACTOR, | ||
} ) { | ||
// Calculate missing minimumFontSize and maximumFontSize from | ||
// defaultFontSize if provided. | ||
if ( fontSize && ( ! minimumFontSize || ! maximumFontSize ) ) { | ||
// Parse default font size. | ||
const fontSizeParsed = getTypographyValueAndUnit( fontSize ); | ||
|
||
// Protect against invalid units. | ||
if ( ! fontSizeParsed?.unit ) { | ||
return null; | ||
} | ||
|
||
// If no minimumFontSize is provided, derive using min scale factor. | ||
if ( ! minimumFontSize ) { | ||
minimumFontSize = | ||
fontSizeParsed.value * minimumFontSizeFactor + | ||
fontSizeParsed.unit; | ||
} | ||
|
||
// If no maximumFontSize is provided, derive using max scale factor. | ||
if ( ! maximumFontSize ) { | ||
maximumFontSize = | ||
fontSizeParsed.value * maximumFontSizeFactor + | ||
fontSizeParsed.unit; | ||
} | ||
} | ||
|
||
// Return early if one of the provided inputs is not provided. | ||
if ( ! minimumFontSize || ! maximumFontSize ) { | ||
return null; | ||
} | ||
|
||
// 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; | ||
} | ||
|
||
if ( typeof rawValue === 'number' && ! Number.isNaN( rawValue ) ) { | ||
rawValue = `${ rawValue }px`; | ||
} | ||
|
||
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
77 changes: 77 additions & 0 deletions
77
packages/block-editor/src/components/font-sizes/test/fluid-utils.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { logged } from '@wordpress/deprecated'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { getComputedFluidTypographyValue } from '../fluid-utils'; | ||
|
||
describe( 'getComputedFluidTypographyValue()', () => { | ||
afterEach( () => { | ||
for ( const key in logged ) { | ||
delete logged[ key ]; | ||
} | ||
} ); | ||
|
||
it( 'should return a fluid font size when given a min and max font size', () => { | ||
const fluidTypographyValues = getComputedFluidTypographyValue( { | ||
minimumFontSize: '20px', | ||
maximumFontSize: '45px', | ||
} ); | ||
expect( fluidTypographyValues ).toBe( | ||
'clamp(20px, 1.25rem + ((1vw - 7.68px) * 3.005), 45px)' | ||
); | ||
} ); | ||
|
||
it( 'should return a fluid font size when given a font size', () => { | ||
const fluidTypographyValues = getComputedFluidTypographyValue( { | ||
fontSize: '30px', | ||
} ); | ||
expect( fluidTypographyValues ).toBe( | ||
'clamp(22.5px, 1.40625rem + ((1vw - 7.68px) * 2.704), 45px)' | ||
); | ||
} ); | ||
|
||
it( 'should return a fluid font size based on px when given a numerical font size', () => { | ||
const fluidTypographyValues = getComputedFluidTypographyValue( { | ||
fontSize: '30px', | ||
} ); | ||
expect( fluidTypographyValues ).toBe( | ||
'clamp(22.5px, 1.40625rem + ((1vw - 7.68px) * 2.704), 45px)' | ||
); | ||
} ); | ||
|
||
it( 'should return a fluid font size when given a min and max viewport width', () => { | ||
const fluidTypographyValues = getComputedFluidTypographyValue( { | ||
fontSize: '30px', | ||
minimumViewPortWidth: '500px', | ||
maximumViewPortWidth: '1000px', | ||
} ); | ||
expect( fluidTypographyValues ).toBe( | ||
'clamp(22.5px, 1.40625rem + ((1vw - 5px) * 4.5), 45px)' | ||
); | ||
} ); | ||
|
||
it( 'should return a fluid font size when given a scale factor', () => { | ||
const fluidTypographyValues = getComputedFluidTypographyValue( { | ||
fontSize: '30px', | ||
scaleFactor: '2', | ||
} ); | ||
expect( fluidTypographyValues ).toBe( | ||
'clamp(22.5px, 1.40625rem + ((1vw - 7.68px) * 5.408), 45px)' | ||
); | ||
} ); | ||
|
||
it( 'should return a fluid font size when given a min and max font size factor', () => { | ||
const fluidTypographyValues = getComputedFluidTypographyValue( { | ||
fontSize: '30px', | ||
minimumFontSizeFactor: '0.5', | ||
maximumFontSizeFactor: '2', | ||
} ); | ||
expect( fluidTypographyValues ).toBe( | ||
'clamp(15px, 0.9375rem + ((1vw - 7.68px) * 5.409), 60px)' | ||
); | ||
} ); | ||
} ); |
Oops, something went wrong.