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 fd4c9c7113900b..632bf534e5cd4b 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 5ff0260944df2b..d4d18fd6cb687e 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 8868185360acdb..668082863114fa 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 becab0d9de11e5..a08e89250df2b2 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 204d4b7e0d28f0..00000000000000
--- 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 1a3a5f923383c7..00000000000000
--- 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 f6ce67d781f34f..00000000000000
--- 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 f17eeab4e3de93..be5c03b1a8c0e1 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 c86c046309d886..0aab500b550764 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 708dae62a39714..b18a175973c1d5 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 d62d68365fe1b3..0afe1bb70cb5a8 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 2244a4c4fe79e1..5819a43c93ed52 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 05faf4624bea90..1457423e4d60e0 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 94ec6fc4ac7f53..8bf1e5e5ee2db1 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 efd28d1c7d6249..dea27caa360498 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 03003030833796..ca6cfc3d0da8cb 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 a617d2261234e1..52ff4f640aac8e 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 c9b8d3e0e8f1d3..07e8bb1ddb580a 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 5780797e2c186b..780b165f785703 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 b0e3cdfd1463e7..ff47afcccdc0ab 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 11e9891c192c95..3ec9bbe4f53874 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 ce276b8f330474..75e346e0982f5b 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