diff --git a/packages/components/src/unit-control/stories/index.js b/packages/components/src/unit-control/stories/index.js index b9bc97934abba6..2605bc4b4c03b9 100644 --- a/packages/components/src/unit-control/stories/index.js +++ b/packages/components/src/unit-control/stories/index.js @@ -20,6 +20,10 @@ export default { component: UnitControl, }; +const ControlWrapperView = styled.div` + max-width: 80px; +`; + function Example() { const [ value, setValue ] = useState( '10px' ); @@ -60,6 +64,43 @@ export const _default = () => { return ; }; -const ControlWrapperView = styled.div` - max-width: 80px; -`; +export function WithCustomUnits() { + const [ value, setValue ] = useState( '10km' ); + + const props = { + isResetValueOnUnitChange: boolean( 'isResetValueOnUnitChange', true ), + label: text( 'label', 'Distance' ), + units: object( 'units', [ + { + value: 'km', + label: 'km', + default: 1, + }, + { + value: 'mi', + label: 'mi', + default: 1, + }, + { + value: 'm', + label: 'm', + default: 1000, + }, + { + value: 'yd', + label: 'yd', + default: 1760, + }, + ] ), + }; + + return ( + + setValue( v ) } + /> + + ); +} diff --git a/packages/components/src/unit-control/test/utils.js b/packages/components/src/unit-control/test/utils.js index f30b9dcbb99b33..ae4b01048e5c40 100644 --- a/packages/components/src/unit-control/test/utils.js +++ b/packages/components/src/unit-control/test/utils.js @@ -64,6 +64,15 @@ describe( 'UnitControl utils', () => { filterUnitsWithSettings( preferredUnits, availableUnits ) ).toEqual( [] ); } ); + + it( 'should return empty array where available units is set to false', () => { + const preferredUnits = [ '%', 'px' ]; + const availableUnits = false; + + expect( + filterUnitsWithSettings( preferredUnits, availableUnits ) + ).toEqual( [] ); + } ); } ); describe( 'getValidParsedUnit', () => { diff --git a/packages/components/src/unit-control/types.ts b/packages/components/src/unit-control/types.ts new file mode 100644 index 00000000000000..712ce659fae8c1 --- /dev/null +++ b/packages/components/src/unit-control/types.ts @@ -0,0 +1,26 @@ +export type Value = number | string; + +export type WPUnitControlUnit = { + /** + * The value for the unit, used in a CSS value (e.g `px`). + */ + value: string; + /** + * The label used in a dropdown selector for the unit. + */ + label: string; + /** + * Default value for the unit, used when switching units. + */ + default?: Value; + /** + * An accessible label used by screen readers. + */ + a11yLabel?: string; + /** + * A step value used when incrementing/decrementing the value. + */ + step?: number; +}; + +export type WPUnitControlUnitList = Array< WPUnitControlUnit > | false; diff --git a/packages/components/src/unit-control/utils.js b/packages/components/src/unit-control/utils.ts similarity index 62% rename from packages/components/src/unit-control/utils.js rename to packages/components/src/unit-control/utils.ts index cfe61235a5d26a..f24925e304a139 100644 --- a/packages/components/src/unit-control/utils.js +++ b/packages/components/src/unit-control/utils.ts @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { isEmpty } from 'lodash'; - /** * WordPress dependencies */ @@ -10,20 +5,13 @@ import { __, _x } from '@wordpress/i18n'; import { Platform } from '@wordpress/element'; /** - * An object containing the details of a unit. - * - * @typedef {Object} WPUnitControlUnit - * @property {string} value The value for the unit, used in a CSS value (e.g `px`). - * @property {string} label The label used in a dropdown selector for the unit. - * @property {string|number} [default] Default value for the unit, used when switching units. - * @property {string} [a11yLabel] An accessible label used by screen readers. - * @property {number} [step] A step value used when incrementing/decrementing the value. + * Internal dependencies */ +import type { Value, WPUnitControlUnit, WPUnitControlUnitList } from './types'; const isWeb = Platform.OS === 'web'; -/** @type {Record} */ -const allUnits = { +const allUnits: Record< string, WPUnitControlUnit > = { px: { value: 'px', label: isWeb ? 'px' : __( 'Pixels (px)' ), @@ -157,12 +145,16 @@ export const DEFAULT_UNIT = allUnits.px; * Moving forward, ideally the value should be a string that contains both * the value and unit, example: '10px' * - * @param {number|string} value Value - * @param {string} unit Unit value - * @param {Array} units Units to derive from. - * @return {Array} The extracted number and unit. + * @param value Value + * @param unit Unit value + * @param units Units to derive from. + * @return The extracted number and unit. */ -export function getParsedValue( value, unit, units ) { +export function getParsedValue( + value: Value, + unit?: string, + units?: WPUnitControlUnitList +): [ Value, string ] { const initialValue = unit ? `${ value }${ unit }` : value; return parseUnit( initialValue, units ); @@ -171,24 +163,27 @@ export function getParsedValue( value, unit, units ) { /** * Checks if units are defined. * - * @param {any} units Units to check. - * @return {boolean} Whether units are defined. + * @param units Units to check. + * @return Whether units are defined. */ -export function hasUnits( units ) { - return ! isEmpty( units ) && units !== false; +export function hasUnits( units: WPUnitControlUnitList ): boolean { + return Array.isArray( units ) && !! units.length; } /** * Parses a number and unit from a value. * - * @param {string} initialValue Value to parse - * @param {Array} units Units to derive from. - * @return {Array} The extracted number and unit. + * @param initialValue Value to parse + * @param units Units to derive from. + * @return The extracted number and unit. */ -export function parseUnit( initialValue, units = ALL_CSS_UNITS ) { +export function parseUnit( + initialValue: Value, + units: WPUnitControlUnitList = ALL_CSS_UNITS +): [ Value, string ] { const value = String( initialValue ).trim(); - let num = parseFloat( value, 10 ); + let num: Value = parseFloat( value ); num = isNaN( num ) ? '' : num; const unitMatch = value.match( /[\d.\-\+]*\s*(.*)/ )[ 1 ]; @@ -196,7 +191,7 @@ export function parseUnit( initialValue, units = ALL_CSS_UNITS ) { let unit = unitMatch !== undefined ? unitMatch : ''; unit = unit.toLowerCase(); - if ( hasUnits( units ) ) { + if ( hasUnits( units ) && units !== false ) { const match = units.find( ( item ) => item.value === unit ); unit = match?.value; } else { @@ -210,18 +205,25 @@ export function parseUnit( initialValue, units = ALL_CSS_UNITS ) { * Parses a number and unit from a value. Validates parsed value, using fallback * value if invalid. * - * @param {number|string} next The next value. - * @param {Array} units Units to derive from. - * @param {number|string} fallbackValue The fallback value. - * @param {string} fallbackUnit The fallback value. - * @return {Array} The extracted number and unit. + * @param next The next value. + * @param units Units to derive from. + * @param fallbackValue The fallback value. + * @param fallbackUnit The fallback value. + * @return The extracted value and unit. */ -export function getValidParsedUnit( next, units, fallbackValue, fallbackUnit ) { +export function getValidParsedUnit( + next: Value, + units: WPUnitControlUnitList, + fallbackValue: Value, + fallbackUnit: string +) { const [ parsedValue, parsedUnit ] = parseUnit( next, units ); let baseValue = parsedValue; - let baseUnit; + let baseUnit: string; - if ( isNaN( parsedValue ) || parsedValue === '' ) { + // The parsed value from `parseUnit` should now be either a + // real number or an empty string. If not, use the fallback value. + if ( ! Number.isFinite( parsedValue ) || parsedValue === '' ) { baseValue = fallbackValue; } @@ -242,26 +244,31 @@ export function getValidParsedUnit( next, units, fallbackValue, fallbackUnit ) { * Takes a unit value and finds the matching accessibility label for the * unit abbreviation. * - * @param {string} unit Unit value (example: px) - * @return {string} a11y label for the unit abbreviation + * @param unit Unit value (example: px) + * @return a11y label for the unit abbreviation */ -export function parseA11yLabelForUnit( unit ) { +export function parseA11yLabelForUnit( unit: string ): string { const match = ALL_CSS_UNITS.find( ( item ) => item.value === unit ); return match?.a11yLabel ? match?.a11yLabel : match?.value; } /** - * Filters available units based on values defined by settings. + * Filters available units based on values defined by the unit setting/property. * - * @param {Array} settings Collection of preferred units. - * @param {Array} units Collection of available units. + * @param unitSetting Collection of preferred unit value strings. + * @param units Collection of available unit objects. * - * @return {Array} Filtered units based on settings. + * @return Filtered units based on settings. */ -export function filterUnitsWithSettings( settings = [], units = [] ) { - return units.filter( ( unit ) => { - return settings.includes( unit.value ); - } ); +export function filterUnitsWithSettings( + unitSetting: Array< string > = [], + units: WPUnitControlUnitList +): Array< WPUnitControlUnit > { + return Array.isArray( units ) + ? units.filter( ( unit ) => { + return unitSetting.includes( unit.value ); + } ) + : []; } /** @@ -269,14 +276,22 @@ export function filterUnitsWithSettings( settings = [], units = [] ) { * TODO: ideally this hook shouldn't be needed * https://github.com/WordPress/gutenberg/pull/31822#discussion_r633280823 * - * @param {Object} args An object containing units, settingPath & defaultUnits. - * @param {Array|undefined} args.units Collection of available units. - * @param {Array|undefined} args.availableUnits The setting path. Defaults to 'spacing.units'. - * @param {Object|undefined} args.defaultValues Collection of default values for defined units. Example: { px: '350', em: '15' }. + * @param args An object containing units, settingPath & defaultUnits. + * @param args.units Collection of all potentially available units. + * @param args.availableUnits Collection of unit value strings for filtering available units. + * @param args.defaultValues Collection of default values for defined units. Example: { px: '350', em: '15' }. * - * @return {Array|boolean} Filtered units based on settings. + * @return Filtered units based on settings. */ -export const useCustomUnits = ( { units, availableUnits, defaultValues } ) => { +export const useCustomUnits = ( { + units, + availableUnits, + defaultValues, +}: { + units?: WPUnitControlUnitList; + availableUnits?: Array< string >; + defaultValues: Record< string, Value >; +} ): WPUnitControlUnitList => { units = units || ALL_CSS_UNITS; const usedUnits = filterUnitsWithSettings( ! availableUnits ? [] : availableUnits, @@ -302,17 +317,17 @@ export const useCustomUnits = ( { units, availableUnits, defaultValues } ) => { * accurately displayed in the UI, even if the intention is to hide * the availability of that unit. * - * @param {number|string} currentValue Selected value to parse. - * @param {string} legacyUnit Legacy unit value, if currentValue needs it appended. - * @param {Array} units List of available units. + * @param currentValue Selected value to parse. + * @param legacyUnit Legacy unit value, if currentValue needs it appended. + * @param units List of available units. * - * @return {Array} A collection of units containing the unit for the current value. + * @return A collection of units containing the unit for the current value. */ export function getUnitsWithCurrentUnit( - currentValue, - legacyUnit, - units = ALL_CSS_UNITS -) { + currentValue: Value, + legacyUnit: string | undefined, + units: Array< WPUnitControlUnit > | false = ALL_CSS_UNITS +): WPUnitControlUnitList { if ( ! Array.isArray( units ) ) { return units; }