Skip to content

Commit

Permalink
BoxControl: Add support for presets
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad committed Dec 10, 2024
1 parent 207dfe3 commit d72909d
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 40 deletions.
15 changes: 15 additions & 0 deletions packages/components/src/box-control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,18 @@ The current values of the control, expressed as an object of `top`, `right`, `bo

- Type: `BoxControlValue`
- Required: No

### `presets`

The list of presets to pick from.

- Type: `Preset`
- Required: No

### `presetKey`

The key of the preset to apply. If you provide a list of presets, you must provide a preset key to use. The format of preset selected values is going to be `var:preset|${ presetKey }|${ presetSlug }`

- Type: `string`
- Required: No

4 changes: 4 additions & 0 deletions packages/components/src/box-control/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ function BoxControl( {
splitOnAxis = false,
allowReset = true,
resetValues = DEFAULT_VALUES,
presets,
presetKey,
onMouseOver,
onMouseOut,
}: BoxControlProps ) {
Expand Down Expand Up @@ -153,6 +155,8 @@ function BoxControl( {
sides,
values: inputValues,
__next40pxDefaultSize,
presets,
presetKey,
};

maybeWarnDeprecated36pxSize( {
Expand Down
181 changes: 141 additions & 40 deletions packages/components/src/box-control/input-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
*/
import { useInstanceId } from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { settings } from '@wordpress/icons';

/**
* Internal dependencies
Expand All @@ -11,10 +13,13 @@ import Tooltip from '../tooltip';
import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils';
import {
CUSTOM_VALUE_SETTINGS,
getAllowedSides,
getMergedValue,
isValueMixed,
getAllowedSides,
getPresetIndexFromValue,
getPresetValueFromIndex,
isValuePreset,
isValuesDefined,
isValueMixed,
LABELS,
} from './utils';
import {
Expand All @@ -24,6 +29,7 @@ import {
StyledUnitControl,
} from './styles/box-control-styles';
import type { BoxControlInputControlProps, BoxControlValue } from './types';
import Button from '../button';

const noop = () => {};

Expand Down Expand Up @@ -78,6 +84,8 @@ export default function BoxInputControl( {
setSelectedUnits,
sides,
side,
presets,
presetKey,
...props
}: BoxControlInputControlProps ) {
const defaultValuesToModify = getSidesToModify( side, sides );
Expand All @@ -90,6 +98,15 @@ export default function BoxInputControl( {
onChange( nextValues );
};

const handleRewOnValueChange = ( next?: string ) => {
const nextValues = { ...values };
defaultValuesToModify.forEach( ( modifiedSide ) => {
nextValues[ modifiedSide ] = next;
} );

handleOnChange( nextValues );
};

const handleOnValueChange = (
next?: string,
extra?: { event: React.SyntheticEvent< Element, Event > }
Expand Down Expand Up @@ -147,51 +164,135 @@ export default function BoxInputControl( {
const usedValue =
mergedValue === undefined && computedUnit ? computedUnit : mergedValue;
const mixedPlaceholder = isMixed || isMixedUnit ? __( 'Mixed' ) : undefined;
const hasPresets = presets && presets.length > 0 && presetKey;
const hasPresetValue =
hasPresets &&
mergedValue !== undefined &&
! isMixed &&
isValuePreset( mergedValue, presetKey );
const [ showCustomValueControl, setShowCustomValueControl ] = useState(
! hasPresets ||
( ! hasPresetValue && ! isMixed && mergedValue !== undefined )
);
const showRangeControl = true;
const presetIndex = hasPresetValue
? getPresetIndexFromValue( mergedValue, presetKey, presets )
: undefined;
const marks = hasPresets
? [ { value: 0, label: __( 'None' ) } ].concat(
presets.map( ( preset, index ) => ( {
value: index + 1,
label: preset.name,
} ) )
)
: [];

return (
<InputWrapper key={ `box-control-${ side }` } expanded>
<FlexedBoxControlIcon side={ side } sides={ sides } />
<Tooltip placement="top-end" text={ LABELS[ side ] }>
<StyledUnitControl
{ ...props }
__shouldNotWarnDeprecated36pxSize
__next40pxDefaultSize={ __next40pxDefaultSize }
className="component-box-control__unit-control"
id={ inputId }
isPressEnterToChange
disableUnits={ isMixed || isMixedUnit }
value={ usedValue }
onChange={ handleOnValueChange }
onUnitChange={ handleOnUnitChange }
onFocus={ handleOnFocus }
{ showCustomValueControl && (
<>
<Tooltip placement="top-end" text={ LABELS[ side ] }>
<StyledUnitControl
{ ...props }
__shouldNotWarnDeprecated36pxSize
__next40pxDefaultSize={ __next40pxDefaultSize }
className="component-box-control__unit-control"
id={ inputId }
isPressEnterToChange
disableUnits={ isMixed || isMixedUnit }
value={ usedValue }
onChange={ handleOnValueChange }
onUnitChange={ handleOnUnitChange }
onFocus={ handleOnFocus }
label={ LABELS[ side ] }
placeholder={ mixedPlaceholder }
hideLabelFromVision
/>
</Tooltip>

<FlexedRangeControl
__nextHasNoMarginBottom
__next40pxDefaultSize={ __next40pxDefaultSize }
__shouldNotWarnDeprecated36pxSize
aria-controls={ inputId }
label={ LABELS[ side ] }
hideLabelFromVision
onChange={ ( newValue ) => {
handleOnValueChange(
newValue !== undefined
? [ newValue, computedUnit ].join( '' )
: undefined
);
} }
min={ 0 }
max={
CUSTOM_VALUE_SETTINGS[ computedUnit ?? 'px' ]
?.max ?? 10
}
step={
CUSTOM_VALUE_SETTINGS[ computedUnit ?? 'px' ]
?.step ?? 0.1
}
value={ parsedQuantity ?? 0 }
withInputField={ false }
/>
</>
) }

{ hasPresets && ! showCustomValueControl && showRangeControl && (
<FlexedRangeControl
__next40pxDefaultSize
className="spacing-sizes-control__range-control"
value={ presetIndex !== undefined ? presetIndex + 1 : 0 }
onChange={ ( newIndex ) => {
const newValue =
newIndex === 0 || newIndex === undefined
? undefined
: getPresetValueFromIndex(
newIndex - 1,
presetKey,
presets
);
handleRewOnValueChange( newValue );
} }
withInputField={ false }
aria-valuenow={
presetIndex !== undefined ? presetIndex + 1 : 0
}
aria-valuetext={
marks[ presetIndex !== undefined ? presetIndex + 1 : 0 ]
.label
}
renderTooltipContent={ ( index ) =>
marks[ ! index ? 0 : index ].label
}
min={ 0 }
max={ marks.length - 1 }
marks={ marks }
label={ LABELS[ side ] }
placeholder={ mixedPlaceholder }
hideLabelFromVision
__nextHasNoMarginBottom
/>
) }

{ hasPresets && (
<Button
label={
showCustomValueControl
? __( 'Use size preset' )
: __( 'Set custom size' )
}
icon={ settings }
onClick={ () => {
setShowCustomValueControl( ! showCustomValueControl );
} }
isPressed={ showCustomValueControl }
size="small"
className="spacing-sizes-control__custom-toggle"
iconSize={ 24 }
/>
</Tooltip>

<FlexedRangeControl
__nextHasNoMarginBottom
__next40pxDefaultSize={ __next40pxDefaultSize }
__shouldNotWarnDeprecated36pxSize
aria-controls={ inputId }
label={ LABELS[ side ] }
hideLabelFromVision
onChange={ ( newValue ) => {
handleOnValueChange(
newValue !== undefined
? [ newValue, computedUnit ].join( '' )
: undefined
);
} }
min={ 0 }
max={ CUSTOM_VALUE_SETTINGS[ computedUnit ?? 'px' ]?.max ?? 10 }
step={
CUSTOM_VALUE_SETTINGS[ computedUnit ?? 'px' ]?.step ?? 0.1
}
value={ parsedQuantity ?? 0 }
withInputField={ false }
/>
) }
</InputWrapper>
);
}
12 changes: 12 additions & 0 deletions packages/components/src/box-control/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,15 @@ AxialControlsWithSingleSide.args = {
sides: [ 'horizontal' ],
splitOnAxis: true,
};

export const ControlWithPresets = TemplateControlled.bind( {} );
ControlWithPresets.args = {
...Default.args,
presets: [
{ name: 'Small', slug: 'small', value: '4px' },
{ name: 'Medium', slug: 'medium', value: '8px' },
{ name: 'Large', slug: 'large', value: '12px' },
{ name: 'Extra Large', slug: 'extra-large', value: '16px' },
],
presetKey: 'padding',
};
18 changes: 18 additions & 0 deletions packages/components/src/box-control/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export type CustomValueUnits = {
[ key: string ]: { max: number; step: number };
};

export interface Preset {
name: string;
slug: string;
value: string;
}

type UnitControlPassthroughProps = Omit<
UnitControlProps,
'label' | 'onChange' | 'onFocus' | 'units'
Expand Down Expand Up @@ -94,6 +100,16 @@ export type BoxControlProps = Pick< UnitControlProps, 'units' > &
* @default false
*/
__next40pxDefaultSize?: boolean;
/**
* Available presets to pick from.
*/
presets?: Preset[];
/**
* The key of the preset to apply.
* If you provide a list of presets, you must provide a preset key to use.
* The format of preset selected values is going to be `var:preset|${ presetKey }|${ presetSlug }`
*/
presetKey?: string;
};

export type BoxControlInputControlProps = UnitControlPassthroughProps & {
Expand All @@ -120,6 +136,8 @@ export type BoxControlInputControlProps = UnitControlPassthroughProps & {
* It can be a concrete side like: left, right, top, bottom or a combined one like: horizontal, vertical.
*/
side: keyof typeof LABELS;
presets?: Preset[];
presetKey?: string;
};

export type BoxControlIconProps = {
Expand Down
57 changes: 57 additions & 0 deletions packages/components/src/box-control/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
BoxControlProps,
BoxControlValue,
CustomValueUnits,
Preset,
} from './types';
import deprecated from '@wordpress/deprecated';

Expand Down Expand Up @@ -272,3 +273,59 @@ export function getAllowedSides(
} );
return allowedSides;
}

/**
* Checks if a value is a preset value.
*
* @param value The value to check.
* @param presetKey The preset key to check against.
* @return Whether the value is a preset value.
*/
export function isValuePreset( value: string, presetKey: string ) {
return value.startsWith( `var:preset|${ presetKey }|` );
}

/**
* Returns the index of the preset value in the presets array.
*
* @param value The value to check.
* @param presetKey The preset key to check against.
* @param presets The array of presets to search.
* @return The index of the preset value in the presets array.
*/
export function getPresetIndexFromValue(
value: string,
presetKey: string,
presets: Preset[]
) {
if ( ! isValuePreset( value, presetKey ) === undefined ) {
return undefined;
}

const match = value.match(
new RegExp( `^var:preset\\|${ presetKey }\\|(.+)$` )
);
const slug = match ? match[ 1 ] : undefined;
const index = presets.findIndex( ( preset ) => {
return preset.slug === slug;
} );

return index !== -1 ? index : undefined;
}

/**
* Returns the preset value from the index.
*
* @param index The index of the preset value in the presets array.
* @param presetKey The preset key to check against.
* @param presets The array of presets to search.
* @return The preset value from the index.
*/
export function getPresetValueFromIndex(
index: number,
presetKey: string,
presets: Preset[]
) {
const preset = presets[ index ];
return `var:preset|${ presetKey }|${ preset.slug }`;
}

0 comments on commit d72909d

Please sign in to comment.