diff --git a/lib/block-supports/border.php b/lib/block-supports/border.php index 0a29d3753cecc..3bdc2253534c9 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 147725d66a19b..3a7ccbd7c8c2c 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 da910ca84747a..dea9d96966d34 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 4bef8e7b93751..c3e7a6da9cfcf 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 0000000000000..ee2ad2482cd3c --- /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 ( +
+ + selectedItem.key === 'default' + ? onChange( undefined ) + : onChange( selectedItem.key ) + } + /> +
+ ); +} 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 0000000000000..827f31bca718c --- /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 ac7471317acbe..dc88ae5a36deb 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 0000000000000..511983d35fab1 --- /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 7ced3df65401a..50e9dece97eaf 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 0000000000000..9f1b1e49b2143 --- /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 0000000000000..24e25f6a06385 --- /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 31bb6f4170c27..9b4aa499f54cd 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 c087e994ba9eb..49b11cc8b4074 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 afa1d45856b2a..0cd3c70d9f2b6 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 f80eeefd3be42..745e64a980d2a 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 0000000000000..9b95c4d861b56 --- /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 126fb563e7d27..f0017eb703d05 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 558f2bf11414d..766a738a42cd2 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() ); }