Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block Support: Update border support UI #31585

Merged
merged 22 commits into from
Jul 7, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b0cb09c
Rename panel to border
aaronrobertshaw May 4, 2021
a20bdea
Remove repetitious border copy in labels
aaronrobertshaw May 4, 2021
848f735
Rearrange border controls to match CSS syntax order
aaronrobertshaw May 4, 2021
1231dac
Change border style to standard select control
aaronrobertshaw May 4, 2021
8ed4bf1
Rearrange controls to match CSS order
aaronrobertshaw May 5, 2021
6500f68
Use segmented control for border style control
aaronrobertshaw May 5, 2021
9f01170
Fix doc block for border style control
aaronrobertshaw May 5, 2021
665d006
Overhaul border radius control
aaronrobertshaw May 5, 2021
e0b2e9e
Update global styles sidebar border controls UI
aaronrobertshaw May 10, 2021
0eb0480
Move border style icons to icons package
aaronrobertshaw May 20, 2021
dbba840
Prevent empty string values for longhand border radii
aaronrobertshaw Jun 17, 2021
5021f2d
Switch to useCustomUnits and unit control parseUnit
aaronrobertshaw Jun 25, 2021
6ab923d
Fix border width step determination in global styles panel
aaronrobertshaw Jun 25, 2021
81874aa
Add aria labels and general polish to border radius control
aaronrobertshaw Jun 25, 2021
af4dfd5
Remove unnecessary lodash import
aaronrobertshaw Jul 5, 2021
d08a3ae
Remove import of noop
aaronrobertshaw Jul 5, 2021
c3355b1
Remove lodash use from utils
aaronrobertshaw Jul 5, 2021
9bb70c1
Improve util function naming
aaronrobertshaw Jul 5, 2021
67e6dc8
Use default units step values from UnitControl
aaronrobertshaw Jul 5, 2021
f99657f
Use default step values for border width
aaronrobertshaw Jul 7, 2021
bc0be14
Use default steps for border width in global styles
aaronrobertshaw Jul 7, 2021
be6c12a
Fix typos and indentation in comments
aaronrobertshaw Jul 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
ramonjd marked this conversation as resolved.
Show resolved Hide resolved
};
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' ] } );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to use the spacing.units or layout.units available via theme.json? I've noticed it's one of the few places where these aren't used so are not configurable by the theme.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

I can see theme developers wishing to allow different units for borders to those of spacing or layouts as well. A PR allowing configuration of border units can be found in #33315.

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( {
aaronrobertshaw marked this conversation as resolved.
Show resolved Hide resolved
...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 an items with the most occurrence within an array
aaronrobertshaw marked this conversation as resolved.
Show resolved Hide resolved
* https://stackoverflow.com/a/20762713
*
* @param {Array<any>} arr Array of items to check.
* @return {any} The item with the most occurrences.
aaronrobertshaw marked this conversation as resolved.
Show resolved Hide resolved
*/
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