From 5424ec0b063bd541b3a2c5602e08721dae6db310 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Thu, 14 Apr 2022 14:30:07 +1000 Subject: [PATCH] Borders: Use new border control components in block support (#37770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This updates border block support to utilise the new `BorderBoxControl` component and offer support for individual side border configuration. Co-authored-by: Ramon Co-authored-by: André <583546+oandregal@users.noreply.github.com> --- .../theme-json-reference/theme-json-living.md | 6 +- lib/block-supports/border.php | 72 ++- .../class-wp-theme-json-gutenberg.php | 92 ++++ .../border-radius-control/style.scss | 7 +- .../block-editor/src/hooks/border-color.js | 315 ----------- .../block-editor/src/hooks/border-style.js | 64 --- .../block-editor/src/hooks/border-width.js | 139 ----- packages/block-editor/src/hooks/border.js | 510 +++++++++++++++--- packages/block-editor/src/hooks/border.scss | 48 ++ packages/block-editor/src/hooks/index.js | 2 +- .../src/hooks/use-border-props.js | 47 +- packages/block-library/src/common.scss | 25 +- packages/block-library/src/search/edit.js | 20 +- packages/block-library/src/search/index.php | 117 ++-- packages/block-library/src/table/edit.js | 11 +- packages/block-library/src/table/editor.scss | 13 + packages/block-library/src/table/style.scss | 52 ++ packages/blocks/src/api/constants.js | 48 ++ .../components/global-styles/border-panel.js | 221 ++++---- .../src/components/sidebar/style.scss | 49 ++ phpunit/block-supports/border-test.php | 409 ++++++++++++-- schemas/json/theme.json | 83 ++- 22 files changed, 1518 insertions(+), 832 deletions(-) delete mode 100644 packages/block-editor/src/hooks/border-color.js delete mode 100644 packages/block-editor/src/hooks/border-style.js delete mode 100644 packages/block-editor/src/hooks/border-width.js diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index fd4c9c7113900..632bf534e5cd4 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -115,9 +115,13 @@ Border styles. | Property | Type | Props | | --- | --- |--- | | color | string | | -| radius | string | | +| radius | undefined | | | style | string | | | width | string | | +| top | undefined | | +| right | undefined | | +| bottom | undefined | | +| left | undefined | | --- diff --git a/lib/block-supports/border.php b/lib/block-supports/border.php index 5ff0260944df2..d4d18fd6cb687 100644 --- a/lib/block-supports/border.php +++ b/lib/block-supports/border.php @@ -38,7 +38,7 @@ function gutenberg_register_border_support( $block_type ) { * Adds CSS classes and inline styles for border styles to the incoming * attributes array. This will be applied to the block markup in the front-end. * - * @param WP_Block_type $block_type Block type. + * @param WP_Block_Type $block_type Block type. * @param array $block_attributes Block attributes. * * @return array Border CSS classes and inline styles. @@ -50,6 +50,10 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { $classes = array(); $styles = array(); + $sides = array( 'top', 'right', 'bottom', 'left' ); + + $has_border_color_support = gutenberg_has_border_feature_support( $block_type, 'color' ); + $has_border_width_support = gutenberg_has_border_feature_support( $block_type, 'width' ); // Border radius. if ( @@ -88,7 +92,7 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { // Border width. if ( - gutenberg_has_border_feature_support( $block_type, 'width' ) && + $has_border_width_support && isset( $block_attributes['style']['border']['width'] ) && ! gutenberg_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'width' ) ) { @@ -104,7 +108,7 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { // Border color. if ( - gutenberg_has_border_feature_support( $block_type, 'color' ) && + $has_border_color_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'color' ) ) { $has_named_border_color = array_key_exists( 'borderColor', $block_attributes ); @@ -122,6 +126,18 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { } } + // Generate styles for individual border sides. + if ( $has_border_color_support || $has_border_width_support ) { + foreach ( $sides as $side ) { + $border = _wp_array_get( $block_attributes, array( 'style', 'border', $side ), false ); + + if ( is_array( $border ) && ! empty( $border ) ) { + $split_border_styles = gutenberg_generate_individual_border_classes_and_styles( $side, $border, $block_type ); + $styles = array_merge( $styles, $split_border_styles ); + } + } + } + // Collect classes and styles. $attributes = array(); @@ -136,6 +152,56 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { return $attributes; } +/** + * Generates longhand CSS styles for an individual side border. + * + * If some values are omitted from the border configuration, using shorthand + * styles would lead to `initial` values being used instead of the more + * desirable inherited values. This could also lead to browser inconsistencies. + * + * @param string $side The side the styles are being generated for. + * @param array $border Array containing border color, style, and width values. + * @param WP_Block_Type $block_type Block type. + * + * @return array Longhand CSS border styles for a single side. + */ +function gutenberg_generate_individual_border_classes_and_styles( $side, $border, $block_type ) { + $styles = array(); + + if ( + isset( $border['width'] ) && + null !== $border['width'] && + ! gutenberg_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'width' ) + ) { + $styles[] = sprintf( 'border-%s-width: %s;', $side, $border['width'] ); + } + + if ( + isset( $border['style'] ) && + null !== $border['style'] && + ! gutenberg_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'style' ) + ) { + $styles[] = sprintf( 'border-%s-style: %s;', $side, $border['style'] ); + } + + $border_color = _wp_array_get( $border, array( 'color' ), null ); + + if ( + $border_color && + ! gutenberg_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'color' ) + ) { + $has_color_preset = strpos( $border_color, 'var:preset|color|' ) !== false; + if ( $has_color_preset ) { + $named_color_slug = substr( $border_color, strrpos( $border_color, '|' ) + 1 ); + $styles [] = sprintf( 'border-%s-color: var(--wp--preset--color--%s);', $side, $named_color_slug ); + } else { + $styles [] = sprintf( 'border-%s-color: %s;', $side, $border['color'] ); + } + } + + return $styles; +} + /** * Checks whether the current block type supports the border feature requested. * diff --git a/lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php b/lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php index 8868185360acd..668082863114f 100644 --- a/lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php +++ b/lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php @@ -15,6 +15,57 @@ * @access private */ class WP_Theme_JSON_Gutenberg extends WP_Theme_JSON_5_9 { + /** + * Metadata for style properties. + * + * Each element is a direct mapping from the CSS property name to the + * path to the value in theme.json & block attributes. + */ + const PROPERTIES_METADATA = array( + 'background' => array( 'color', 'gradient' ), + 'background-color' => array( 'color', 'background' ), + 'border-radius' => array( 'border', 'radius' ), + 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), + 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), + 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ), + 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ), + 'border-color' => array( 'border', 'color' ), + 'border-width' => array( 'border', 'width' ), + 'border-style' => array( 'border', 'style' ), + 'border-top-color' => array( 'border', 'top', 'color' ), + 'border-top-width' => array( 'border', 'top', 'width' ), + 'border-top-style' => array( 'border', 'top', 'style' ), + 'border-right-color' => array( 'border', 'right', 'color' ), + 'border-right-width' => array( 'border', 'right', 'width' ), + 'border-right-style' => array( 'border', 'right', 'style' ), + 'border-bottom-color' => array( 'border', 'bottom', 'color' ), + 'border-bottom-width' => array( 'border', 'bottom', 'width' ), + 'border-bottom-style' => array( 'border', 'bottom', 'style' ), + 'border-left-color' => array( 'border', 'left', 'color' ), + 'border-left-width' => array( 'border', 'left', 'width' ), + 'border-left-style' => array( 'border', 'left', 'style' ), + 'color' => array( 'color', 'text' ), + 'font-family' => array( 'typography', 'fontFamily' ), + 'font-size' => array( 'typography', 'fontSize' ), + 'font-style' => array( 'typography', 'fontStyle' ), + 'font-weight' => array( 'typography', 'fontWeight' ), + 'letter-spacing' => array( 'typography', 'letterSpacing' ), + 'line-height' => array( 'typography', 'lineHeight' ), + 'margin' => array( 'spacing', 'margin' ), + 'margin-top' => array( 'spacing', 'margin', 'top' ), + 'margin-right' => array( 'spacing', 'margin', 'right' ), + 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ), + 'margin-left' => array( 'spacing', 'margin', 'left' ), + 'padding' => array( 'spacing', 'padding' ), + 'padding-top' => array( 'spacing', 'padding', 'top' ), + 'padding-right' => array( 'spacing', 'padding', 'right' ), + 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ), + 'padding-left' => array( 'spacing', 'padding', 'left' ), + '--wp--style--block-gap' => array( 'spacing', 'blockGap' ), + 'text-decoration' => array( 'typography', 'textDecoration' ), + 'text-transform' => array( 'typography', 'textTransform' ), + 'filter' => array( 'filter', 'duotone' ), + ); /** * Presets are a set of values that serve @@ -197,6 +248,47 @@ class WP_Theme_JSON_Gutenberg extends WP_Theme_JSON_5_9 { ), ); + /** + * The valid properties under the styles key. + * + * @var array + */ + const VALID_STYLES = array( + 'border' => array( + 'color' => null, + 'radius' => null, + 'style' => null, + 'width' => null, + 'top' => null, + 'right' => null, + 'bottom' => null, + 'left' => null, + ), + 'color' => array( + 'background' => null, + 'gradient' => null, + 'text' => null, + ), + 'filter' => array( + 'duotone' => null, + ), + 'spacing' => array( + 'margin' => null, + 'padding' => null, + 'blockGap' => 'top', + ), + 'typography' => array( + 'fontFamily' => null, + 'fontSize' => null, + 'fontStyle' => null, + 'fontWeight' => null, + 'letterSpacing' => null, + 'lineHeight' => null, + 'textDecoration' => null, + 'textTransform' => null, + ), + ); + /** * Returns the current theme's wanted patterns(slugs) to be * registered from Pattern Directory. diff --git a/packages/block-editor/src/components/border-radius-control/style.scss b/packages/block-editor/src/components/border-radius-control/style.scss index becab0d9de11e..a08e89250df2b 100644 --- a/packages/block-editor/src/components/border-radius-control/style.scss +++ b/packages/block-editor/src/components/border-radius-control/style.scss @@ -11,12 +11,14 @@ align-items: flex-start; > .components-unit-control-wrapper { - width: calc(50% - 26px); + width: 110px; margin-bottom: 0; + margin-right: #{ $grid-unit-10 }; + flex-shrink: 0; } .components-range-control { - width: calc(50% - 26px); + flex: 1; margin-bottom: 0; .components-base-control__field { @@ -49,6 +51,7 @@ .component-border-radius-control__linked-button.has-icon { display: flex; justify-content: center; + margin-left: 2px; svg { margin-right: 0; diff --git a/packages/block-editor/src/hooks/border-color.js b/packages/block-editor/src/hooks/border-color.js deleted file mode 100644 index 204d4b7e0d28f..0000000000000 --- a/packages/block-editor/src/hooks/border-color.js +++ /dev/null @@ -1,315 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { addFilter } from '@wordpress/hooks'; -import { __ } from '@wordpress/i18n'; -import { createHigherOrderComponent } from '@wordpress/compose'; -import { useEffect, useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import ColorGradientSettingsDropdown from '../components/colors-gradients/dropdown'; -import useMultipleOriginColorsAndGradients from '../components/colors-gradients/use-multiple-origin-colors-and-gradients'; -import { - getColorClassName, - getColorObjectByColorValue, - getColorObjectByAttributeValues, -} from '../components/colors'; -import useSetting from '../components/use-setting'; -import { - BORDER_SUPPORT_KEY, - hasBorderSupport, - removeBorderAttribute, -} from './border'; -import { cleanEmptyObject, shouldSkipSerialization } from './utils'; - -// Defining empty array here instead of inline avoids unnecessary re-renders of -// color control. -const EMPTY_ARRAY = []; - -/** - * Inspector control panel containing the border color related configuration. - * - * There is deliberate overlap between the colors and borders block supports - * relating to border color. It can be argued the border color controls could - * be included within either, or both, the colors and borders panels in the - * inspector controls. If they share the same block attributes it should not - * matter. - * - * @param {Object} props Block properties. - * - * @return {WPElement} Border color edit element. - */ -export function BorderColorEdit( props ) { - const { - attributes: { borderColor, style }, - setAttributes, - } = props; - const colorGradientSettings = useMultipleOriginColorsAndGradients(); - const availableColors = colorGradientSettings.colors.reduce( - ( colors, origin ) => colors.concat( origin.colors ), - [] - ); - const { color: customBorderColor } = style?.border || {}; - const [ colorValue, setColorValue ] = useState( - () => - getColorObjectByAttributeValues( - availableColors, - borderColor, - customBorderColor - )?.color - ); - - // Detect changes in the color attributes and update the colorValue to keep the - // UI in sync. This is necessary for situations when border controls interact with - // each other: eg, setting the border width to zero causes the color and style - // selections to be cleared. - useEffect( () => { - setColorValue( - getColorObjectByAttributeValues( - availableColors, - borderColor, - customBorderColor - )?.color - ); - }, [ borderColor, customBorderColor, availableColors ] ); - - const onChangeColor = ( value ) => { - setColorValue( value ); - - const colorObject = getColorObjectByColorValue( - availableColors, - value - ); - const newStyle = { - ...style, - border: { - ...style?.border, - color: colorObject?.slug ? undefined : value, - }, - }; - - // If empty slug, ensure undefined to remove attribute. - const newNamedColor = colorObject?.slug ? colorObject.slug : undefined; - - setAttributes( { - style: cleanEmptyObject( newStyle ), - borderColor: newNamedColor, - } ); - }; - - const settings = [ - { - label: __( 'Color' ), - onColorChange: onChangeColor, - colorValue, - clearable: false, - }, - ]; - return ( - - ); -} - -/** - * Checks if there is a current value in the border color block support - * attributes. - * - * @param {Object} props Block props. - * @return {boolean} Whether or not the block has a border color value set. - */ -export function hasBorderColorValue( props ) { - const { - attributes: { borderColor, style }, - } = props; - - return !! borderColor || !! style?.border?.color; -} - -/** - * Resets the border color block support attributes. This can be used when - * disabling the border color support controls for a block via a progressive - * discovery panel. - * - * @param {Object} props Block props. - * @param {Object} props.attributes Block's attributes. - * @param {Object} props.setAttributes Function to set block's attributes. - */ -export function resetBorderColor( { attributes = {}, setAttributes } ) { - const { style } = attributes; - - setAttributes( { - borderColor: undefined, - style: removeBorderAttribute( style, 'color' ), - } ); -} - -/** - * Filters registered block settings, extending attributes to include - * `borderColor` if needed. - * - * @param {Object} settings Original block settings. - * - * @return {Object} Updated block settings. - */ -function addAttributes( settings ) { - if ( ! hasBorderSupport( settings, 'color' ) ) { - return settings; - } - - // Allow blocks to specify default value if needed. - if ( settings.attributes.borderColor ) { - return settings; - } - - // Add new borderColor attribute to block settings. - return { - ...settings, - attributes: { - ...settings.attributes, - borderColor: { - type: 'string', - }, - }, - }; -} - -/** - * Override props assigned to save component to inject border color. - * - * @param {Object} props Additional props applied to save element. - * @param {Object} blockType Block type definition. - * @param {Object} attributes Block's attributes. - * - * @return {Object} Filtered props to apply to save element. - */ -function addSaveProps( props, blockType, attributes ) { - if ( - ! hasBorderSupport( blockType, 'color' ) || - shouldSkipSerialization( blockType, BORDER_SUPPORT_KEY, 'color' ) - ) { - return props; - } - - const { borderColor, style } = attributes; - const borderColorClass = getColorClassName( 'border-color', borderColor ); - - const newClassName = classnames( props.className, { - 'has-border-color': borderColor || style?.border?.color, - [ borderColorClass ]: !! borderColorClass, - } ); - - // If we are clearing the last of the previous classes in `className` - // set it to `undefined` to avoid rendering empty DOM attributes. - props.className = newClassName ? newClassName : undefined; - - return props; -} - -/** - * Filters the registered block settings to apply border color styles and - * classnames to the block edit wrapper. - * - * @param {Object} settings Original block settings. - * - * @return {Object} Filtered block settings. - */ -function addEditProps( settings ) { - if ( - ! hasBorderSupport( settings, 'color' ) || - shouldSkipSerialization( settings, BORDER_SUPPORT_KEY, 'color' ) - ) { - return settings; - } - - const existingGetEditWrapperProps = settings.getEditWrapperProps; - settings.getEditWrapperProps = ( attributes ) => { - let props = {}; - - if ( existingGetEditWrapperProps ) { - props = existingGetEditWrapperProps( attributes ); - } - - return addSaveProps( props, settings, attributes ); - }; - - return settings; -} - -/** - * This adds inline styles for color palette colors. - * Ideally, this is not needed and themes should load their palettes on the editor. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -export const withBorderColorPaletteStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const { name, attributes } = props; - const { borderColor } = attributes; - const colors = useSetting( 'color.palette' ) || EMPTY_ARRAY; - - if ( - ! hasBorderSupport( name, 'color' ) || - shouldSkipSerialization( name, BORDER_SUPPORT_KEY, 'color' ) - ) { - return ; - } - - const extraStyles = { - borderColor: borderColor - ? getColorObjectByAttributeValues( colors, borderColor )?.color - : undefined, - }; - - let wrapperProps = props.wrapperProps; - wrapperProps = { - ...props.wrapperProps, - style: { - ...extraStyles, - ...props.wrapperProps?.style, - }, - }; - - return ; - } -); - -addFilter( - 'blocks.registerBlockType', - 'core/border/addAttributes', - addAttributes -); - -addFilter( - 'blocks.getSaveContent.extraProps', - 'core/border/addSaveProps', - addSaveProps -); - -addFilter( - 'blocks.registerBlockType', - 'core/border/addEditProps', - addEditProps -); - -addFilter( - 'editor.BlockListBlock', - 'core/border/with-border-color-palette-styles', - withBorderColorPaletteStyles -); diff --git a/packages/block-editor/src/hooks/border-style.js b/packages/block-editor/src/hooks/border-style.js deleted file mode 100644 index 1a3a5f923383c..0000000000000 --- a/packages/block-editor/src/hooks/border-style.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Internal dependencies - */ -import BorderStyleControl from '../components/border-style-control'; -import { cleanEmptyObject } from './utils'; -import { removeBorderAttribute } from './border'; - -/** - * Inspector control for configuring border style property. - * - * @param {Object} props Block properties. - * - * @return {WPElement} Border style edit element. - */ -export const BorderStyleEdit = ( props ) => { - const { - attributes: { style }, - setAttributes, - } = props; - - const onChange = ( newBorderStyle ) => { - const newStyleAttributes = { - ...style, - border: { - ...style?.border, - style: newBorderStyle, - }, - }; - - setAttributes( { style: cleanEmptyObject( newStyleAttributes ) } ); - }; - - return ( - - ); -}; - -/** - * Checks if there is a current value in the border style block support - * attributes. - * - * @param {Object} props Block props. - * @return {boolean} Whether or not the block has a border style value set. - */ -export function hasBorderStyleValue( props ) { - return !! props.attributes.style?.border?.style; -} - -/** - * Resets the border style block support attribute. This can be used when - * disabling the border style support control for a block via a progressive - * discovery panel. - * - * @param {Object} props Block props. - * @param {Object} props.attributes Block's attributes. - * @param {Object} props.setAttributes Function to set block's attributes. - */ -export function resetBorderStyle( { attributes = {}, setAttributes } ) { - const { style } = attributes; - setAttributes( { style: removeBorderAttribute( style, 'style' ) } ); -} diff --git a/packages/block-editor/src/hooks/border-width.js b/packages/block-editor/src/hooks/border-width.js deleted file mode 100644 index f6ce67d781f34..0000000000000 --- a/packages/block-editor/src/hooks/border-width.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * WordPress dependencies - */ -import { - __experimentalUnitControl as UnitControl, - __experimentalUseCustomUnits as useCustomUnits, -} from '@wordpress/components'; -import { useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { cleanEmptyObject } from './utils'; -import { removeBorderAttribute } from './border'; -import useSetting from '../components/use-setting'; - -const MIN_BORDER_WIDTH = 0; - -/** - * Inspector control for configuring border width property. - * - * @param {Object} props Block properties. - * - * @return {WPElement} Border width edit element. - */ -export const BorderWidthEdit = ( props ) => { - const { - attributes: { borderColor, style }, - setAttributes, - } = props; - - const { width, color: customBorderColor, style: borderStyle } = - style?.border || {}; - - // Used to temporarily track previous border color & style selections to be - // able to restore them when border width changes from zero value. - const [ styleSelection, setStyleSelection ] = useState(); - const [ colorSelection, setColorSelection ] = useState(); - const [ customColorSelection, setCustomColorSelection ] = useState(); - - const onChange = ( newWidth ) => { - let newStyle = { - ...style, - border: { - ...style?.border, - width: newWidth, - }, - }; - - // Used to clear named border color attribute. - let borderPaletteColor = borderColor; - - const hasZeroWidth = parseFloat( newWidth ) === 0; - const hadPreviousZeroWidth = parseFloat( width ) === 0; - - // Setting the border width explicitly to zero will also set the - // border style to `none` and clear border color attributes. - if ( hasZeroWidth && ! hadPreviousZeroWidth ) { - // Before clearing color and style selections, keep track of - // the current selections so they can be restored when the width - // changes to a non-zero value. - setColorSelection( borderColor ); - setCustomColorSelection( customBorderColor ); - setStyleSelection( borderStyle ); - - // Clear style and color attributes. - borderPaletteColor = undefined; - newStyle.border.color = undefined; - newStyle.border.style = 'none'; - } - - if ( ! hasZeroWidth && hadPreviousZeroWidth ) { - // Restore previous border style selection if width is now not zero and - // border style was 'none'. This is to support changes to the UI which - // change the border style UI to a segmented control without a "none" - // option. - if ( borderStyle === 'none' ) { - newStyle.border.style = styleSelection; - } - - // Restore previous border color selection if width is no longer zero - // and current border color is undefined. - if ( borderColor === undefined ) { - borderPaletteColor = colorSelection; - newStyle.border.color = customColorSelection; - } - } - - // If width was reset, clean out undefined styles. - if ( newWidth === undefined || newWidth === '' ) { - newStyle = cleanEmptyObject( newStyle ); - } - - setAttributes( { - borderColor: borderPaletteColor, - style: newStyle, - } ); - }; - - const units = useCustomUnits( { - availableUnits: useSetting( 'spacing.units' ) || [ 'px', 'em', 'rem' ], - } ); - - return ( - - ); -}; - -/** - * Checks if there is a current value in the border width block support - * attributes. - * - * @param {Object} props Block props. - * @return {boolean} Whether or not the block has a border width value set. - */ -export function hasBorderWidthValue( props ) { - return !! props.attributes.style?.border?.width; -} - -/** - * Resets the border width block support attribute. This can be used when - * disabling the border width support control for a block via a progressive - * discovery panel. - * - * @param {Object} props Block props. - * @param {Object} props.attributes Block's attributes. - * @param {Object} props.setAttributes Function to set block's attributes. - */ -export function resetBorderWidth( { attributes = {}, setAttributes } ) { - const { style } = attributes; - setAttributes( { style: removeBorderAttribute( style, 'width' ) } ); -} diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index f17eeab4e3de9..be5c03b1a8c0e 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -1,53 +1,174 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ import { getBlockSupport } from '@wordpress/blocks'; -import { __experimentalToolsPanelItem as ToolsPanelItem } from '@wordpress/components'; +import { + __experimentalBorderBoxControl as BorderBoxControl, + __experimentalHasSplitBorders as hasSplitBorders, + __experimentalIsDefinedBorder as isDefinedBorder, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; +import { createHigherOrderComponent } from '@wordpress/compose'; import { Platform } from '@wordpress/element'; +import { addFilter } from '@wordpress/hooks'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { - BorderColorEdit, - hasBorderColorValue, - resetBorderColor, -} from './border-color'; import { BorderRadiusEdit, hasBorderRadiusValue, resetBorderRadius, } from './border-radius'; -import { - BorderStyleEdit, - hasBorderStyleValue, - resetBorderStyle, -} from './border-style'; -import { - BorderWidthEdit, - hasBorderWidthValue, - resetBorderWidth, -} from './border-width'; +import { getColorClassName } from '../components/colors'; import InspectorControls from '../components/inspector-controls'; +import useMultipleOriginColorsAndGradients from '../components/colors-gradients/use-multiple-origin-colors-and-gradients'; import useSetting from '../components/use-setting'; -import { cleanEmptyObject } from './utils'; +import { cleanEmptyObject, shouldSkipSerialization } from './utils'; export const BORDER_SUPPORT_KEY = '__experimentalBorder'; +const borderSides = [ 'top', 'right', 'bottom', 'left' ]; + +const hasBorderValue = ( props ) => { + const { borderColor, style } = props.attributes; + return isDefinedBorder( style?.border ) || !! borderColor; +}; + +// The border color, style, and width are omitted so they get undefined. The +// border radius is separate and must retain its selection. +const resetBorder = ( { attributes = {}, setAttributes } ) => { + const { style } = attributes; + setAttributes( { + borderColor: undefined, + style: { + ...style, + border: cleanEmptyObject( { + radius: style?.border?.radius, + } ), + }, + } ); +}; + +const resetBorderFilter = ( newAttributes ) => ( { + ...newAttributes, + borderColor: undefined, + style: { + ...newAttributes.style, + border: { + radius: newAttributes.style?.border?.radius, + }, + }, +} ); + +const getColorByProperty = ( colors, property, value ) => { + let matchedColor; + + colors.some( ( origin ) => + origin.colors.some( ( color ) => { + if ( color[ property ] === value ) { + matchedColor = color; + return true; + } + + return false; + } ) + ); + + return matchedColor; +}; + +export const getMultiOriginColor = ( { colors, namedColor, customColor } ) => { + // Search each origin (default, theme, or user) for matching color by name. + if ( namedColor ) { + const colorObject = getColorByProperty( colors, 'slug', namedColor ); + if ( colorObject ) { + return colorObject; + } + } + + // Skip if no custom color or matching named color. + if ( ! customColor ) { + return { color: undefined }; + } + + // Attempt to find color via custom color value or build new object. + const colorObject = getColorByProperty( colors, 'color', customColor ); + return colorObject ? colorObject : { color: customColor }; +}; + +const getBorderObject = ( attributes, colors ) => { + const { borderColor, style } = attributes; + const { border: borderStyles } = style || {}; + + // If we have a named color for a flat border. Fetch that color object and + // apply that color's value to the color property within the style object. + if ( borderColor ) { + const { color } = getMultiOriginColor( { + colors, + namedColor: borderColor, + } ); + + return color ? { ...borderStyles, color } : borderStyles; + } + + // Individual side border color slugs are stored within the border style + // object. If we don't have a border styles object we have nothing further + // to hydrate. + if ( ! borderStyles ) { + return borderStyles; + } + + // If we have named colors for the individual side borders, retrieve their + // related color objects and apply the real color values to the split + // border objects. + const hydratedBorderStyles = { ...borderStyles }; + borderSides.forEach( ( side ) => { + const colorSlug = getColorSlugFromVariable( + hydratedBorderStyles[ side ]?.color + ); + if ( colorSlug ) { + const { color } = getMultiOriginColor( { + colors, + namedColor: colorSlug, + } ); + hydratedBorderStyles[ side ] = { + ...hydratedBorderStyles[ side ], + color, + }; + } + } ); + + return hydratedBorderStyles; +}; + +function getColorSlugFromVariable( value ) { + const namedColor = /var:preset\|color\|(.+)/.exec( value ); + if ( namedColor && namedColor[ 1 ] ) { + return namedColor[ 1 ]; + } + return null; +} + export function BorderPanel( props ) { - const { clientId } = props; + const { attributes, clientId, setAttributes } = props; + const { style } = attributes; + const { colors } = useMultipleOriginColorsAndGradients(); + const isSupported = hasBorderSupport( props.name ); const isColorSupported = useSetting( 'border.color' ) && hasBorderSupport( props.name, 'color' ); - const isRadiusSupported = useSetting( 'border.radius' ) && hasBorderSupport( props.name, 'radius' ); - const isStyleSupported = useSetting( 'border.style' ) && hasBorderSupport( props.name, 'style' ); - const isWidthSupported = useSetting( 'border.width' ) && hasBorderSupport( props.name, 'width' ); @@ -58,7 +179,7 @@ export function BorderPanel( props ) { ! isWidthSupported, ].every( Boolean ); - if ( isDisabled ) { + if ( isDisabled || ! isSupported ) { return null; } @@ -67,61 +188,103 @@ export function BorderPanel( props ) { '__experimentalDefaultControls', ] ); - const createResetAllFilter = ( - borderAttribute, - topLevelAttributes = {} - ) => ( newAttributes ) => ( { - ...newAttributes, - ...topLevelAttributes, - style: { - ...newAttributes.style, - border: { - ...newAttributes.style?.border, - [ borderAttribute ]: undefined, - }, - }, - } ); + const showBorderByDefault = + defaultBorderControls?.color || defaultBorderControls?.width; + + const onBorderChange = ( newBorder ) => { + // Filter out named colors and apply them to appropriate block + // attributes so that CSS classes can be used to apply those colors. + // e.g. has-primary-border-top-color. + + let newBorderStyles = { ...newBorder }; + let newBorderColor; + + if ( hasSplitBorders( newBorder ) ) { + // For each side check if the side has a color value set + // If so, determine if it belongs to a named color, in which case + // we update the color property. + // + // This deliberately overwrites `newBorderStyles` to avoid mutating + // the passed object which causes problems otherwise. + newBorderStyles = { + top: { ...newBorder.top }, + right: { ...newBorder.right }, + bottom: { ...newBorder.bottom }, + left: { ...newBorder.left }, + }; + + borderSides.forEach( ( side ) => { + if ( newBorder[ side ]?.color ) { + const colorObject = getMultiOriginColor( { + colors, + customColor: newBorder[ side ]?.color, + } ); + + if ( colorObject.slug ) { + newBorderStyles[ + side + ].color = `var:preset|color|${ colorObject.slug }`; + } + } + } ); + } else if ( newBorder?.color ) { + // We have a flat border configuration. Apply named color slug to + // `borderColor` attribute and clear color style property if found. + const customColor = newBorder?.color; + const colorObject = getMultiOriginColor( { colors, customColor } ); + + if ( colorObject.slug ) { + newBorderColor = colorObject.slug; + newBorderStyles.color = undefined; + } + } + + // Ensure previous border radius styles are maintained and clean + // overall result for empty objects or properties. + const newStyle = cleanEmptyObject( { + ...style, + border: { radius: style?.border?.radius, ...newBorderStyles }, + } ); + + setAttributes( { + style: newStyle, + borderColor: newBorderColor, + } ); + }; + + const hydratedBorder = getBorderObject( attributes, colors ); return ( - { isWidthSupported && ( + { ( isWidthSupported || isColorSupported ) && ( hasBorderWidthValue( props ) } - label={ __( 'Width' ) } - onDeselect={ () => resetBorderWidth( props ) } - isShownByDefault={ defaultBorderControls?.width } - resetAllFilter={ createResetAllFilter( 'width' ) } + hasValue={ () => hasBorderValue( props ) } + label={ __( 'Border' ) } + onDeselect={ () => resetBorder( props ) } + isShownByDefault={ showBorderByDefault } + resetAllFilter={ resetBorderFilter } panelId={ clientId } > - - - ) } - { isStyleSupported && ( - hasBorderStyleValue( props ) } - label={ __( 'Style' ) } - onDeselect={ () => resetBorderStyle( props ) } - isShownByDefault={ defaultBorderControls?.style } - resetAllFilter={ createResetAllFilter( 'style' ) } - panelId={ clientId } - > - - - ) } - { isColorSupported && ( - hasBorderColorValue( props ) } - label={ __( 'Color' ) } - onDeselect={ () => resetBorderColor( props ) } - isShownByDefault={ defaultBorderControls?.color } - resetAllFilter={ createResetAllFilter( 'color', { - borderColor: undefined, - } ) } - panelId={ clientId } - > - + ) } { isRadiusSupported && ( @@ -130,7 +293,16 @@ export function BorderPanel( props ) { label={ __( 'Radius' ) } onDeselect={ () => resetBorderRadius( props ) } isShownByDefault={ defaultBorderControls?.radius } - resetAllFilter={ createResetAllFilter( 'radius' ) } + resetAllFilter={ ( newAttributes ) => ( { + ...newAttributes, + style: { + ...newAttributes.style, + border: { + ...newAttributes.style?.border, + radius: undefined, + }, + }, + } ) } panelId={ clientId } > @@ -189,3 +361,197 @@ export function removeBorderAttribute( style, attribute ) { }, } ); } + +/** + * Filters registered block settings, extending attributes to include + * `borderColor` if needed. + * + * @param {Object} settings Original block settings. + * + * @return {Object} Updated block settings. + */ +function addAttributes( settings ) { + if ( ! hasBorderSupport( settings, 'color' ) ) { + return settings; + } + + // Allow blocks to specify default value if needed. + if ( settings.attributes.borderColor ) { + return settings; + } + + // Add new borderColor attribute to block settings. + return { + ...settings, + attributes: { + ...settings.attributes, + borderColor: { + type: 'string', + }, + }, + }; +} + +/** + * Override props assigned to save component to inject border color. + * + * @param {Object} props Additional props applied to save element. + * @param {Object} blockType Block type definition. + * @param {Object} attributes Block's attributes. + * + * @return {Object} Filtered props to apply to save element. + */ +function addSaveProps( props, blockType, attributes ) { + if ( + ! hasBorderSupport( blockType, 'color' ) || + shouldSkipSerialization( blockType, BORDER_SUPPORT_KEY, 'color' ) + ) { + return props; + } + + const borderClasses = getBorderClasses( attributes ); + const newClassName = classnames( props.className, borderClasses ); + + // If we are clearing the last of the previous classes in `className` + // set it to `undefined` to avoid rendering empty DOM attributes. + props.className = newClassName ? newClassName : undefined; + + return props; +} + +/** + * Generates a CSS class name consisting of all the applicable border color + * classes given the current block attributes. + * + * @param {Object} attributes Block's attributes. + * + * @return {string} CSS class name. + */ +export function getBorderClasses( attributes ) { + const { borderColor, style } = attributes; + const borderColorClass = getColorClassName( 'border-color', borderColor ); + + return classnames( { + 'has-border-color': borderColor || style?.border?.color, + [ borderColorClass ]: !! borderColorClass, + } ); +} + +/** + * Filters the registered block settings to apply border color styles and + * classnames to the block edit wrapper. + * + * @param {Object} settings Original block settings. + * + * @return {Object} Filtered block settings. + */ +function addEditProps( settings ) { + if ( + ! hasBorderSupport( settings, 'color' ) || + shouldSkipSerialization( settings, BORDER_SUPPORT_KEY, 'color' ) + ) { + return settings; + } + + const existingGetEditWrapperProps = settings.getEditWrapperProps; + settings.getEditWrapperProps = ( attributes ) => { + let props = {}; + + if ( existingGetEditWrapperProps ) { + props = existingGetEditWrapperProps( attributes ); + } + + return addSaveProps( props, settings, attributes ); + }; + + return settings; +} + +/** + * This adds inline styles for color palette colors. + * Ideally, this is not needed and themes should load their palettes on the editor. + * + * @param {Function} BlockListBlock Original component. + * + * @return {Function} Wrapped component. + */ +export const withBorderColorPaletteStyles = createHigherOrderComponent( + ( BlockListBlock ) => ( props ) => { + const { name, attributes } = props; + const { borderColor, style } = attributes; + const { colors } = useMultipleOriginColorsAndGradients(); + + if ( + ! hasBorderSupport( name, 'color' ) || + shouldSkipSerialization( name, BORDER_SUPPORT_KEY, 'color' ) + ) { + return ; + } + + const { color: borderColorValue } = getMultiOriginColor( { + colors, + namedColor: borderColor, + } ); + const { color: borderTopColor } = getMultiOriginColor( { + colors, + namedColor: getColorSlugFromVariable( style?.border?.top?.color ), + } ); + const { color: borderRightColor } = getMultiOriginColor( { + colors, + namedColor: getColorSlugFromVariable( style?.border?.right?.color ), + } ); + + const { color: borderBottomColor } = getMultiOriginColor( { + colors, + namedColor: getColorSlugFromVariable( + style?.border?.bottom?.color + ), + } ); + const { color: borderLeftColor } = getMultiOriginColor( { + colors, + namedColor: getColorSlugFromVariable( style?.border?.left?.color ), + } ); + + const extraStyles = { + borderTopColor: borderTopColor || borderColorValue, + borderRightColor: borderRightColor || borderColorValue, + borderBottomColor: borderBottomColor || borderColorValue, + borderLeftColor: borderLeftColor || borderColorValue, + }; + + let wrapperProps = props.wrapperProps; + wrapperProps = { + ...props.wrapperProps, + style: { + ...props.wrapperProps?.style, + ...extraStyles, + }, + }; + + return ; + } +); + +addFilter( + 'blocks.registerBlockType', + 'core/border/addAttributes', + addAttributes +); + +addFilter( + 'blocks.getSaveContent.extraProps', + 'core/border/addSaveProps', + addSaveProps +); + +addFilter( + 'blocks.registerBlockType', + 'core/border/addEditProps', + addEditProps +); + +addFilter( + 'editor.BlockListBlock', + 'core/border/with-border-color-palette-styles', + withBorderColorPaletteStyles +); diff --git a/packages/block-editor/src/hooks/border.scss b/packages/block-editor/src/hooks/border.scss index c86c046309d88..0aab500b55076 100644 --- a/packages/block-editor/src/hooks/border.scss +++ b/packages/block-editor/src/hooks/border.scss @@ -3,3 +3,51 @@ grid-column: span 1; } } + +.block-editor__border-box-control__popover, +.block-editor__border-box-control__popover-top, +.block-editor__border-box-control__popover-right, +.block-editor__border-box-control__popover-bottom, +.block-editor__border-box-control__popover-left { + .components-popover__content { + width: 282px; + } +} + +$split-border-control-offset: 55px; + +@include break-medium() { + .block-editor__border-box-control__popover, + .block-editor__border-box-control__popover-left { + .components-popover__content { + margin-right: #{ $grid-unit-50 + $grid-unit-15 } !important; + } + } + + .block-editor__border-box-control__popover-top, + .block-editor__border-box-control__popover-bottom { + .components-popover__content { + margin-right: #{ $grid-unit-50 + $grid-unit-15 + $split-border-control-offset } !important; + } + } + + .block-editor__border-box-control__popover-right { + .components-popover__content { + margin-right: #{ $grid-unit-50 + $grid-unit-15 + ( $split-border-control-offset * 2 )} !important; + } + } + + .block-editor__border-box-control__popover, + .block-editor__border-box-control__popover-top, + .block-editor__border-box-control__popover-right, + .block-editor__border-box-control__popover-bottom, + .block-editor__border-box-control__popover-left { + &.is-from-top .components-popover__content { + margin-top: #{ -($grid-unit-50 + $grid-unit-15) } !important; + } + + &.is-from-bottom .components-popover__content { + margin-bottom: #{ -($grid-unit-50 + $grid-unit-15) } !important; + } + } +} diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 708dae62a3971..b18a175973c1d 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -11,7 +11,7 @@ import './style'; import './color'; import './duotone'; import './font-size'; -import './border-color'; +import './border'; import './layout'; export { useCustomSides } from './dimensions'; diff --git a/packages/block-editor/src/hooks/use-border-props.js b/packages/block-editor/src/hooks/use-border-props.js index d62d68365fe1b..0afe1bb70cb5a 100644 --- a/packages/block-editor/src/hooks/use-border-props.js +++ b/packages/block-editor/src/hooks/use-border-props.js @@ -1,46 +1,28 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * Internal dependencies */ import { getInlineStyles } from './style'; -import { - getColorClassName, - getColorObjectByAttributeValues, -} from '../components/colors'; -import useSetting from '../components/use-setting'; +import { getBorderClasses, getMultiOriginColor } from './border'; +import useMultipleOriginColorsAndGradients from '../components/colors-gradients/use-multiple-origin-colors-and-gradients'; // This utility is intended to assist where the serialization of the border // block support is being skipped for a block but the border related CSS classes // & styles still need to be generated so they can be applied to inner elements. -const EMPTY_ARRAY = []; - /** * Provides the CSS class names and inline styles for a block's border support * attributes. * - * @param {Object} attributes Block attributes. - * @param {string} attributes.borderColor Selected named border color. - * @param {Object} attributes.style Block's styles attribute. - * + * @param {Object} attributes Block attributes. * @return {Object} Border block support derived CSS classes & styles. */ -export function getBorderClassesAndStyles( { borderColor, style } ) { - const borderStyles = style?.border || {}; - const borderClass = getColorClassName( 'border-color', borderColor ); - - const className = classnames( { - [ borderClass ]: !! borderClass, - 'has-border-color': borderColor || style?.border?.color, - } ); +export function getBorderClassesAndStyles( attributes ) { + const border = attributes.style?.border || {}; + const className = getBorderClasses( attributes ); return { className: className || undefined, - style: getInlineStyles( { border: borderStyles } ), + style: getInlineStyles( { border } ), }; } @@ -56,16 +38,17 @@ export function getBorderClassesAndStyles( { borderColor, style } ) { * @return {Object} ClassName & style props from border block support. */ export function useBorderProps( attributes ) { - const colors = useSetting( 'color.palette' ) || EMPTY_ARRAY; + const { colors } = useMultipleOriginColorsAndGradients(); const borderProps = getBorderClassesAndStyles( attributes ); + const { borderColor } = attributes; - // Force inline style to apply border color when themes do not load their - // color stylesheets in the editor. - if ( attributes.borderColor ) { - const borderColorObject = getColorObjectByAttributeValues( + // Force inline styles to apply named border colors when themes do not load + // their color stylesheets in the editor. + if ( borderColor ) { + const borderColorObject = getMultiOriginColor( { colors, - attributes.borderColor - ); + namedColor: borderColor, + } ); borderProps.style.borderColor = borderColorObject.color; } diff --git a/packages/block-library/src/common.scss b/packages/block-library/src/common.scss index 2244a4c4fe79e..5819a43c93ed5 100644 --- a/packages/block-library/src/common.scss +++ b/packages/block-library/src/common.scss @@ -116,11 +116,34 @@ html :where(.has-border-color) { border-style: solid; } +html :where([style*="border-top-color"]) { + border-top-style: solid; +} +html :where([style*="border-right-color"]) { + border-right-style: solid; +} +html :where([style*="border-bottom-color"]) { + border-bottom-style: solid; +} +html :where([style*="border-left-color"]) { + border-left-style: solid; +} html :where([style*="border-width"]) { border-style: solid; } - +html :where([style*="border-top-width"]) { + border-top-style: solid; +} +html :where([style*="border-right-width"]) { + border-right-style: solid; +} +html :where([style*="border-bottom-width"]) { + border-bottom-style: solid; +} +html :where([style*="border-left-width"]) { + border-left-style: solid; +} /** * Provide baseline responsiveness for images. diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index 05faf4624bea9..1457423e4d60e 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -103,8 +103,6 @@ export default function SearchEdit( { } ); }, [ insertedInNavigationBlock ] ); const borderRadius = style?.border?.radius; - const borderColor = style?.border?.color; - const borderWidth = style?.border?.width; const borderProps = useBorderProps( attributes ); // Check for old deprecated numerical border radius. Done as a separate @@ -392,10 +390,18 @@ export default function SearchEdit( { radius ? `calc(${ radius } + ${ DEFAULT_INNER_PADDING })` : undefined; const getWrapperStyles = () => { - const styles = { - borderColor, - borderWidth: isButtonPositionInside ? borderWidth : undefined, - }; + const styles = isButtonPositionInside + ? borderProps.style + : { + borderRadius: borderProps.style?.borderRadius, + borderTopLeftRadius: borderProps.style?.borderTopLeftRadius, + borderTopRightRadius: + borderProps.style?.borderTopRightRadius, + borderBottomLeftRadius: + borderProps.style?.borderBottomLeftRadius, + borderBottomRightRadius: + borderProps.style?.borderBottomRightRadius, + }; const isNonZeroBorderRadius = borderRadius !== undefined && parseInt( borderRadius, 10 ) !== 0; @@ -417,11 +423,11 @@ export default function SearchEdit( { } = borderRadius; return { + ...styles, borderTopLeftRadius: padBorderRadius( topLeft ), borderTopRightRadius: padBorderRadius( topRight ), borderBottomLeftRadius: padBorderRadius( bottomLeft ), borderBottomRightRadius: padBorderRadius( bottomRight ), - ...styles, }; } diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 94ec6fc4ac7f5..8bf1e5e5ee2db 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -171,6 +171,75 @@ function classnames_for_block_core_search( $attributes ) { return implode( ' ', $classnames ); } +/** + * This generates a CSS rule for the given border property and side if provided. + * Based on whether the Search block is configured to display the button inside + * or not, the generated rule is injected into the appropriate collection of + * styles for later application in the block's markup. + * + * @param array $attributes The block attributes. + * @param string $property Border property to generate rule for e.g. width or color. + * @param string $side Optional side border. The dictates the value retrieved and final CSS property. + * @param array $wrapper_styles Current collection of wrapper styles. + * @param array $button_styles Current collection of button styles. + * @param array $input_styles Current collection of input styles. + * + * @return void + */ +function apply_block_core_search_border_style( $attributes, $property, $side, &$wrapper_styles, &$button_styles, &$input_styles ) { + $is_button_inside = 'button-inside' === _wp_array_get( $attributes, array( 'buttonPosition' ), false ); + + $path = array( 'style', 'border', $property ); + + if ( $side ) { + array_splice( $path, 2, 0, $side ); + } + + $value = _wp_array_get( $attributes, $path, false ); + + if ( empty( $value ) ) { + return; + } + + if ( 'color' === $property && $side ) { + $has_color_preset = strpos( $value, 'var:preset|color|' ) !== false; + if ( $has_color_preset ) { + $named_color_value = substr( $value, strrpos( $value, '|' ) + 1 ); + $value = sprintf( 'var(--wp--preset--color--%s)', $named_color_value ); + } + } + + $property_suffix = $side ? sprintf( '%s-%s', $side, $property ) : $property; + + if ( $is_button_inside ) { + $wrapper_styles[] = sprintf( 'border-%s: %s;', $property_suffix, esc_attr( $value ) ); + } else { + $button_styles[] = sprintf( 'border-%s: %s;', $property_suffix, esc_attr( $value ) ); + $input_styles[] = sprintf( 'border-%s: %s;', $property_suffix, esc_attr( $value ) ); + } +} + +/** + * This adds CSS rules for a given border property e.g. width or color. It + * injects rules into the provided wrapper, button and input style arrays for + * uniform "flat" borders or those with individual sides configured. + * + * @param array $attributes The block attributes. + * @param string $property Border property to generate rule for e.g. width or color. + * @param array $wrapper_styles Current collection of wrapper styles. + * @param array $button_styles Current collection of button styles. + * @param array $input_styles Current collection of input styles. + * + * @return void + */ +function apply_block_core_search_border_styles( $attributes, $property, &$wrapper_styles, &$button_styles, &$input_styles ) { + apply_block_core_search_border_style( $attributes, $property, null, $wrapper_styles, $button_styles, $input_styles ); + apply_block_core_search_border_style( $attributes, $property, 'top', $wrapper_styles, $button_styles, $input_styles ); + apply_block_core_search_border_style( $attributes, $property, 'right', $wrapper_styles, $button_styles, $input_styles ); + apply_block_core_search_border_style( $attributes, $property, 'bottom', $wrapper_styles, $button_styles, $input_styles ); + apply_block_core_search_border_style( $attributes, $property, 'left', $wrapper_styles, $button_styles, $input_styles ); +} + /** * Builds an array of inline styles for the search block. * @@ -201,19 +270,10 @@ function styles_for_block_core_search( $attributes ) { ); } - // Add border width styles. - $has_border_width = ! empty( $attributes['style']['border']['width'] ); - - if ( $has_border_width ) { - $border_width = $attributes['style']['border']['width']; - - if ( $is_button_inside ) { - $wrapper_styles[] = sprintf( 'border-width: %s;', esc_attr( $border_width ) ); - } else { - $button_styles[] = sprintf( 'border-width: %s;', esc_attr( $border_width ) ); - $input_styles[] = sprintf( 'border-width: %s;', esc_attr( $border_width ) ); - } - } + // Add border width and color styles. + apply_block_core_search_border_styles( $attributes, 'width', $wrapper_styles, $button_styles, $input_styles ); + apply_block_core_search_border_styles( $attributes, 'color', $wrapper_styles, $button_styles, $input_styles ); + apply_block_core_search_border_styles( $attributes, 'style', $wrapper_styles, $button_styles, $input_styles ); // Add border radius styles. $has_border_radius = ! empty( $attributes['style']['border']['radius'] ); @@ -269,21 +329,6 @@ function styles_for_block_core_search( $attributes ) { } } - // Add border color styles. - $has_border_color = ! empty( $attributes['style']['border']['color'] ); - - if ( $has_border_color ) { - $border_color = $attributes['style']['border']['color']; - - // Apply wrapper border color if button placed inside. - if ( $is_button_inside ) { - $wrapper_styles[] = sprintf( 'border-color: %s;', esc_attr( $border_color ) ); - } else { - $button_styles[] = sprintf( 'border-color: %s;', esc_attr( $border_color ) ); - $input_styles[] = sprintf( 'border-color: %s;', esc_attr( $border_color ) ); - } - } - // Add color styles. $has_text_color = ! empty( $attributes['style']['color']['text'] ); if ( $has_text_color ) { @@ -315,13 +360,19 @@ function styles_for_block_core_search( $attributes ) { * @return string The border color classnames to be applied to the block elements. */ function get_border_color_classes_for_block_core_search( $attributes ) { + $border_color_classes = array(); $has_custom_border_color = ! empty( $attributes['style']['border']['color'] ); - $border_color_classes = ! empty( $attributes['borderColor'] ) ? sprintf( 'has-border-color has-%s-border-color', $attributes['borderColor'] ) : ''; - // If there's a border color style and no `borderColor` text string, we still want to add the generic `has-border-color` class name to the element. - if ( $has_custom_border_color && empty( $attributes['borderColor'] ) ) { - $border_color_classes = 'has-border-color'; + $has_named_border_color = ! empty( $attributes['borderColor'] ); + + if ( $has_custom_border_color || $has_named_border_color ) { + $border_color_classes[] = 'has-border-color'; + } + + if ( $has_named_border_color ) { + $border_color_classes[] = sprintf( 'has-%s-border-color', esc_attr( $attributes['borderColor'] ) ); } - return $border_color_classes; + + return implode( ' ', $border_color_classes ); } /** diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js index efd28d1c7d624..dea27caa36049 100644 --- a/packages/block-library/src/table/edit.js +++ b/packages/block-library/src/table/edit.js @@ -25,6 +25,7 @@ import { TextControl, ToggleControl, ToolbarDropdownMenu, + __experimentalHasSplitBorders as hasSplitBorders, } from '@wordpress/components'; import { alignLeft, @@ -477,7 +478,15 @@ function TableEdit( { className={ classnames( colorProps.className, borderProps.className, - { 'has-fixed-layout': hasFixedLayout } + { + 'has-fixed-layout': hasFixedLayout, + // This is required in the editor only to overcome + // the fact the editor rewrites individual border + // widths into a shorthand format. + 'has-individual-borders': hasSplitBorders( + attributes?.style?.border + ), + } ) } style={ { ...colorProps.style, ...borderProps.style } } > diff --git a/packages/block-library/src/table/editor.scss b/packages/block-library/src/table/editor.scss index 0300303083379..ca6cfc3d0da8c 100644 --- a/packages/block-library/src/table/editor.scss +++ b/packages/block-library/src/table/editor.scss @@ -43,6 +43,19 @@ figcaption { @include caption-style-theme(); } + + // This is only required in the editor to overcome the fact the editor + // rewrites border width styles into shorthand. + table.has-individual-borders { + > *, + tr, + th, + td { + border-width: $border-width; + border-style: solid; + border-color: currentColor; + } + } } .blocks-table__placeholder-form.blocks-table__placeholder-form { diff --git a/packages/block-library/src/table/style.scss b/packages/block-library/src/table/style.scss index a617d2261234e..52ff4f640aac8 100644 --- a/packages/block-library/src/table/style.scss +++ b/packages/block-library/src/table/style.scss @@ -112,6 +112,58 @@ } } + table[style*="border-top-color"] { + > *, + tr:first-child { + border-top-color: inherit; + + th, + td { + border-top-color: inherit; + } + } + + tr:not(:first-child) { + border-top-color: currentColor; + } + } + table[style*="border-right-color"] { + > *, + tr, + th, + td:last-child { + border-right-color: inherit; + } + } + table[style*="border-bottom-color"] { + > *, + tr:last-child { + border-bottom-color: inherit; + + th, + td { + border-bottom-color: inherit; + } + } + + // Border support classes and styles are applied on the table block + // itself. This means that without the rule below every table row would + // have a bottom border matching the color of the table's border. + // This style gives the best visual appearance and most expected result + // until we can control individual table rows or cells via inner blocks. + tr:not(:last-child) { + border-bottom-color: currentColor; + } + } + table[style*="border-left-color"] { + > *, + tr, + th, + td:first-child { + border-left-color: inherit; + } + } + table[style*="border-style"] { > *, tr, diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index c9b8d3e0e8f1d..07e8bb1ddb580 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -51,6 +51,54 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { value: [ 'border', 'width' ], support: [ '__experimentalBorder', 'width' ], }, + borderTopColor: { + value: [ 'border', 'top', 'color' ], + support: [ '__experimentalBorder', 'color' ], + }, + borderTopStyle: { + value: [ 'border', 'top', 'style' ], + support: [ '__experimentalBorder', 'style' ], + }, + borderTopWidth: { + value: [ 'border', 'top', 'width' ], + support: [ '__experimentalBorder', 'width' ], + }, + borderRightColor: { + value: [ 'border', 'right', 'color' ], + support: [ '__experimentalBorder', 'color' ], + }, + borderRightStyle: { + value: [ 'border', 'right', 'style' ], + support: [ '__experimentalBorder', 'style' ], + }, + borderRightWidth: { + value: [ 'border', 'right', 'width' ], + support: [ '__experimentalBorder', 'width' ], + }, + borderBottomColor: { + value: [ 'border', 'bottom', 'color' ], + support: [ '__experimentalBorder', 'color' ], + }, + borderBottomStyle: { + value: [ 'border', 'bottom', 'style' ], + support: [ '__experimentalBorder', 'style' ], + }, + borderBottomWidth: { + value: [ 'border', 'bottom', 'width' ], + support: [ '__experimentalBorder', 'width' ], + }, + borderLeftColor: { + value: [ 'border', 'left', 'color' ], + support: [ '__experimentalBorder', 'color' ], + }, + borderLeftStyle: { + value: [ 'border', 'left', 'style' ], + support: [ '__experimentalBorder', 'style' ], + }, + borderLeftWidth: { + value: [ 'border', 'left', 'width' ], + support: [ '__experimentalBorder', 'width' ], + }, color: { value: [ 'color', 'text' ], support: [ 'color', 'text' ], diff --git a/packages/edit-site/src/components/global-styles/border-panel.js b/packages/edit-site/src/components/global-styles/border-panel.js index 5780797e2c186..780b165f78570 100644 --- a/packages/edit-site/src/components/global-styles/border-panel.js +++ b/packages/edit-site/src/components/global-styles/border-panel.js @@ -1,17 +1,15 @@ /** * WordPress dependencies */ +import { __experimentalBorderRadiusControl as BorderRadiusControl } from '@wordpress/block-editor'; import { - __experimentalBorderRadiusControl as BorderRadiusControl, - __experimentalBorderStyleControl as BorderStyleControl, - __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, -} from '@wordpress/block-editor'; -import { + __experimentalBorderBoxControl as BorderBoxControl, + __experimentalHasSplitBorders as hasSplitBorders, + __experimentalIsDefinedBorder as isDefinedBorder, __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, - __experimentalUnitControl as UnitControl, - __experimentalUseCustomUnits as useCustomUnits, } from '@wordpress/components'; +import { useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** @@ -24,8 +22,6 @@ import { useStyle, } from './hooks'; -const MIN_BORDER_WIDTH = 0; - export function useHasBorderPanel( name ) { const controls = [ useHasBorderColorControl( name ), @@ -69,63 +65,45 @@ function useHasBorderWidthControl( name ) { ); } -export default function BorderPanel( { name } ) { - // To better reflect if the user has customized a value we need to - // ensure the style value being checked is from the `user` origin. - const [ userBorderStyles ] = useStyle( 'border', name, 'user' ); - const createHasValueCallback = ( feature ) => () => - !! userBorderStyles?.[ feature ]; +function applyFallbackStyle( border ) { + if ( ! border ) { + return border; + } - const createResetCallback = ( setStyle ) => () => setStyle( undefined ); - - const handleOnChange = ( setStyle ) => ( value ) => { - setStyle( value || undefined ); - }; - - const units = useCustomUnits( { - availableUnits: useSetting( 'spacing.units' )[ 0 ] || [ - 'px', - 'em', - 'rem', - ], - } ); - - // Border width. - const showBorderWidth = useHasBorderWidthControl( name ); - const [ borderWidthValue, setBorderWidth ] = useStyle( - 'border.width', - name - ); + if ( ! border.style && ( border.color || border.width ) ) { + return { ...border, style: 'solid' }; + } - // Border style. - const showBorderStyle = useHasBorderStyleControl( name ); - const [ borderStyle, setBorderStyle ] = useStyle( 'border.style', name ); + return border; +} - // When we set a border color or width ensure we have a style so the user - // can see a visible border. - const handleOnChangeWithStyle = ( setStyle ) => ( value ) => { - if ( !! value && ! borderStyle ) { - setBorderStyle( 'solid' ); - } +function applyAllFallbackStyles( border ) { + if ( ! border ) { + return border; + } + + if ( hasSplitBorders( border ) ) { + return { + top: applyFallbackStyle( border.top ), + right: applyFallbackStyle( border.right ), + bottom: applyFallbackStyle( border.bottom ), + left: applyFallbackStyle( border.left ), + }; + } + + return applyFallbackStyle( border ); +} - setStyle( value || undefined ); - }; +export default function BorderPanel( { name } ) { + // To better reflect if the user has customized a value we need to + // ensure the style value being checked is from the `user` origin. + const [ userBorderStyles ] = useStyle( 'border', name, 'user' ); + const [ border, setBorder ] = useStyle( 'border', name ); + const colors = useColorsPerOrigin( name ); - // Border color. const showBorderColor = useHasBorderColorControl( name ); - const [ borderColor, setBorderColor ] = useStyle( 'border.color', name ); - const disableCustomColors = ! useSetting( 'color.custom' )[ 0 ]; - const disableCustomGradients = ! useSetting( 'color.customGradient' )[ 0 ]; - - const borderColorSettings = [ - { - label: __( 'Color' ), - colors: useColorsPerOrigin( name ), - colorValue: borderColor, - onColorChange: handleOnChangeWithStyle( setBorderColor ), - clearable: false, - }, - ]; + const showBorderStyle = useHasBorderStyleControl( name ); + const showBorderWidth = useHasBorderWidthControl( name ); // Border radius. const showBorderRadius = useHasBorderRadiusControl( name ); @@ -141,60 +119,83 @@ export default function BorderPanel( { name } ) { return !! borderValues; }; - const resetAll = () => { - setBorderColor( undefined ); - setBorderRadius( undefined ); - setBorderStyle( undefined ); - setBorderWidth( undefined ); + const resetBorder = () => { + if ( hasBorderRadius() ) { + return setBorder( { radius: userBorderStyles.radius } ); + } + + setBorder( undefined ); }; + const resetAll = useCallback( () => setBorder( undefined ), [ setBorder ] ); + const onBorderChange = useCallback( + ( newBorder ) => { + // Ensure we have a visible border style when a border width or + // color is being selected. + const newBorderWithStyle = applyAllFallbackStyles( newBorder ); + + // As we can't conditionally generate styles based on if other + // style properties have been set we need to force split border + // definitions for user set border styles. Border radius is derived + // from the same property i.e. `border.radius` if it is a string + // that is used. The longhand border radii styles are only generated + // if that property is an object. + // + // For borders (color, style, and width) those are all properties on + // the `border` style property. This means if the theme.json defined + // split borders and the user condenses them into a flat border or + // vice-versa we'd get both sets of styles which would conflict. + const updatedBorder = ! hasSplitBorders( newBorderWithStyle ) + ? { + top: newBorderWithStyle, + right: newBorderWithStyle, + bottom: newBorderWithStyle, + left: newBorderWithStyle, + } + : { + color: null, + style: null, + width: null, + ...newBorderWithStyle, + }; + + // As radius is maintained separately to color, style, and width + // maintain its value. Undefined values here will be cleaned when + // global styles are saved. + setBorder( { radius: border?.radius, ...updatedBorder } ); + }, + [ setBorder ] + ); + return ( - { showBorderWidth && ( - - - - ) } - { showBorderStyle && ( - - - - ) } - { showBorderColor && ( + { ( showBorderWidth || showBorderColor ) && ( isDefinedBorder( userBorderStyles ) } + label={ __( 'Border' ) } + onDeselect={ () => resetBorder() } isShownByDefault={ true } > - ) } @@ -202,12 +203,14 @@ export default function BorderPanel( { name } ) { setBorderRadius( undefined ) } isShownByDefault={ true } > { + setBorderRadius( value || undefined ); + } } /> ) } diff --git a/packages/edit-site/src/components/sidebar/style.scss b/packages/edit-site/src/components/sidebar/style.scss index b0e3cdfd1463e..ff47afcccdc0a 100644 --- a/packages/edit-site/src/components/sidebar/style.scss +++ b/packages/edit-site/src/components/sidebar/style.scss @@ -86,3 +86,52 @@ font-size: $helptext-font-size; line-height: 1; } + + +.edit-site-global-styles-sidebar__border-box-control__popover, +.edit-site-global-styles-sidebar__border-box-control__popover-top, +.edit-site-global-styles-sidebar__border-box-control__popover-right, +.edit-site-global-styles-sidebar__border-box-control__popover-bottom, +.edit-site-global-styles-sidebar__border-box-control__popover-left { + .components-popover__content { + width: 282px; + } +} + +$split-border-control-offset: 55px; + +@include break-medium() { + .edit-site-global-styles-sidebar__border-box-control__popover, + .edit-site-global-styles-sidebar__border-box-control__popover-left { + .components-popover__content { + margin-right: #{ $grid-unit-50 + $grid-unit-15 } !important; + } + } + + .edit-site-global-styles-sidebar__border-box-control__popover-top, + .edit-site-global-styles-sidebar__border-box-control__popover-bottom { + .components-popover__content { + margin-right: #{ $grid-unit-50 + $grid-unit-15 + $split-border-control-offset } !important; + } + } + + .edit-site-global-styles-sidebar__border-box-control__popover-right { + .components-popover__content { + margin-right: #{ $grid-unit-50 + $grid-unit-15 + ( $split-border-control-offset * 2 )} !important; + } + } + + .edit-site-global-styles-sidebar__border-box-control__popover, + .edit-site-global-styles-sidebar__border-box-control__popover-top, + .edit-site-global-styles-sidebar__border-box-control__popover-right, + .edit-site-global-styles-sidebar__border-box-control__popover-bottom, + .edit-site-global-styles-sidebar__border-box-control__popover-left { + &.is-from-top .components-popover__content { + margin-top: #{ -($grid-unit-50 + $grid-unit-15) } !important; + } + + &.is-from-bottom .components-popover__content { + margin-bottom: #{ -($grid-unit-50 + $grid-unit-15) } !important; + } + } +} diff --git a/phpunit/block-supports/border-test.php b/phpunit/block-supports/border-test.php index 11e9891c192c9..3ec9bbe4f5387 100644 --- a/phpunit/block-supports/border-test.php +++ b/phpunit/block-supports/border-test.php @@ -23,8 +23,16 @@ function tearDown() { parent::tearDown(); } - function test_border_color_slug_with_numbers_is_kebab_cased_properly() { - $this->test_block_name = 'test/border-color-slug-with-numbers-is-kebab-cased-properly'; + /** + * Registers a new block for testing border support. + * + * @param string $block_name Name for the test block. + * @param array $supports Array defining block support configuration. + * + * @return WP_Block_Type The block type for the newly registered test block. + */ + private function register_bordered_block_with_support( $block_name, $supports = array() ) { + $this->test_block_name = $block_name; register_block_type( $this->test_block_name, array( @@ -37,18 +45,64 @@ function test_border_color_slug_with_numbers_is_kebab_cased_properly() { 'type' => 'object', ), ), - 'supports' => array( - '__experimentalBorder' => array( - 'color' => true, - 'radius' => true, - 'width' => true, - 'style' => true, - ), + 'supports' => $supports, + ) + ); + $registry = WP_Block_Type_Registry::get_instance(); + + return $registry->get_registered( $this->test_block_name ); + } + + function test_border_object_with_no_styles() { + $block_type = self::register_bordered_block_with_support( + 'test/border-object-with-no-styles', + array( + '__experimentalBorder' => array( + 'color' => true, + 'radius' => true, + 'width' => true, + 'style' => true, + ), + ) + ); + $block_attrs = array( 'style' => array( 'border' => array() ) ); + $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); + $expected = array(); + + $this->assertSame( $expected, $actual ); + } + + function test_border_object_with_invalid_style_prop() { + $block_type = self::register_bordered_block_with_support( + 'test/border-object-with-invalid-style-prop', + array( + '__experimentalBorder' => array( + 'color' => true, + 'radius' => true, + 'width' => true, + 'style' => true, + ), + ) + ); + $block_attrs = array( 'style' => array( 'border' => array( 'invalid' => '10px' ) ) ); + $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); + $expected = array(); + + $this->assertSame( $expected, $actual ); + } + + function test_border_color_slug_with_numbers_is_kebab_cased_properly() { + $block_type = self::register_bordered_block_with_support( + 'test/border-color-slug-with-numbers-is-kebab-cased-properly', + array( + '__experimentalBorder' => array( + 'color' => true, + 'radius' => true, + 'width' => true, + 'style' => true, ), ) ); - $registry = WP_Block_Type_Registry::get_instance(); - $block_type = $registry->get_registered( $this->test_block_name ); $block_atts = array( 'borderColor' => 'red', 'style' => array( @@ -69,30 +123,19 @@ function test_border_color_slug_with_numbers_is_kebab_cased_properly() { $this->assertSame( $expected, $actual ); } - function test_border_with_skipped_serialization_block_supports() { - $this->test_block_name = 'test/border-with-skipped-serialization-block-supports'; - register_block_type( - $this->test_block_name, + function test_flat_border_with_skipped_serialization() { + $block_type = self::register_bordered_block_with_support( + 'test/flat-border-with-skipped-serialization', array( - 'api_version' => 2, - 'attributes' => array( - 'style' => array( - 'type' => 'object', - ), - ), - 'supports' => array( - '__experimentalBorder' => array( - 'color' => true, - 'radius' => true, - 'width' => true, - 'style' => true, - '__experimentalSkipSerialization' => true, - ), + '__experimentalBorder' => array( + 'color' => true, + 'radius' => true, + 'width' => true, + 'style' => true, + '__experimentalSkipSerialization' => true, ), ) ); - $registry = WP_Block_Type_Registry::get_instance(); - $block_type = $registry->get_registered( $this->test_block_name ); $block_atts = array( 'style' => array( 'border' => array( @@ -110,30 +153,19 @@ function test_border_with_skipped_serialization_block_supports() { $this->assertSame( $expected, $actual ); } - function test_radius_with_individual_skipped_serialization_block_supports() { - $this->test_block_name = 'test/radius-with-individual-skipped-serialization-block-supports'; - register_block_type( - $this->test_block_name, + function test_flat_border_with_individual_skipped_serialization() { + $block_type = self::register_bordered_block_with_support( + 'test/flat-border-with-individual-skipped-serialization', array( - 'api_version' => 2, - 'attributes' => array( - 'style' => array( - 'type' => 'object', - ), - ), - 'supports' => array( - '__experimentalBorder' => array( - 'color' => true, - 'radius' => true, - 'width' => true, - 'style' => true, - '__experimentalSkipSerialization' => array( 'radius', 'color' ), - ), + '__experimentalBorder' => array( + 'color' => true, + 'radius' => true, + 'width' => true, + 'style' => true, + '__experimentalSkipSerialization' => array( 'radius', 'color' ), ), ) ); - $registry = WP_Block_Type_Registry::get_instance(); - $block_type = $registry->get_registered( $this->test_block_name ); $block_atts = array( 'style' => array( 'border' => array( @@ -152,4 +184,279 @@ function test_radius_with_individual_skipped_serialization_block_supports() { $this->assertSame( $expected, $actual ); } + + function test_split_border_radius() { + $block_type = self::register_bordered_block_with_support( + 'test/split-border-radius', + array( + '__experimentalBorder' => array( + 'radius' => true, + ), + ) + ); + $block_attrs = array( + 'style' => array( + 'border' => array( + 'radius' => array( + 'topLeft' => '1em', + 'topRight' => '2rem', + 'bottomLeft' => '30px', + 'bottomRight' => '4vh', + ), + ), + ), + ); + $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); + $expected = array( + 'style' => 'border-top-left-radius: 1em; border-top-right-radius: 2rem; border-bottom-left-radius: 30px; border-bottom-right-radius: 4vh;', + ); + + $this->assertSame( $expected, $actual ); + } + + function test_flat_border_with_custom_color() { + $block_type = self::register_bordered_block_with_support( + 'test/flat-border-with-custom-color', + array( + '__experimentalBorder' => array( + 'color' => true, + 'width' => true, + 'style' => true, + ), + ) + ); + $block_attrs = array( + 'style' => array( + 'border' => array( + 'color' => '#72aee6', + 'width' => '2px', + 'style' => 'dashed', + ), + ), + ); + $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); + $expected = array( + 'class' => 'has-border-color', + 'style' => 'border-style: dashed; border-width: 2px; border-color: #72aee6;', + ); + + $this->assertSame( $expected, $actual ); + } + + function test_split_borders_with_custom_colors() { + $block_type = self::register_bordered_block_with_support( + 'test/split-borders-with-custom-colors', + array( + '__experimentalBorder' => array( + 'color' => true, + 'width' => true, + 'style' => true, + ), + ) + ); + $block_attrs = array( + 'style' => array( + 'border' => array( + 'top' => array( + 'color' => '#72aee6', + 'width' => '2px', + 'style' => 'dashed', + ), + 'right' => array( + 'color' => '#e65054', + 'width' => '0.25rem', + 'style' => 'dotted', + ), + 'bottom' => array( + 'color' => '#007017', + 'width' => '0.5em', + 'style' => 'solid', + ), + 'left' => array( + 'color' => '#f6f7f7', + 'width' => '1px', + 'style' => 'solid', + ), + ), + ), + ); + $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); + $expected = array( + 'style' => 'border-top-width: 2px; border-top-style: dashed; border-top-color: #72aee6; border-right-width: 0.25rem; border-right-style: dotted; border-right-color: #e65054; border-bottom-width: 0.5em; border-bottom-style: solid; border-bottom-color: #007017; border-left-width: 1px; border-left-style: solid; border-left-color: #f6f7f7;', + ); + + $this->assertSame( $expected, $actual ); + } + + function test_split_borders_with_skipped_serialization() { + $block_type = self::register_bordered_block_with_support( + 'test/split-borders-with-skipped-serialization', + array( + '__experimentalBorder' => array( + 'color' => true, + 'width' => true, + 'style' => true, + '__experimentalSkipSerialization' => true, + ), + ) + ); + $block_attrs = array( + 'style' => array( + 'border' => array( + 'top' => array( + 'color' => '#72aee6', + 'width' => '2px', + 'style' => 'dashed', + ), + 'right' => array( + 'color' => '#e65054', + 'width' => '0.25rem', + 'style' => 'dotted', + ), + 'bottom' => array( + 'color' => '#007017', + 'width' => '0.5em', + 'style' => 'solid', + ), + 'left' => array( + 'color' => '#f6f7f7', + 'width' => '1px', + 'style' => 'solid', + ), + ), + ), + ); + $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); + $expected = array(); + + $this->assertSame( $expected, $actual ); + } + + function test_split_borders_with_skipped_individual_feature_serialization() { + $block_type = self::register_bordered_block_with_support( + 'test/split-borders-with-skipped-individual-feature-serialization', + array( + '__experimentalBorder' => array( + 'color' => true, + 'width' => true, + 'style' => true, + '__experimentalSkipSerialization' => array( 'width', 'style' ), + ), + ) + ); + $block_attrs = array( + 'style' => array( + 'border' => array( + 'top' => array( + 'color' => '#72aee6', + 'width' => '2px', + 'style' => 'dashed', + ), + 'right' => array( + 'color' => '#e65054', + 'width' => '0.25rem', + 'style' => 'dotted', + ), + 'bottom' => array( + 'color' => '#007017', + 'width' => '0.5em', + 'style' => 'solid', + ), + 'left' => array( + 'color' => '#f6f7f7', + 'width' => '1px', + 'style' => 'solid', + ), + ), + ), + ); + $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); + $expected = array( + 'style' => 'border-top-color: #72aee6; border-right-color: #e65054; border-bottom-color: #007017; border-left-color: #f6f7f7;', + ); + + $this->assertSame( $expected, $actual ); + } + + function test_partial_split_borders() { + $block_type = self::register_bordered_block_with_support( + 'test/partial-split-borders', + array( + '__experimentalBorder' => array( + 'color' => true, + 'width' => true, + 'style' => true, + ), + ) + ); + $block_attrs = array( + 'style' => array( + 'border' => array( + 'top' => array( + 'color' => '#72aee6', + 'width' => '2px', + 'style' => 'dashed', + ), + 'right' => array( + 'color' => '#e65054', + 'width' => '0.25rem', + ), + 'left' => array( + 'style' => 'solid', + ), + ), + ), + ); + $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); + $expected = array( + 'style' => 'border-top-width: 2px; border-top-style: dashed; border-top-color: #72aee6; border-right-width: 0.25rem; border-right-color: #e65054; border-left-style: solid;', + ); + + $this->assertSame( $expected, $actual ); + } + + function test_split_borders_with_named_colors() { + $block_type = self::register_bordered_block_with_support( + 'test/split-borders-with-named-colors', + array( + '__experimentalBorder' => array( + 'color' => true, + 'width' => true, + 'style' => true, + ), + ) + ); + $block_attrs = array( + 'style' => array( + 'border' => array( + 'top' => array( + 'width' => '2px', + 'style' => 'dashed', + 'color' => 'var:preset|color|red', + ), + 'right' => array( + 'width' => '0.25rem', + 'style' => 'dotted', + 'color' => 'var:preset|color|green', + ), + 'bottom' => array( + 'width' => '0.5em', + 'style' => 'solid', + 'color' => 'var:preset|color|blue', + ), + 'left' => array( + 'width' => '1px', + 'style' => 'solid', + 'color' => 'var:preset|color|yellow', + ), + ), + ), + ); + $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); + $expected = array( + 'style' => 'border-top-width: 2px; border-top-style: dashed; border-top-color: var(--wp--preset--color--red); border-right-width: 0.25rem; border-right-style: dotted; border-right-color: var(--wp--preset--color--green); border-bottom-width: 0.5em; border-bottom-style: solid; border-bottom-color: var(--wp--preset--color--blue); border-left-width: 1px; border-left-style: solid; border-left-color: var(--wp--preset--color--yellow);', + ); + + $this->assertSame( $expected, $actual ); + } } diff --git a/schemas/json/theme.json b/schemas/json/theme.json index ce276b8f33047..75e346e0982f5 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -681,7 +681,32 @@ }, "radius": { "description": "Sets the `border-radius` CSS property.", - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "topLeft": { + "description": "Sets the `border-top-left-radius` CSS property.", + "type": "string" + }, + "topRight": { + "description": "Sets the `border-top-right-radius` CSS property.", + "type": "string" + }, + "bottomLeft": { + "description": "Sets the `border-bottom-left-radius` CSS property.", + "type": "string" + }, + "bottomRight": { + "description": "Sets the `border-bottom-right-radius` CSS property.", + "type": "string" + } + } + } + ] }, "style": { "description": "Sets the `border-style` CSS property.", @@ -690,6 +715,62 @@ "width": { "description": "Sets the `border-width` CSS property.", "type": "string" + }, + "top": { + "color": { + "description": "Sets the `border-top-color` CSS property.", + "type": "string" + }, + "style": { + "description": "Sets the `border-top-style` CSS property.", + "type": "string" + }, + "width": { + "description": "Sets the `border-top-width` CSS property.", + "type": "string" + } + }, + "right": { + "color": { + "description": "Sets the `border-right-color` CSS property.", + "type": "string" + }, + "style": { + "description": "Sets the `border-right-style` CSS property.", + "type": "string" + }, + "width": { + "description": "Sets the `border-right-width` CSS property.", + "type": "string" + } + }, + "bottom": { + "color": { + "description": "Sets the `border-bottom-color` CSS property.", + "type": "string" + }, + "style": { + "description": "Sets the `border-bottom-style` CSS property.", + "type": "string" + }, + "width": { + "description": "Sets the `border-bottom-width` CSS property.", + "type": "string" + } + }, + "left": { + "color": { + "description": "Sets the `border-left-color` CSS property.", + "type": "string" + }, + "style": { + "description": "Sets the `border-left-style` CSS property.", + "type": "string" + }, + "width": { + "description": "Sets the `border-left-width` CSS property.", + "type": "string" + } } }, "additionalProperties": false