diff --git a/lib/block-supports/border.php b/lib/block-supports/border.php
index 0a29d3753cecc2..3bdc2253534c97 100644
--- a/lib/block-supports/border.php
+++ b/lib/block-supports/border.php
@@ -6,26 +6,32 @@
*/
/**
- * Registers the style attribute used by the border feature if needed for block types that
- * support borders.
+ * Registers the style attribute used by the border feature if needed for block
+ * types that support borders.
*
* @param WP_Block_Type $block_type Block Type.
*/
function gutenberg_register_border_support( $block_type ) {
- // Determine border related features supported.
- // Border width, style etc can be added in the future.
- $has_border_radius_support = gutenberg_block_has_support( $block_type, array( '__experimentalBorder', 'radius' ), false );
+ // Determine if any border related features are supported.
+ $has_border_support = gutenberg_block_has_support( $block_type, array( '__experimentalBorder' ) );
+ $has_border_color_support = gutenberg_block_has_support( $block_type, array( '__experimentalBorder', 'color' ) );
// Setup attributes and styles within that if needed.
if ( ! $block_type->attributes ) {
$block_type->attributes = array();
}
- if ( $has_border_radius_support && ! array_key_exists( 'style', $block_type->attributes ) ) {
+ if ( $has_border_support && ! array_key_exists( 'style', $block_type->attributes ) ) {
$block_type->attributes['style'] = array(
'type' => 'object',
);
}
+
+ if ( $has_border_color_support && ! array_key_exists( 'borderColor', $block_type->attributes ) ) {
+ $block_type->attributes['borderColor'] = array(
+ 'type' => 'string',
+ );
+ }
}
/**
@@ -38,33 +44,64 @@ function gutenberg_register_border_support( $block_type ) {
* @return array Border CSS classes and inline styles.
*/
function gutenberg_apply_border_support( $block_type, $block_attributes ) {
- $border_support = _wp_array_get( $block_type->supports, array( '__experimentalBorder' ), false );
+ if ( gutenberg_skip_border_serialization( $block_type ) ) {
+ return array();
+ }
+ $classes = array();
+ $styles = array();
+
+ // Border radius.
if (
- is_array( $border_support ) &&
- array_key_exists( '__experimentalSkipSerialization', $border_support ) &&
- $border_support['__experimentalSkipSerialization']
+ gutenberg_block_has_support( $block_type, array( '__experimentalBorder', 'radius' ) ) &&
+ isset( $block_attributes['style']['border']['radius'] )
) {
- return array();
+ $border_radius = (int) $block_attributes['style']['border']['radius'];
+ $styles[] = sprintf( 'border-radius: %dpx;', $border_radius );
}
- // Arrays used to ease addition of further border related features in future.
- $styles = array();
+ // Border style.
+ if (
+ gutenberg_block_has_support( $block_type, array( '__experimentalBorder', 'style' ) ) &&
+ isset( $block_attributes['style']['border']['style'] )
+ ) {
+ $border_style = $block_attributes['style']['border']['style'];
+ $styles[] = sprintf( 'border-style: %s;', $border_style );
+ }
- // Border Radius.
- $has_border_radius_support = gutenberg_block_has_support( $block_type, array( '__experimentalBorder', 'radius' ), false );
- if ( $has_border_radius_support ) {
- if ( isset( $block_attributes['style']['border']['radius'] ) ) {
- $border_radius = (int) $block_attributes['style']['border']['radius'];
- $styles[] = sprintf( 'border-radius: %dpx;', $border_radius );
- }
+ // Border width.
+ if (
+ gutenberg_block_has_support( $block_type, array( '__experimentalBorder', 'width' ) ) &&
+ isset( $block_attributes['style']['border']['width'] )
+ ) {
+ $border_width = intval( $block_attributes['style']['border']['width'] );
+ $styles[] = sprintf( 'border-width: %dpx;', $border_width );
}
- // Border width, style etc can be added here.
+ // Border color.
+ if ( gutenberg_block_has_support( $block_type, array( '__experimentalBorder', 'color' ) ) ) {
+ $has_named_border_color = array_key_exists( 'borderColor', $block_attributes );
+ $has_custom_border_color = isset( $block_attributes['style']['border']['color'] );
+
+ if ( $has_named_border_color || $has_custom_border_color ) {
+ $classes[] = 'has-border-color';
+ }
+
+ if ( $has_named_border_color ) {
+ $classes[] = sprintf( 'has-%s-border-color', $block_attributes['borderColor'] );
+ } elseif ( $has_custom_border_color ) {
+ $border_color = $block_attributes['style']['border']['color'];
+ $styles[] = sprintf( 'border-color: %s;', $border_color );
+ }
+ }
// Collect classes and styles.
$attributes = array();
+ if ( ! empty( $classes ) ) {
+ $attributes['class'] = implode( ' ', $classes );
+ }
+
if ( ! empty( $styles ) ) {
$attributes['style'] = implode( ' ', $styles );
}
@@ -72,6 +109,22 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) {
return $attributes;
}
+/**
+ * Checks whether serialization of the current block's border properties should
+ * occur.
+ *
+ * @param WP_Block_type $block_type Block type.
+ *
+ * @return boolean
+ */
+function gutenberg_skip_border_serialization( $block_type ) {
+ $border_support = _wp_array_get( $block_type->supports, array( '__experimentalBorder' ), false );
+
+ return is_array( $border_support ) &&
+ array_key_exists( '__experimentalSkipSerialization', $border_support ) &&
+ $border_support['__experimentalSkipSerialization'];
+}
+
// Register the block support.
WP_Block_Supports::get_instance()->register(
'border',
diff --git a/lib/class-wp-theme-json.php b/lib/class-wp-theme-json.php
index 147725d66a19b2..3a7ccbd7c8c2c3 100644
--- a/lib/class-wp-theme-json.php
+++ b/lib/class-wp-theme-json.php
@@ -198,6 +198,10 @@ class WP_Theme_JSON {
'class_suffix' => 'background-color',
'property_name' => 'background-color',
),
+ array(
+ 'class_suffix' => 'border-color',
+ 'property_name' => 'border-color',
+ ),
),
),
array(
diff --git a/lib/experimental-default-theme.json b/lib/experimental-default-theme.json
index da910ca84747a7..dea9d96966d34e 100644
--- a/lib/experimental-default-theme.json
+++ b/lib/experimental-default-theme.json
@@ -171,7 +171,10 @@
"units": [ "px", "em", "rem", "vh", "vw" ]
},
"border": {
- "customRadius": false
+ "customColor": false,
+ "customRadius": false,
+ "customStyle": false,
+ "customWidth": false
}
}
}
diff --git a/lib/global-styles.php b/lib/global-styles.php
index 4bef8e7b93751c..c3e7a6da9cfcf1 100644
--- a/lib/global-styles.php
+++ b/lib/global-styles.php
@@ -260,6 +260,7 @@ function gutenberg_global_styles_include_support_for_wp_variables( $allow_css, $
$allowed_preset_attributes = array(
'background',
'background-color',
+ 'border-color',
'color',
'font-family',
'font-size',
diff --git a/packages/block-editor/src/components/border-style-control/index.js b/packages/block-editor/src/components/border-style-control/index.js
new file mode 100644
index 00000000000000..ee2ad2482cd3c4
--- /dev/null
+++ b/packages/block-editor/src/components/border-style-control/index.js
@@ -0,0 +1,64 @@
+/**
+ * WordPress dependencies
+ */
+import { CustomSelectControl } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+const DEFAULT_STYLE = {
+ key: 'default',
+ name: __( 'Default' ),
+ style: { borderStyle: undefined },
+};
+
+const BORDER_STYLES = [
+ DEFAULT_STYLE,
+ {
+ key: 'none',
+ name: __( 'None' ),
+ style: { borderStyle: 'none' },
+ },
+ {
+ key: 'solid',
+ name: __( 'Solid' ),
+ style: { borderStyle: 'solid' },
+ },
+ {
+ key: 'dashed',
+ name: __( 'Dashed' ),
+ style: { borderStyle: 'dashed' },
+ },
+ {
+ key: 'dotted',
+ name: __( 'Dotted' ),
+ style: { borderStyle: 'dotted' },
+ },
+];
+
+/**
+ * Control to display border style options.
+ *
+ * @param {Object} props Component props.
+ * @param {Object} props.onChange Handler for changing border style selection.
+ * @param {Object} props.value Currently selected border style value.
+ *
+ * @return {WPElement} Custom border style select control.
+ */
+export default function BorderStyleControl( { onChange, value } ) {
+ const style = BORDER_STYLES.find( ( option ) => option.key === value );
+
+ return (
+
+ );
+}
diff --git a/packages/block-editor/src/components/border-style-control/style.scss b/packages/block-editor/src/components/border-style-control/style.scss
new file mode 100644
index 00000000000000..827f31bca718c5
--- /dev/null
+++ b/packages/block-editor/src/components/border-style-control/style.scss
@@ -0,0 +1,14 @@
+.components-border-style-control__select {
+ margin-bottom: 24px;
+
+ button {
+ width: 100%;
+ }
+
+ ul {
+ li,
+ li:last-child {
+ margin: 6px;
+ }
+ }
+}
diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js
index ac7471317acbe4..dc88ae5a36deb3 100644
--- a/packages/block-editor/src/components/index.js
+++ b/packages/block-editor/src/components/index.js
@@ -32,6 +32,7 @@ export {
BlockVerticalAlignmentToolbar,
BlockVerticalAlignmentControl,
} from './block-vertical-alignment-control';
+export { default as __experimentalBorderStyleControl } from './border-style-control';
export { default as ButtonBlockerAppender } from './button-block-appender';
export { default as ColorPalette } from './color-palette';
export { default as ColorPaletteControl } from './color-palette/control';
diff --git a/packages/block-editor/src/hooks/border-color.js b/packages/block-editor/src/hooks/border-color.js
new file mode 100644
index 00000000000000..511983d35fab1c
--- /dev/null
+++ b/packages/block-editor/src/hooks/border-color.js
@@ -0,0 +1,234 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { addFilter } from '@wordpress/hooks';
+import { __ } from '@wordpress/i18n';
+import { createHigherOrderComponent } from '@wordpress/compose';
+
+/**
+ * Internal dependencies
+ */
+import ColorGradientControl from '../components/colors-gradients/control';
+import {
+ getColorClassName,
+ getColorObjectByColorValue,
+ getColorObjectByAttributeValues,
+} from '../components/colors';
+import useEditorFeature from '../components/use-editor-feature';
+import { hasBorderSupport, shouldSkipSerialization } from './border';
+import { cleanEmptyObject } 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 colors = useEditorFeature( 'color.palette' ) || EMPTY_ARRAY;
+
+ const disableCustomColors = ! useEditorFeature( 'color.custom' );
+ const disableCustomGradients = ! useEditorFeature( 'color.customGradient' );
+
+ const onChangeColor = ( value ) => {
+ const colorObject = getColorObjectByColorValue( colors, 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,
+ } );
+ };
+
+ return (
+
+ );
+}
+
+/**
+ * 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 )
+ ) {
+ 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 )
+ ) {
+ 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 = useEditorFeature( 'color.palette' ) || EMPTY_ARRAY;
+
+ if (
+ ! hasBorderSupport( name, 'color' ) ||
+ shouldSkipSerialization( name )
+ ) {
+ 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-radius.js b/packages/block-editor/src/hooks/border-radius.js
index 7ced3df65401a6..50e9dece97eaf6 100644
--- a/packages/block-editor/src/hooks/border-radius.js
+++ b/packages/block-editor/src/hooks/border-radius.js
@@ -1,15 +1,12 @@
/**
* WordPress dependencies
*/
-import { getBlockSupport } from '@wordpress/blocks';
import { RangeControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
-import useEditorFeature from '../components/use-editor-feature';
-import { BORDER_SUPPORT_KEY } from './border';
import { cleanEmptyObject } from './utils';
const MIN_BORDER_RADIUS_VALUE = 0;
@@ -27,10 +24,6 @@ export function BorderRadiusEdit( props ) {
setAttributes,
} = props;
- if ( useIsBorderRadiusDisabled( props ) ) {
- return null;
- }
-
const onChange = ( newRadius ) => {
let newStyle = {
...style,
@@ -59,25 +52,3 @@ export function BorderRadiusEdit( props ) {
/>
);
}
-
-/**
- * Determines if there is border radius support.
- *
- * @param {string|Object} blockType Block name or Block Type object.
- * @return {boolean} Whether there is support.
- */
-export function hasBorderRadiusSupport( blockType ) {
- const support = getBlockSupport( blockType, BORDER_SUPPORT_KEY );
- return !! ( true === support || support?.radius );
-}
-
-/**
- * Custom hook that checks if border radius settings have been disabled.
- *
- * @param {string} name The name of the block.
- * @return {boolean} Whether border radius setting is disabled.
- */
-export function useIsBorderRadiusDisabled( { name: blockName } = {} ) {
- const isDisabled = ! useEditorFeature( 'border.customRadius' );
- return ! hasBorderRadiusSupport( blockName ) || isDisabled;
-}
diff --git a/packages/block-editor/src/hooks/border-style.js b/packages/block-editor/src/hooks/border-style.js
new file mode 100644
index 00000000000000..9f1b1e49b21433
--- /dev/null
+++ b/packages/block-editor/src/hooks/border-style.js
@@ -0,0 +1,37 @@
+/**
+ * Internal dependencies
+ */
+import BorderStyleControl from '../components/border-style-control';
+import { cleanEmptyObject } from './utils';
+
+/**
+ * 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 (
+
+ );
+};
diff --git a/packages/block-editor/src/hooks/border-width.js b/packages/block-editor/src/hooks/border-width.js
new file mode 100644
index 00000000000000..24e25f6a063850
--- /dev/null
+++ b/packages/block-editor/src/hooks/border-width.js
@@ -0,0 +1,50 @@
+/**
+ * WordPress dependencies
+ */
+import { RangeControl } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { cleanEmptyObject } from './utils';
+
+const MIN_BORDER_WIDTH = 0;
+const MAX_BORDER_WIDTH = 50;
+
+/**
+ * Inspector control for configuring border width property.
+ *
+ * @param {Object} props Block properties.
+ * @return {WPElement} Border width edit element.
+ */
+export const BorderWidthEdit = ( props ) => {
+ const {
+ attributes: { style },
+ setAttributes,
+ } = props;
+
+ const onChange = ( newWidth ) => {
+ const newStyle = {
+ ...style,
+ border: {
+ ...style?.border,
+ width: newWidth,
+ },
+ };
+
+ setAttributes( { style: cleanEmptyObject( newStyle ) } );
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js
index 31bb6f4170c27b..9b4aa499f54cdd 100644
--- a/packages/block-editor/src/hooks/border.js
+++ b/packages/block-editor/src/hooks/border.js
@@ -10,7 +10,11 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import InspectorControls from '../components/inspector-controls';
-import { BorderRadiusEdit, useIsBorderRadiusDisabled } from './border-radius';
+import useEditorFeature from '../components/use-editor-feature';
+import { BorderColorEdit } from './border-color';
+import { BorderRadiusEdit } from './border-radius';
+import { BorderStyleEdit } from './border-style';
+import { BorderWidthEdit } from './border-width';
export const BORDER_SUPPORT_KEY = '__experimentalBorder';
@@ -18,50 +22,92 @@ export function BorderPanel( props ) {
const isDisabled = useIsBorderDisabled( props );
const isSupported = hasBorderSupport( props.name );
+ const isColorSupported =
+ useEditorFeature( 'border.customColor' ) &&
+ hasBorderSupport( props.name, 'color' );
+
+ const isRadiusSupported =
+ useEditorFeature( 'border.customRadius' ) &&
+ hasBorderSupport( props.name, 'radius' );
+
+ const isStyleSupported =
+ useEditorFeature( 'border.customStyle' ) &&
+ hasBorderSupport( props.name, 'style' );
+
+ const isWidthSupported =
+ useEditorFeature( 'border.customWidth' ) &&
+ hasBorderSupport( props.name, 'width' );
+
if ( isDisabled || ! isSupported ) {
return null;
}
return (
-
-
+
+ { isStyleSupported && }
+ { isWidthSupported && }
+ { isRadiusSupported && }
+ { isColorSupported && }
);
}
/**
- * Determine whether there is block support for borders.
+ * Determine whether there is block support for border properties.
*
- * @param {string} blockName Block name.
- * @return {boolean} Whether there is support.
+ * @param {string} blockName Block name.
+ * @param {string} feature Border feature to check support for.
+ * @return {boolean} Whether there is support.
*/
-export function hasBorderSupport( blockName ) {
+export function hasBorderSupport( blockName, feature = 'any' ) {
if ( Platform.OS !== 'web' ) {
return false;
}
const support = getBlockSupport( blockName, BORDER_SUPPORT_KEY );
- // Further border properties to be added in future iterations.
- // e.g. support && ( support.radius || support.width || support.style )
- return !! ( true === support || support?.radius );
+ if ( support === true ) {
+ return true;
+ }
+
+ if ( feature === 'any' ) {
+ return !! (
+ support?.color ||
+ support?.radius ||
+ support?.width ||
+ support?.style
+ );
+ }
+
+ return !! support?.[ feature ];
}
/**
- * Determines whether there is any block support for borders e.g. border radius,
- * style, width etc.
+ * Check whether serialization of border classes and styles should be skipped.
*
- * @param {Object} props Block properties.
- * @return {boolean} If border support is completely disabled.
+ * @param {string|Object} blockType Block name or block type object.
+ * @return {boolean} Whether serialization of border properties should occur.
*/
-const useIsBorderDisabled = ( props = {} ) => {
- // Further border properties to be added in future iterations.
- // e.g. const configs = [
- // useIsBorderRadiusDisabled( props ),
- // useIsBorderWidthDisabled( props ),
- // ];
- const configs = [ useIsBorderRadiusDisabled( props ) ];
+export function shouldSkipSerialization( blockType ) {
+ const support = getBlockSupport( blockType, BORDER_SUPPORT_KEY );
+
+ return support?.__experimentalSkipSerialization;
+}
+
+/**
+ * Determines if all border support features have been disabled.
+ *
+ * @return {boolean} If border support is completely disabled.
+ */
+const useIsBorderDisabled = () => {
+ const configs = [
+ ! useEditorFeature( 'border.customColor' ),
+ ! useEditorFeature( 'border.customRadius' ),
+ ! useEditorFeature( 'border.customStyle' ),
+ ! useEditorFeature( 'border.customWidth' ),
+ ];
+
return configs.every( Boolean );
};
diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js
index c087e994ba9eb9..49b11cc8b4074b 100644
--- a/packages/block-editor/src/hooks/index.js
+++ b/packages/block-editor/src/hooks/index.js
@@ -8,4 +8,5 @@ import './generated-class-name';
import './style';
import './color';
import './font-size';
+import './border-color';
import './layout';
diff --git a/packages/block-editor/src/hooks/test/style.js b/packages/block-editor/src/hooks/test/style.js
index afa1d45856b2a4..0cd3c70d9f2b64 100644
--- a/packages/block-editor/src/hooks/test/style.js
+++ b/packages/block-editor/src/hooks/test/style.js
@@ -17,11 +17,19 @@ describe( 'getInlineStyles', () => {
getInlineStyles( {
color: { text: 'red', background: 'black' },
typography: { lineHeight: 1.5, fontSize: 10 },
- border: { radius: 10 },
+ border: {
+ radius: 10,
+ width: 3,
+ style: 'dotted',
+ color: '#21759b',
+ },
} )
).toEqual( {
backgroundColor: 'black',
+ borderColor: '#21759b',
borderRadius: 10,
+ borderStyle: 'dotted',
+ borderWidth: 3,
color: 'red',
lineHeight: 1.5,
fontSize: 10,
diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss
index f80eeefd3be42d..745e64a980d2a9 100644
--- a/packages/block-editor/src/style.scss
+++ b/packages/block-editor/src/style.scss
@@ -27,6 +27,7 @@
@import "./components/block-types-list/style.scss";
@import "./components/block-variation-picker/style.scss";
@import "./components/block-variation-transforms/style.scss";
+@import "./components/border-style-control/style.scss";
@import "./components/button-block-appender/style.scss";
@import "./components/colors-gradients/style.scss";
@import "./components/contrast-checker/style.scss";
diff --git a/packages/edit-site/src/components/sidebar/border-panel.js b/packages/edit-site/src/components/sidebar/border-panel.js
new file mode 100644
index 00000000000000..9b95c4d861b564
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar/border-panel.js
@@ -0,0 +1,147 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ __experimentalBorderStyleControl as BorderStyleControl,
+ __experimentalColorGradientControl as ColorGradientControl,
+} from '@wordpress/block-editor';
+import { PanelBody, RangeControl } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { useEditorFeature } from '../editor/utils';
+
+const MIN_BORDER_RADIUS_VALUE = 0;
+const MAX_BORDER_RADIUS_VALUE = 50;
+const MIN_BORDER_WIDTH = 0;
+const MAX_BORDER_WIDTH = 50;
+
+// Defining empty array here instead of inline avoids unnecessary re-renders of
+// color control.
+const EMPTY_ARRAY = [];
+
+export function useHasBorderPanel( { supports, name } ) {
+ const controls = [
+ useHasBorderColorControl( { supports, name } ),
+ useHasBorderRadiusControl( { supports, name } ),
+ useHasBorderStyleControl( { supports, name } ),
+ useHasBorderWidthControl( { supports, name } ),
+ ];
+
+ return controls.every( Boolean );
+}
+
+function useHasBorderColorControl( { supports, name } ) {
+ return (
+ useEditorFeature( 'border.customColor', name ) &&
+ supports.includes( 'borderColor' )
+ );
+}
+
+function useHasBorderRadiusControl( { supports, name } ) {
+ return (
+ useEditorFeature( 'border.customRadius', name ) &&
+ supports.includes( 'borderRadius' )
+ );
+}
+
+function useHasBorderStyleControl( { supports, name } ) {
+ return (
+ useEditorFeature( 'border.customStyle', name ) &&
+ supports.includes( 'borderStyle' )
+ );
+}
+
+function useHasBorderWidthControl( { supports, name } ) {
+ return (
+ useEditorFeature( 'border.customWidth', name ) &&
+ supports.includes( 'borderWidth' )
+ );
+}
+
+export default function BorderPanel( {
+ context: { supports, name },
+ getStyle,
+ setStyle,
+} ) {
+ // Border style.
+ const hasBorderStyle = useHasBorderStyleControl( { supports, name } );
+ const borderStyle = getStyle( name, 'borderStyle' );
+
+ // Border width.
+ const hasBorderWidth = useHasBorderWidthControl( { supports, name } );
+ const borderWidthValue = parseInt(
+ getStyle( name, 'borderWidth' ) || 0,
+ 10
+ );
+
+ // Border radius.
+ const hasBorderRadius = useHasBorderRadiusControl( { supports, name } );
+ const borderRadiusValue = parseInt(
+ getStyle( name, 'borderRadius' ) || 0,
+ 10
+ );
+
+ // Border color.
+ const colors = useEditorFeature( 'color.palette' ) || EMPTY_ARRAY;
+ const disableCustomColors = ! useEditorFeature( 'color.custom' );
+ const disableCustomGradients = ! useEditorFeature( 'color.customGradient' );
+ const hasBorderColor = useHasBorderColorControl( { supports, name } );
+ const borderColor = getStyle( name, 'borderColor' );
+
+ return (
+
+ { hasBorderStyle && (
+
+ setStyle( name, 'borderStyle', value )
+ }
+ />
+ ) }
+ { hasBorderWidth && (
+ {
+ const widthStyle = value ? `${ value }px` : undefined;
+ setStyle( name, 'borderWidth', widthStyle );
+ } }
+ />
+ ) }
+ { hasBorderRadius && (
+ {
+ const radiusStyle = value ? `${ value }px` : undefined;
+ setStyle( name, 'borderRadius', radiusStyle );
+ } }
+ />
+ ) }
+ { hasBorderColor && (
+
+ setStyle( name, 'borderColor', value )
+ }
+ />
+ ) }
+
+ );
+}
diff --git a/packages/edit-site/src/components/sidebar/global-styles-sidebar.js b/packages/edit-site/src/components/sidebar/global-styles-sidebar.js
index 126fb563e7d279..f0017eb703d05c 100644
--- a/packages/edit-site/src/components/sidebar/global-styles-sidebar.js
+++ b/packages/edit-site/src/components/sidebar/global-styles-sidebar.js
@@ -29,6 +29,7 @@ import {
default as TypographyPanel,
useHasTypographyPanel,
} from './typography-panel';
+import { default as BorderPanel, useHasBorderPanel } from './border-panel';
import { default as ColorPanel, useHasColorPanel } from './color-panel';
import { default as SpacingPanel, useHasSpacingPanel } from './spacing-panel';
@@ -40,6 +41,7 @@ function GlobalStylesPanel( {
getSetting,
setSetting,
} ) {
+ const hasBorderPanel = useHasBorderPanel( context );
const hasColorPanel = useHasColorPanel( context );
const hasTypographyPanel = useHasTypographyPanel( context );
const hasSpacingPanel = useHasSpacingPanel( context );
@@ -75,6 +77,13 @@ function GlobalStylesPanel( {
setStyle={ setStyle }
/>
) }
+ { hasBorderPanel && (
+
+ ) }
);
if ( ! wrapperPanelTitle ) {
diff --git a/packages/edit-site/src/components/sidebar/typography-panel.js b/packages/edit-site/src/components/sidebar/typography-panel.js
index 558f2bf11414dc..766a738a42cd2e 100644
--- a/packages/edit-site/src/components/sidebar/typography-panel.js
+++ b/packages/edit-site/src/components/sidebar/typography-panel.js
@@ -16,9 +16,9 @@ import { useEditorFeature } from '../editor/utils';
export function useHasTypographyPanel( { supports, name } ) {
const hasLineHeight = useHasLineHeightControl( { supports, name } );
- const hasFontAppearence = useHasAppearenceControl( { supports, name } );
+ const hasFontAppearance = useHasAppearanceControl( { supports, name } );
return (
- hasLineHeight || hasFontAppearence || supports.includes( 'fontSize' )
+ hasLineHeight || hasFontAppearance || supports.includes( 'fontSize' )
);
}
@@ -29,7 +29,7 @@ function useHasLineHeightControl( { supports, name } ) {
);
}
-function useHasAppearenceControl( { supports, name } ) {
+function useHasAppearanceControl( { supports, name } ) {
const hasFontStyles =
useEditorFeature( 'typography.customFontStyle', name ) &&
supports.includes( 'fontStyle' );
@@ -57,7 +57,7 @@ export default function TypographyPanel( {
useEditorFeature( 'typography.customFontWeight', name ) &&
supports.includes( 'fontWeight' );
const hasLineHeightEnabled = useHasLineHeightControl( { supports, name } );
- const hasAppearenceControl = useHasAppearenceControl( { supports, name } );
+ const hasAppearanceControl = useHasAppearanceControl( { supports, name } );
return (
@@ -88,7 +88,7 @@ export default function TypographyPanel( {
}
/>
) }
- { hasAppearenceControl && (
+ { hasAppearanceControl && (
assertEquals(
- ':root{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}:root{--wp--style--color--link: #111;color: var(--wp--preset--color--grey);}.wp-block-group{padding-top: 12px;padding-bottom: 24px;}.has-grey-color{color: grey !important;}.has-grey-background-color{background-color: grey !important;}',
+ ':root{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}:root{--wp--style--color--link: #111;color: var(--wp--preset--color--grey);}.wp-block-group{padding-top: 12px;padding-bottom: 24px;}.has-grey-color{color: grey !important;}.has-grey-background-color{background-color: grey !important;}.has-grey-border-color{border-color: grey !important;}',
$theme_json->get_stylesheet()
);
$this->assertEquals(
- ':root{--wp--style--color--link: #111;color: var(--wp--preset--color--grey);}.wp-block-group{padding-top: 12px;padding-bottom: 24px;}.has-grey-color{color: grey !important;}.has-grey-background-color{background-color: grey !important;}',
+ ':root{--wp--style--color--link: #111;color: var(--wp--preset--color--grey);}.wp-block-group{padding-top: 12px;padding-bottom: 24px;}.has-grey-color{color: grey !important;}.has-grey-background-color{background-color: grey !important;}.has-grey-border-color{border-color: grey !important;}',
$theme_json->get_stylesheet( 'block_styles' )
);
$this->assertEquals(
@@ -284,11 +284,11 @@ function test_get_stylesheet_preset_rules_come_after_block_rules() {
);
$this->assertEquals(
- '.wp-block-group{--wp--preset--color--grey: grey;}.wp-block-group{color: red;}.wp-block-group.has-grey-color{color: grey !important;}.wp-block-group.has-grey-background-color{background-color: grey !important;}',
+ '.wp-block-group{--wp--preset--color--grey: grey;}.wp-block-group{color: red;}.wp-block-group.has-grey-color{color: grey !important;}.wp-block-group.has-grey-background-color{background-color: grey !important;}.wp-block-group.has-grey-border-color{border-color: grey !important;}',
$theme_json->get_stylesheet()
);
$this->assertEquals(
- '.wp-block-group{color: red;}.wp-block-group.has-grey-color{color: grey !important;}.wp-block-group.has-grey-background-color{background-color: grey !important;}',
+ '.wp-block-group{color: red;}.wp-block-group.has-grey-color{color: grey !important;}.wp-block-group.has-grey-background-color{background-color: grey !important;}.wp-block-group.has-grey-border-color{border-color: grey !important;}',
$theme_json->get_stylesheet( 'block_styles' )
);
}
@@ -324,7 +324,7 @@ public function test_get_stylesheet_preset_values_are_marked_as_important() {
);
$this->assertEquals(
- ':root{--wp--preset--color--grey: grey;}h2.wp-block-post-title{background-color: blue;color: red;font-size: 12px;line-height: 1.3;}.has-grey-color{color: grey !important;}.has-grey-background-color{background-color: grey !important;}',
+ ':root{--wp--preset--color--grey: grey;}h2.wp-block-post-title{background-color: blue;color: red;font-size: 12px;line-height: 1.3;}.has-grey-color{color: grey !important;}.has-grey-background-color{background-color: grey !important;}.has-grey-border-color{border-color: grey !important;}',
$theme_json->get_stylesheet()
);
}