@gechiui/components
Version:
UI components for GeChiUI.
354 lines (324 loc) • 9.17 kB
text/typescript
/**
* GeChiUI dependencies
*/
import { __, _x } from '@gechiui/i18n';
import { Platform } from '@gechiui/element';
/**
* Internal dependencies
*/
import type { Value, GCUnitControlUnit, GCUnitControlUnitList } from './types';
const isWeb = Platform.OS === 'web';
const allUnits: Record< string, GCUnitControlUnit > = {
px: {
value: 'px',
label: isWeb ? 'px' : __( 'Pixels (px)' ),
default: '',
a11yLabel: __( 'Pixels (px)' ),
step: 1,
},
'%': {
value: '%',
label: isWeb ? '%' : __( 'Percentage (%)' ),
default: '',
a11yLabel: __( 'Percent (%)' ),
step: 0.1,
},
em: {
value: 'em',
label: isWeb ? 'em' : __( 'Relative to parent font size (em)' ),
default: '',
a11yLabel: _x( 'ems', 'Relative to parent font size (em)' ),
step: 0.01,
},
rem: {
value: 'rem',
label: isWeb ? 'rem' : __( 'Relative to root font size (rem)' ),
default: '',
a11yLabel: _x( 'rems', 'Relative to root font size (rem)' ),
step: 0.01,
},
vw: {
value: 'vw',
label: isWeb ? 'vw' : __( 'Viewport width (vw)' ),
default: '',
a11yLabel: __( 'Viewport width (vw)' ),
step: 0.1,
},
vh: {
value: 'vh',
label: isWeb ? 'vh' : __( 'Viewport height (vh)' ),
default: '',
a11yLabel: __( 'Viewport height (vh)' ),
step: 0.1,
},
vmin: {
value: 'vmin',
label: isWeb ? 'vmin' : __( 'Viewport smallest dimension (vmin)' ),
default: '',
a11yLabel: __( 'Viewport smallest dimension (vmin)' ),
step: 0.1,
},
vmax: {
value: 'vmax',
label: isWeb ? 'vmax' : __( 'Viewport largest dimension (vmax)' ),
default: '',
a11yLabel: __( 'Viewport largest dimension (vmax)' ),
step: 0.1,
},
ch: {
value: 'ch',
label: isWeb ? 'ch' : __( 'Width of the zero (0) character (ch)' ),
default: '',
a11yLabel: __( 'Width of the zero (0) character (ch)' ),
step: 0.01,
},
ex: {
value: 'ex',
label: isWeb ? 'ex' : __( 'x-height of the font (ex)' ),
default: '',
a11yLabel: __( 'x-height of the font (ex)' ),
step: 0.01,
},
cm: {
value: 'cm',
label: isWeb ? 'cm' : __( 'Centimeters (cm)' ),
default: '',
a11yLabel: __( 'Centimeters (cm)' ),
step: 0.001,
},
mm: {
value: 'mm',
label: isWeb ? 'mm' : __( 'Millimeters (mm)' ),
default: '',
a11yLabel: __( 'Millimeters (mm)' ),
step: 0.1,
},
in: {
value: 'in',
label: isWeb ? 'in' : __( 'Inches (in)' ),
default: '',
a11yLabel: __( 'Inches (in)' ),
step: 0.001,
},
pc: {
value: 'pc',
label: isWeb ? 'pc' : __( 'Picas (pc)' ),
default: '',
a11yLabel: __( 'Picas (pc)' ),
step: 1,
},
pt: {
value: 'pt',
label: isWeb ? 'pt' : __( 'Points (pt)' ),
default: '',
a11yLabel: __( 'Points (pt)' ),
step: 1,
},
};
/**
* An array of all available CSS length units.
*/
export const ALL_CSS_UNITS = Object.values( allUnits );
/**
* Units of measurements. `a11yLabel` is used by screenreaders.
*/
export const CSS_UNITS = [
allUnits.px,
allUnits[ '%' ],
allUnits.em,
allUnits.rem,
allUnits.vw,
allUnits.vh,
];
export const DEFAULT_UNIT = allUnits.px;
/**
* Handles legacy value + unit handling.
* This component use to manage both incoming value and units separately.
*
* Moving forward, ideally the value should be a string that contains both
* the value and unit, example: '10px'
*
* @param value Value
* @param unit Unit value
* @param units Units to derive from.
* @return The extracted number and unit.
*/
export function getParsedValue(
value: Value,
unit?: string,
units?: GCUnitControlUnitList
): [ Value, string | undefined ] {
const initialValue = unit ? `${ value }${ unit }` : value;
return parseUnit( initialValue, units );
}
/**
* Checks if units are defined.
*
* @param units Units to check.
* @return Whether units are defined.
*/
export function hasUnits( units: GCUnitControlUnitList ): boolean {
return Array.isArray( units ) && !! units.length;
}
/**
* Parses a number and unit from a value.
*
* @param initialValue Value to parse
* @param units Units to derive from.
* @return The extracted number and unit.
*/
export function parseUnit(
initialValue: Value | undefined,
units: GCUnitControlUnitList = ALL_CSS_UNITS
): [ Value, string | undefined ] {
const value = String( initialValue ).trim();
let num: Value = parseFloat( value );
num = isNaN( num ) ? '' : num;
const unitMatch = value.match( /[\d.\-\+]*\s*(.*)/ );
let unit: string | undefined =
unitMatch?.[ 1 ] !== undefined ? unitMatch[ 1 ] : '';
unit = unit.toLowerCase();
if ( hasUnits( units ) && units !== false ) {
const match = units.find( ( item ) => item.value === unit );
unit = match?.value;
} else {
unit = DEFAULT_UNIT.value;
}
return [ num, unit ];
}
/**
* Parses a number and unit from a value. Validates parsed value, using fallback
* value if invalid.
*
* @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: Value | undefined,
units: GCUnitControlUnitList,
fallbackValue: Value,
fallbackUnit: string | undefined
): [ Value, string | undefined ] {
const [ parsedValue, parsedUnit ] = parseUnit( next, units );
let baseValue = parsedValue;
let baseUnit: string | undefined;
// 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;
}
baseUnit = parsedUnit || fallbackUnit;
/**
* If no unit is found, attempt to use the first value from the collection
* of units as a default fallback.
*/
if ( Array.isArray( units ) && hasUnits( units ) && ! baseUnit ) {
baseUnit = units[ 0 ]?.value;
}
return [ baseValue, baseUnit ];
}
/**
* Takes a unit value and finds the matching accessibility label for the
* unit abbreviation.
*
* @param unit Unit value (example: px)
* @return a11y label for the unit abbreviation
*/
export function parseA11yLabelForUnit( unit: string ): string | undefined {
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 the unit setting/property.
*
* @param unitSetting Collection of preferred unit value strings.
* @param units Collection of available unit objects.
*
* @return Filtered units based on settings.
*/
export function filterUnitsWithSettings(
unitSetting: Array< string > = [],
units: GCUnitControlUnitList
): Array< GCUnitControlUnit > {
return Array.isArray( units )
? units.filter( ( unit ) => {
return unitSetting.includes( unit.value );
} )
: [];
}
/**
* Custom hook to retrieve and consolidate units setting from add_theme_support().
* TODO: ideally this hook shouldn't be needed
* https://github.com/GeChiUI/gutenberg/pull/31822#discussion_r633280823
*
* @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 Filtered units based on settings.
*/
export const useCustomUnits = ( {
units,
availableUnits,
defaultValues,
}: {
units?: GCUnitControlUnitList;
availableUnits?: Array< string >;
defaultValues: Record< string, Value >;
} ): GCUnitControlUnitList => {
units = units || ALL_CSS_UNITS;
const usedUnits = filterUnitsWithSettings(
! availableUnits ? [] : availableUnits,
units
);
if ( defaultValues ) {
usedUnits.forEach( ( unit, i ) => {
if ( defaultValues[ unit.value ] ) {
usedUnits[ i ].default = defaultValues[ unit.value ];
}
} );
}
return usedUnits.length === 0 ? false : usedUnits;
};
/**
* Get available units with the unit for the currently selected value
* prepended if it is not available in the list of units.
*
* This is useful to ensure that the current value's unit is always
* accurately displayed in the UI, even if the intention is to hide
* the availability of that unit.
*
* @param currentValue Selected value to parse.
* @param legacyUnit Legacy unit value, if currentValue needs it appended.
* @param units List of available units.
*
* @return A collection of units containing the unit for the current value.
*/
export function getUnitsWithCurrentUnit(
currentValue: Value,
legacyUnit: string | undefined,
units: GCUnitControlUnitList = ALL_CSS_UNITS
): GCUnitControlUnitList {
if ( ! Array.isArray( units ) ) {
return units;
}
const unitsWithCurrentUnit = [ ...units ];
const [ , currentUnit ] = getParsedValue(
currentValue,
legacyUnit,
ALL_CSS_UNITS
);
if (
currentUnit &&
! unitsWithCurrentUnit.some( ( unit ) => unit.value === currentUnit )
) {
if ( allUnits[ currentUnit ] ) {
unitsWithCurrentUnit.unshift( allUnits[ currentUnit ] );
}
}
return unitsWithCurrentUnit;
}