-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Block Support: Update border support UI #31585
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
Changes from all commits
b0cb09c
a20bdea
848f735
1231dac
8ed4bf1
6500f68
9f01170
665d006
e0b2e9e
0eb0480
dbba840
5021f2d
6ab923d
81874aa
af4dfd5
d08a3ae
c3355b1
9bb70c1
67e6dc8
f99657f
bc0be14
be6c12a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 } | ||
| /> | ||
| ); | ||
| } |
| 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' ] } ); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to use the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| } | ||
| } |
| 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; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.