diff --git a/packages/block-library/src/separator/block.json b/packages/block-library/src/separator/block.json index 2983a93a86190a..ef486f22170052 100644 --- a/packages/block-library/src/separator/block.json +++ b/packages/block-library/src/separator/block.json @@ -8,6 +8,9 @@ }, "customColor": { "type": "string" + }, + "style": { + "type": "object" } }, "supports": { diff --git a/packages/block-library/src/separator/edit.js b/packages/block-library/src/separator/edit.js index f611abde42a62b..8e951a4e1daa75 100644 --- a/packages/block-library/src/separator/edit.js +++ b/packages/block-library/src/separator/edit.js @@ -6,29 +6,59 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { HorizontalRule } from '@wordpress/components'; +import { + HorizontalRule, + __experimentalBoxControl as BoxControl, +} from '@wordpress/components'; +import { View } from '@wordpress/primitives'; import { withColors, useBlockProps } from '@wordpress/block-editor'; + /** * Internal dependencies */ import SeparatorSettings from './separator-settings'; +import { MARGIN_CONSTRAINTS, parseUnit } from './shared'; + +const { __Visualizer: BoxControlVisualizer } = BoxControl; + +function SeparatorEdit( props ) { + const { + color, + attributes: { style }, + } = props; + + const { top, bottom } = style?.spacing?.margin || {}; + const marginUnit = parseUnit( top || bottom ); + const blockProps = useBlockProps(); -function SeparatorEdit( { color, setColor, className } ) { return ( <> - + + - + marginTop: top || MARGIN_CONSTRAINTS[ marginUnit ].min, + marginBottom: + bottom || MARGIN_CONSTRAINTS[ marginUnit ].min, + } } + /> + + ); } diff --git a/packages/block-library/src/separator/edit.native.js b/packages/block-library/src/separator/edit.native.js new file mode 100644 index 00000000000000..e42d73b663170c --- /dev/null +++ b/packages/block-library/src/separator/edit.native.js @@ -0,0 +1,48 @@ +/** + * WordPress dependencies + */ +import { HorizontalRule, useConvertUnitToMobile } from '@wordpress/components'; +import { withColors, useBlockProps } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import SeparatorSettings from './separator-settings'; +import { MARGIN_CONSTRAINTS, parseUnit } from './shared'; + +function SeparatorEdit( props ) { + const { + color, + attributes: { style }, + } = props; + + const { top, bottom } = style?.spacing?.margin || {}; + const marginUnit = parseUnit( top || bottom ); + + const convertedMarginTop = useConvertUnitToMobile( + parseFloat( top || 0 ) || MARGIN_CONSTRAINTS[ marginUnit ].min, + marginUnit + ); + + const convertedMarginBottom = useConvertUnitToMobile( + parseFloat( bottom || 0 ) || MARGIN_CONSTRAINTS[ marginUnit ].min, + marginUnit + ); + + return ( + <> + + + + ); +} + +export default withColors( 'color', { textColor: 'color' } )( SeparatorEdit ); diff --git a/packages/block-library/src/separator/editor.scss b/packages/block-library/src/separator/editor.scss index 24e940684279e8..f8e155a43e6e8d 100644 --- a/packages/block-library/src/separator/editor.scss +++ b/packages/block-library/src/separator/editor.scss @@ -1,5 +1,7 @@ -.block-editor-block-list__block[data-type="core/separator"] { - // Prevent margin collapsing so the area to select the separator is bigger. - padding-top: 0.1px; - padding-bottom: 0.1px; +.wp-block-separator-wrapper { + display: flex; + + .wp-block-separator { + width: 100%; + } } diff --git a/packages/block-library/src/separator/save.js b/packages/block-library/src/separator/save.js index 67d489bd611c3c..f913385b0b9e90 100644 --- a/packages/block-library/src/separator/save.js +++ b/packages/block-library/src/separator/save.js @@ -27,6 +27,8 @@ export default function separatorSave( { attributes } ) { const style = { backgroundColor: backgroundClass ? undefined : customColor, color: colorClass ? undefined : customColor, + marginBottom: attributes.style?.spacing?.margin?.bottom, + marginTop: attributes.style?.spacing?.margin?.top, }; return
; diff --git a/packages/block-library/src/separator/separator-settings.js b/packages/block-library/src/separator/separator-settings.js index 320cfc862b93bd..f0df3e5ce3cf48 100644 --- a/packages/block-library/src/separator/separator-settings.js +++ b/packages/block-library/src/separator/separator-settings.js @@ -3,20 +3,74 @@ */ import { __ } from '@wordpress/i18n'; import { InspectorControls, PanelColorSettings } from '@wordpress/block-editor'; +import { + PanelBody, + __experimentalBoxControl as BoxControl, +} from '@wordpress/components'; -const SeparatorSettings = ( { color, setColor } ) => ( - - { + const updateMargins = ( { top, bottom } ) => { + setAttributes( { + style: { + ...style, + spacing: { + ...style?.spacing, + margin: { top, bottom }, }, - ] } - > - -); + }, + } ); + }; + + const onChangeShowVisualizer = ( { top, bottom } ) => { + setAttributes( { + style: { + ...style, + visualizers: { + margin: { top, bottom }, + }, + }, + } ); + }; + + return ( + + + + + + + ); +}; export default SeparatorSettings; diff --git a/packages/block-library/src/separator/separator-settings.native.js b/packages/block-library/src/separator/separator-settings.native.js index d2bdd8ef6443a3..0f7295e486802b 100644 --- a/packages/block-library/src/separator/separator-settings.native.js +++ b/packages/block-library/src/separator/separator-settings.native.js @@ -1,3 +1,106 @@ -// Mobile has no separator settings at this time, so render nothing -const SeparatorSettings = () => null; +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { InspectorControls, PanelColorSettings } from '@wordpress/block-editor'; +import { PanelBody, UnitControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { CSS_UNITS, MARGIN_CONSTRAINTS, parseUnit } from './shared'; + +const SeparatorSettings = ( { + color, + setColor, + attributes, + setAttributes, +} ) => { + const { style } = attributes; + const { top, bottom } = style?.spacing?.margin || {}; + + const topUnit = parseUnit( top ); + const bottomUnit = parseUnit( bottom ); + const topValue = top + ? parseFloat( top ) + : MARGIN_CONSTRAINTS[ topUnit ].min; + const bottomValue = bottom + ? parseFloat( bottom ) + : MARGIN_CONSTRAINTS[ bottomUnit ].min; + + const updateMargins = ( margins ) => { + setAttributes( { + style: { + ...style, + spacing: { + ...style?.spacing, + margin: margins, + }, + }, + } ); + }; + + const createHandleMarginChange = ( side, unit ) => ( value ) => { + updateMargins( { + ...style?.spacing?.margin, + [ side ]: `${ value }${ unit }`, + } ); + }; + + const onUnitChange = ( unit ) => { + updateMargins( { + top: MARGIN_CONSTRAINTS[ unit ].default, + bottom: MARGIN_CONSTRAINTS[ unit ].default, + } ); + }; + + return ( + + + + + + + + ); +}; + export default SeparatorSettings; diff --git a/packages/block-library/src/separator/shared.js b/packages/block-library/src/separator/shared.js new file mode 100644 index 00000000000000..7f3ae05fdf0908 --- /dev/null +++ b/packages/block-library/src/separator/shared.js @@ -0,0 +1,72 @@ +/** + * WordPress dependencies + */ +import { Platform } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +// Separator margin related constants. +export const MIN_PX_MARGIN = 15; +export const MIN_EM_MARGIN = 0.75; +export const MIN_REM_MARGIN = 0.7; +export const MAX_PX_MARGIN = 300; +export const MAX_EM_MARGIN = 20; +export const MAX_REM_MARGIN = 20; + +const isWeb = Platform.OS === 'web'; + +/** + * Available CSS units for specifying separator block margins. + */ +export const CSS_UNITS = [ + { value: 'px', label: isWeb ? 'px' : __( 'Pixels (px)' ), default: 0 }, + { + value: 'em', + label: isWeb ? 'em' : __( 'Relative to parent font size (em)' ), + default: 0, + }, + { + value: 'rem', + label: isWeb ? 'rem' : __( 'Relative to root font size (rem)' ), + default: 0, + }, +]; + +/** + * Separator margin constraints for available CSS units. + */ +export const MARGIN_CONSTRAINTS = { + px: { + min: MIN_PX_MARGIN, + max: MAX_PX_MARGIN, + default: `${ MIN_PX_MARGIN }px`, + }, + em: { + min: MIN_EM_MARGIN, + max: MAX_EM_MARGIN, + default: '1em', + }, + rem: { + min: MIN_REM_MARGIN, + max: MAX_REM_MARGIN, + default: '1rem', + }, +}; + +/** + * Extracts CSS unit from string. + * + * @param { string } cssValue CSS string containing unit and value. + * @return { string } CSS unit. Defaults to 'px'. + */ +export const parseUnit = ( cssValue ) => { + if ( ! cssValue ) { + return 'px'; + } + + const matches = cssValue.trim().match( /[\d.\-+]*\s*([a-zA-Z]*)$/ ); + if ( ! matches ) { + return 'px'; + } + const [ , unit ] = matches; + return ( unit || 'px' ).toLowerCase(); +}; diff --git a/packages/components/src/box-control/all-input-control.js b/packages/components/src/box-control/all-input-control.js index 1f1260898505d1..8c2772b62988de 100644 --- a/packages/components/src/box-control/all-input-control.js +++ b/packages/components/src/box-control/all-input-control.js @@ -14,6 +14,7 @@ export default function AllInputControl( { onHoverOn = noop, onHoverOff = noop, values, + sides, ...props } ) { const allValue = getAllValue( values ); @@ -26,13 +27,15 @@ export default function AllInputControl( { onFocus( event, { side: 'all' } ); }; + const isSideDisabled = ( side ) => sides && sides[ side ] === false; + const handleOnChange = ( next ) => { const nextValues = { ...values }; - nextValues.top = next; - nextValues.bottom = next; - nextValues.left = next; - nextValues.right = next; + nextValues.top = isSideDisabled( 'top' ) ? values.top : next; + nextValues.right = isSideDisabled( 'right' ) ? values.right : next; + nextValues.bottom = isSideDisabled( 'bottom' ) ? values.bottom : next; + nextValues.left = isSideDisabled( 'left' ) ? values.left : next; onChange( nextValues ); }; diff --git a/packages/components/src/box-control/icon.js b/packages/components/src/box-control/icon.js index 15249c01e4db13..e34ea1d326dee9 100644 --- a/packages/components/src/box-control/icon.js +++ b/packages/components/src/box-control/icon.js @@ -15,12 +15,22 @@ const BASE_ICON_SIZE = 24; export default function BoxControlIcon( { size = 24, side = 'all', + sides, ...props } ) { - const top = getSide( side, 'top' ); - const right = getSide( side, 'right' ); - const bottom = getSide( side, 'bottom' ); - const left = getSide( side, 'left' ); + const isSideDisabled = ( value ) => sides && sides[ value ] === false; + const getSide = ( value ) => { + if ( isSideDisabled( value ) ) { + return false; + } + + return side === 'all' || side === value; + }; + + const top = getSide( 'top' ); + const right = getSide( 'right' ); + const bottom = getSide( 'bottom' ); + const left = getSide( 'left' ); // Simulates SVG Icon scaling const scale = size / BASE_ICON_SIZE; @@ -36,7 +46,3 @@ export default function BoxControlIcon( { ); } - -function getSide( side, value ) { - return side === 'all' || side === value; -} diff --git a/packages/components/src/box-control/index.js b/packages/components/src/box-control/index.js index 187e8f10214011..6e1f8be2f81576 100644 --- a/packages/components/src/box-control/index.js +++ b/packages/components/src/box-control/index.js @@ -51,6 +51,7 @@ export default function BoxControl( { label = __( 'Box Control' ), values: valuesProp, units, + sides, } ) { const [ values, setValues ] = useControlledState( valuesProp, { fallback: DEFAULT_VALUES, @@ -107,6 +108,7 @@ export default function BoxControl( { onHoverOff: handleOnHoverOff, isLinked, units, + sides, values: inputValues, }; @@ -135,7 +137,7 @@ export default function BoxControl( { - + { isLinked && ( diff --git a/packages/components/src/box-control/input-controls.js b/packages/components/src/box-control/input-controls.js index 0177052151273f..85d47bb93d7668 100644 --- a/packages/components/src/box-control/input-controls.js +++ b/packages/components/src/box-control/input-controls.js @@ -10,12 +10,38 @@ import UnitControl from './unit-control'; import { LABELS } from './utils'; import { LayoutContainer, Layout } from './styles/box-control-styles'; +const getFirstLastAndOnlySides = ( sides ) => { + // Handle default config allowing all sides to be configured. + if ( ! sides ) { + return { first: 'top', last: 'left', only: undefined }; + } + + let first, last; + + // Check which sides have been opted-into to determine which are first, + // last & only. + [ 'top', 'right', 'bottom', 'left' ].forEach( ( side ) => { + if ( sides && sides[ side ] ) { + if ( ! first ) { + first = side; + } + + last = side; + } + } ); + + const only = first === last && first; + + return { first, last, only }; +}; + export default function BoxInputControls( { onChange = noop, onFocus = noop, onHoverOn = noop, onHoverOff = noop, values, + sides, ...props } ) { const createHandleOnFocus = ( side ) => ( event ) => { @@ -66,6 +92,9 @@ export default function BoxInputControls( { handleOnChange( nextValues ); }; + const isSideDisabled = ( side ) => sides && sides[ side ] === false; + const { first, last, only } = getFirstLastAndOnlySides( sides ); + return ( - - - - + { ! isSideDisabled( 'top' ) && ( + + ) } + { ! isSideDisabled( 'right' ) && ( + + ) } + { ! isSideDisabled( 'bottom' ) && ( + + ) } + { ! isSideDisabled( 'left' ) && ( + + ) } ); diff --git a/packages/components/src/box-control/styles/box-control-styles.js b/packages/components/src/box-control/styles/box-control-styles.js index 78cf6ceb9a6968..7d42aa42dcb186 100644 --- a/packages/components/src/box-control/styles/box-control-styles.js +++ b/packages/components/src/box-control/styles/box-control-styles.js @@ -40,6 +40,7 @@ export const Layout = styled( Flex )` position: relative; height: 100%; width: 100%; + justify-content: flex-start; `; const unitControlBorderRadiusStyles = ( { isFirst, isLast, isOnly } ) => { diff --git a/packages/components/src/input-control/input-field.js b/packages/components/src/input-control/input-field.js index 18044ee455ac9c..f70bd019052904 100644 --- a/packages/components/src/input-control/input-field.js +++ b/packages/components/src/input-control/input-field.js @@ -155,6 +155,9 @@ function InputField( const dragGestureProps = useDrag( ( dragProps ) => { const { distance, dragging, event } = dragProps; + // The event is persisted to prevent errors in components using this + // to check if a modifier key was held while dragging. + event.persist(); if ( ! distance ) return; event.stopPropagation(); diff --git a/packages/primitives/src/horizontal-rule/index.native.js b/packages/primitives/src/horizontal-rule/index.native.js index 853b57e76d0c1a..3d338ad7d4ba7f 100644 --- a/packages/primitives/src/horizontal-rule/index.native.js +++ b/packages/primitives/src/horizontal-rule/index.native.js @@ -2,6 +2,7 @@ * External dependencies */ import Hr from 'react-native-hr'; +import { View } from 'react-native'; /** * WordPress dependencies @@ -13,16 +14,30 @@ import { withPreferredColorScheme } from '@wordpress/compose'; */ import styles from './styles.scss'; -const HR = ( { getStylesFromColorScheme, ...props } ) => { +const HR = ( { getStylesFromColorScheme, style, ...props } ) => { const lineStyle = getStylesFromColorScheme( styles.line, styles.lineDark ); + const customBackground = style?.backgroundColor + ? { backgroundColor: style.backgroundColor } + : {}; return ( -
+ +
+
); };