Skip to content

Commit

Permalink
Block Support: Update border support UI (#31585)
Browse files Browse the repository at this point in the history
Refine the controls provided via the border block support feature.
  • Loading branch information
aaronrobertshaw authored and sarayourfriend committed Jul 15, 2021
1 parent ede5e74 commit 803dffa
Show file tree
Hide file tree
Showing 21 changed files with 557 additions and 162 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* WordPress dependencies
*/
import { __experimentalUnitControl as UnitControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { getAllValue, hasMixedValues, hasDefinedValues } from './utils';

export default function AllInputControl( { onChange, values, ...props } ) {
const allValue = getAllValue( values );
const hasValues = hasDefinedValues( values );
const isMixed = hasValues && hasMixedValues( values );
const allPlaceholder = isMixed ? __( 'Mixed' ) : null;

return (
<UnitControl
{ ...props }
aria-label={ __( 'Border radius' ) }
disableUnits={ isMixed }
isOnly
value={ allValue }
onChange={ onChange }
placeholder={ allPlaceholder }
/>
);
}
102 changes: 102 additions & 0 deletions packages/block-editor/src/components/border-radius-control/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* WordPress dependencies
*/
import {
RangeControl,
__experimentalParseUnit as parseUnit,
__experimentalUseCustomUnits as useCustomUnits,
} from '@wordpress/components';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import AllInputControl from './all-input-control';
import InputControls from './input-controls';
import LinkedButton from './linked-button';
import {
getAllValue,
getAllUnit,
hasDefinedValues,
hasMixedValues,
} from './utils';

const DEFAULT_VALUES = {
topLeft: null,
topRight: null,
bottomLeft: null,
bottomRight: null,
};
const MIN_BORDER_RADIUS_VALUE = 0;
const MAX_BORDER_RADIUS_VALUES = {
px: 100,
em: 20,
rem: 20,
};

/**
* Control to display border radius options.
*
* @param {Object} props Component props.
* @param {Function} props.onChange Callback to handle onChange.
* @param {Object} props.values Border radius values.
*
* @return {WPElement} Custom border radius control.
*/
export default function BorderRadiusControl( { onChange, values } ) {
const [ isLinked, setIsLinked ] = useState(
! hasDefinedValues( values ) || ! hasMixedValues( values )
);

const units = useCustomUnits( { availableUnits: [ 'px', 'em', 'rem' ] } );
const unit = getAllUnit( values );
const unitConfig = units.find( ( item ) => item.value === unit );
const step = unitConfig?.step || 1;

const [ allValue ] = parseUnit( getAllValue( values ) );

const toggleLinked = () => setIsLinked( ! isLinked );

const handleSliderChange = ( next ) => {
onChange( next !== undefined ? `${ next }${ unit }` : undefined );
};

return (
<fieldset className="components-border-radius-control">
<legend>{ __( 'Radius' ) }</legend>
<div className="components-border-radius-control__wrapper">
{ isLinked ? (
<>
<AllInputControl
className="components-border-radius-control__unit-control"
values={ values }
min={ MIN_BORDER_RADIUS_VALUE }
onChange={ onChange }
unit={ unit }
units={ units }
/>
<RangeControl
className="components-border-radius-control__range-control"
value={ allValue }
min={ MIN_BORDER_RADIUS_VALUE }
max={ MAX_BORDER_RADIUS_VALUES[ unit ] }
initialPosition={ 0 }
withInputField={ false }
onChange={ handleSliderChange }
step={ step }
/>
</>
) : (
<InputControls
min={ MIN_BORDER_RADIUS_VALUE }
onChange={ onChange }
values={ values || DEFAULT_VALUES }
units={ units }
/>
) }
<LinkedButton onClick={ toggleLinked } isLinked={ isLinked } />
</div>
</fieldset>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* WordPress dependencies
*/
import { __experimentalUnitControl as UnitControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

const CORNERS = {
topLeft: __( 'Top left' ),
topRight: __( 'Top right' ),
bottomLeft: __( 'Bottom left' ),
bottomRight: __( 'Bottom right' ),
};

export default function BoxInputControls( {
onChange,
values: valuesProp,
...props
} ) {
const createHandleOnChange = ( corner ) => ( next ) => {
if ( ! onChange ) {
return;
}

onChange( {
...values,
[ corner ]: next ? next : undefined,
} );
};

// For shorthand style & backwards compatibility, handle flat string value.
const values =
typeof valuesProp !== 'string'
? valuesProp
: {
topLeft: valuesProp,
topRight: valuesProp,
bottomLeft: valuesProp,
bottomRight: valuesProp,
};

// Controls are wrapped in tooltips as visible labels aren't desired here.
return (
<div className="components-border-radius-control__input-controls-wrapper">
{ Object.entries( CORNERS ).map( ( [ key, label ] ) => (
<UnitControl
{ ...props }
key={ key }
aria-label={ label }
value={ values[ key ] }
onChange={ createHandleOnChange( key ) }
/>
) ) }
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* WordPress dependencies
*/
import { Button, Tooltip } from '@wordpress/components';
import { link, linkOff } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';

export default function LinkedButton( { isLinked, ...props } ) {
const label = isLinked ? __( 'Unlink Radii' ) : __( 'Link Radii' );

return (
<Tooltip text={ label }>
<Button
{ ...props }
className="component-border-radius-control__linked-button"
isPrimary={ isLinked }
isSecondary={ ! isLinked }
isSmall
icon={ isLinked ? link : linkOff }
iconSize={ 16 }
aria-label={ label }
/>
</Tooltip>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.components-border-radius-control {
margin-bottom: $grid-unit-15;

legend {
padding-bottom: $grid-unit-05;
}

.components-border-radius-control__wrapper {
display: flex;
justify-content: space-between;
align-items: flex-start;

> .components-unit-control-wrapper {
width: calc(50% - 26px);
margin-bottom: 0;
}

.components-range-control {
width: calc(50% - 26px);
margin-bottom: 0;

.components-base-control__field {
margin-bottom: 0;
height: 30px;
}

.components-range-control__wrapper {
margin-right: 10px;
}
}

> span {
flex: 0 0 auto;
}
}

.components-border-radius-control__input-controls-wrapper {
display: flex;
width: 70%;
flex-wrap: wrap;

.components-unit-control-wrapper {
width: calc(50% - #{ $grid-unit-10 });
margin-bottom: $grid-unit-10;
margin-right: $grid-unit-10;
}
}

.component-border-radius-control__linked-button.has-icon {
display: flex;
justify-content: center;

svg {
margin-right: 0;
}
}
}
112 changes: 112 additions & 0 deletions packages/block-editor/src/components/border-radius-control/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* WordPress dependencies
*/
import { __experimentalParseUnit as parseUnit } from '@wordpress/components';

/**
* Gets the item with the highest occurrence within an array
* https://stackoverflow.com/a/20762713
*
* @param {Array<any>} arr Array of items to check.
* @return {any} The item with the most occurrences.
*/
function mode( arr ) {
return arr
.sort(
( a, b ) =>
arr.filter( ( v ) => v === a ).length -
arr.filter( ( v ) => v === b ).length
)
.pop();
}

/**
* Returns the most common CSS unit in the radius values.
*
* @param {Object|string} values Radius values.
* @return {string} Most common CSS unit in values.
*/
export function getAllUnit( values = {} ) {
if ( typeof values === 'string' ) {
const [ , unit ] = parseUnit( values );
return unit || 'px';
}

const allUnits = Object.values( values ).map( ( value ) => {
const [ , unit ] = parseUnit( value );
return unit;
} );

return mode( allUnits );
}

/**
* Gets the 'all' input value and unit from values data.
*
* @param {Object|string} values Radius values.
* @return {string} A value + unit for the 'all' input.
*/
export function getAllValue( values = {} ) {
/**
* Border radius support was originally a single pixel value.
*
* To maintain backwards compatibility treat this case as the all value.
*/
if ( typeof values === 'string' ) {
return values;
}

const parsedValues = Object.values( values ).map( ( value ) =>
parseUnit( value )
);

const allValues = parsedValues.map( ( value ) => value[ 0 ] );
const allUnits = parsedValues.map( ( value ) => value[ 1 ] );

const value = allValues.every( ( v ) => v === allValues[ 0 ] )
? allValues[ 0 ]
: '';
const unit = mode( allUnits );

const allValue = value === 0 || value ? `${ value }${ unit }` : null;

return allValue;
}

/**
* Checks to determine if values are mixed.
*
* @param {Object} values Radius values.
* @return {boolean} Whether values are mixed.
*/
export function hasMixedValues( values = {} ) {
const allValue = getAllValue( values );
const isMixed = isNaN( parseFloat( allValue ) );

return isMixed;
}

/**
* Checks to determine if values are defined.
*
* @param {Object} values Radius values.
* @return {boolean} Whether values are mixed.
*/
export function hasDefinedValues( values ) {
if ( ! values ) {
return false;
}

// A string value represents a shorthand value.
if ( typeof values === 'string' ) {
return true;
}

// An object represents longhand border radius values, if any are set
// flag values as being defined.
const filteredValues = Object.values( values ).filter( ( value ) => {
return !! value || value === 0;
} );

return !! filteredValues.length;
}
Loading

0 comments on commit 803dffa

Please sign in to comment.