diff --git a/packages/block-editor/src/components/global-styles/border-panel.js b/packages/block-editor/src/components/global-styles/border-panel.js new file mode 100644 index 00000000000000..83979b2845c4be --- /dev/null +++ b/packages/block-editor/src/components/global-styles/border-panel.js @@ -0,0 +1,285 @@ +/** + * WordPress dependencies + */ +import { + __experimentalBorderBoxControl as BorderBoxControl, + __experimentalHasSplitBorders as hasSplitBorders, + __experimentalIsDefinedBorder as isDefinedBorder, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; +import { useCallback, useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import BorderRadiusControl from '../border-radius-control'; +import { useColorsPerOrigin } from './hooks'; +import { getValueFromVariable } from './utils'; + +export function useHasBorderPanel( settings ) { + const controls = [ + useHasBorderColorControl( settings ), + useHasBorderRadiusControl( settings ), + useHasBorderStyleControl( settings ), + useHasBorderWidthControl( settings ), + ]; + + return controls.some( Boolean ); +} + +function useHasBorderColorControl( settings ) { + return settings?.border?.color; +} + +function useHasBorderRadiusControl( settings ) { + return settings?.border?.radius; +} + +function useHasBorderStyleControl( settings ) { + return settings?.border?.style; +} + +function useHasBorderWidthControl( settings ) { + return settings?.border?.width; +} + +function applyFallbackStyle( border ) { + if ( ! border ) { + return border; + } + + if ( ! border.style && ( border.color || border.width ) ) { + return { ...border, style: 'solid' }; + } + + return border; +} + +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 ); +} + +function BorderToolsPanel( { + resetAllFilter, + onChange, + value, + panelId, + children, +} ) { + const resetAll = () => { + const updatedValue = resetAllFilter( value ); + onChange( updatedValue ); + }; + + return ( + + { children } + + ); +} + +const DEFAULT_CONTROLS = { + radius: true, + color: true, + width: true, +}; + +export default function BorderPanel( { + as: Wrapper = BorderToolsPanel, + value, + onChange, + inheritedValue = value, + settings, + panelId, + defaultControls = DEFAULT_CONTROLS, +} ) { + const colors = useColorsPerOrigin( settings ); + const decodeValue = ( rawValue ) => + getValueFromVariable( { settings }, '', rawValue ); + const encodeColorValue = ( colorValue ) => { + const allColors = colors.flatMap( + ( { colors: originColors } ) => originColors + ); + const colorObject = allColors.find( + ( { color } ) => color === colorValue + ); + return colorObject + ? 'var:preset|color|' + colorObject.slug + : colorValue; + }; + const decodeColorValue = useCallback( + ( colorValue ) => { + const allColors = colors.flatMap( + ( { colors: originColors } ) => originColors + ); + const colorObject = allColors.find( + ( { slug } ) => colorValue === 'var:preset|color|' + slug + ); + return colorObject ? colorObject.color : colorValue; + }, + [ colors ] + ); + const border = useMemo( () => { + if ( hasSplitBorders( inheritedValue?.border ) ) { + const borderValue = { ...inheritedValue?.border }; + [ 'top', 'right', 'bottom', 'left' ].forEach( ( side ) => { + borderValue[ side ] = { + ...borderValue[ side ], + color: decodeColorValue( borderValue[ side ]?.color ), + }; + } ); + return borderValue; + } + return { + ...inheritedValue?.border, + color: inheritedValue?.border?.color + ? decodeColorValue( inheritedValue?.border?.color ) + : undefined, + }; + }, [ inheritedValue?.border, decodeColorValue ] ); + const setBorder = ( newBorder ) => + onChange( { ...value, border: newBorder } ); + const showBorderColor = useHasBorderColorControl( settings ); + const showBorderStyle = useHasBorderStyleControl( settings ); + const showBorderWidth = useHasBorderWidthControl( settings ); + + // Border radius. + const showBorderRadius = useHasBorderRadiusControl( settings ); + const borderRadiusValues = decodeValue( border?.radius ); + const setBorderRadius = ( newBorderRadius ) => + setBorder( { ...border, radius: newBorderRadius } ); + const hasBorderRadius = () => { + const borderValues = value?.border?.radius; + if ( typeof borderValues === 'object' ) { + return Object.entries( borderValues ).some( Boolean ); + } + return !! borderValues; + }; + + const resetBorder = () => { + if ( hasBorderRadius() ) { + return setBorder( { radius: value?.border?.radius } ); + } + + setBorder( undefined ); + }; + + const onBorderChange = ( 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, + }; + + [ 'top', 'right', 'bottom', 'left' ].forEach( ( side ) => { + updatedBorder[ side ] = { + ...updatedBorder[ side ], + color: encodeColorValue( updatedBorder[ side ]?.color ), + }; + } ); + + // 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 } ); + }; + + const resetAllFilter = useCallback( ( previousValue ) => { + return { + ...previousValue, + border: undefined, + }; + }, [] ); + + const showBorderByDefault = + defaultControls?.color || defaultControls?.width; + + return ( + + { ( showBorderWidth || showBorderColor ) && ( + isDefinedBorder( value?.border ) } + label={ __( 'Border' ) } + onDeselect={ () => resetBorder() } + isShownByDefault={ showBorderByDefault } + panelId={ panelId } + > + + + ) } + { showBorderRadius && ( + setBorderRadius( undefined ) } + isShownByDefault={ defaultControls.radius } + panelId={ panelId } + > + { + setBorderRadius( newValue || undefined ); + } } + /> + + ) } + + ); +} diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index f483a9208e6e11..2414f3d4b4b9c7 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -10,6 +10,7 @@ import { get, set } from 'lodash'; import { useContext, useCallback, useMemo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { store as blocksStore } from '@wordpress/blocks'; +import { _x } from '@wordpress/i18n'; /** * Internal dependencies @@ -303,6 +304,67 @@ export function useSettingsForBlockElement( }; } + [ 'radius', 'color', 'style', 'width' ].forEach( ( key ) => { + if ( + ! supportedStyles.includes( + 'border' + key.charAt( 0 ).toUpperCase() + key.slice( 1 ) + ) + ) { + updatedSettings.border = { + ...updatedSettings.border, + [ key ]: false, + }; + } + } ); + return updatedSettings; }, [ parentSettings, supportedStyles, supports ] ); } + +export function useColorsPerOrigin( settings ) { + const customColors = settings?.color?.palette?.custom; + const themeColors = settings?.color?.palette?.theme; + const defaultColors = settings?.color?.palette?.default; + const shouldDisplayDefaultColors = settings?.color?.defaultPalette; + + return useMemo( () => { + const result = []; + if ( themeColors && themeColors.length ) { + result.push( { + name: _x( + 'Theme', + 'Indicates this palette comes from the theme.' + ), + colors: themeColors, + } ); + } + if ( + shouldDisplayDefaultColors && + defaultColors && + defaultColors.length + ) { + result.push( { + name: _x( + 'Default', + 'Indicates this palette comes from WordPress.' + ), + colors: defaultColors, + } ); + } + if ( customColors && customColors.length ) { + result.push( { + name: _x( + 'Custom', + 'Indicates this palette is created by the user.' + ), + colors: customColors, + } ); + } + return result; + }, [ + customColors, + themeColors, + defaultColors, + shouldDisplayDefaultColors, + ] ); +} diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js index 9c06af05b2e8b6..372f56fcead0d4 100644 --- a/packages/block-editor/src/components/global-styles/index.js +++ b/packages/block-editor/src/components/global-styles/index.js @@ -3,6 +3,7 @@ export { useGlobalSetting, useGlobalStyle, useSettingsForBlockElement, + useColorsPerOrigin, } from './hooks'; export { useGlobalStylesOutput } from './use-global-styles-output'; export { GlobalStylesContext } from './context'; @@ -14,3 +15,4 @@ export { default as DimensionsPanel, useHasDimensionsPanel, } from './dimensions-panel'; +export { default as BorderPanel, useHasBorderPanel } from './border-panel'; diff --git a/packages/block-editor/src/hooks/border-radius.js b/packages/block-editor/src/hooks/border-radius.js deleted file mode 100644 index 6b247e141b6818..00000000000000 --- a/packages/block-editor/src/hooks/border-radius.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Internal dependencies - */ -import BorderRadiusControl from '../components/border-radius-control'; -import { cleanEmptyObject } from './utils'; -import { removeBorderAttribute } from './border'; - -/** - * Inspector control panel containing the border radius related configuration. - * - * @param {Object} props Block properties. - * - * @return {WPElement} Border radius edit element. - */ -export function BorderRadiusEdit( props ) { - const { - attributes: { style }, - setAttributes, - } = props; - - const onChange = ( newRadius ) => { - const newStyle = cleanEmptyObject( { - ...style, - border: { - ...style?.border, - radius: newRadius, - }, - } ); - - setAttributes( { style: newStyle } ); - }; - - return ( - - ); -} - -/** - * Checks if there is a current value in the border radius block support - * attributes. - * - * @param {Object} props Block props. - * @return {boolean} Whether or not the block has a border radius value set. - */ -export function hasBorderRadiusValue( props ) { - const borderRadius = props.attributes.style?.border?.radius; - - if ( typeof borderRadius === 'object' ) { - return Object.entries( borderRadius ).some( Boolean ); - } - - return !! borderRadius; -} - -/** - * Resets the border radius block support attributes. This can be used when - * disabling the border radius 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 resetBorderRadius( { attributes = {}, setAttributes } ) { - const { style } = attributes; - setAttributes( { style: removeBorderAttribute( style, 'radius' ) } ); -} diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index d99416e6cd6651..a5f08b64515b01 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -7,66 +7,29 @@ import classnames from 'classnames'; * WordPress dependencies */ import { getBlockSupport } from '@wordpress/blocks'; -import { - __experimentalBorderBoxControl as BorderBoxControl, - __experimentalHasSplitBorders as hasSplitBorders, - __experimentalIsDefinedBorder as isDefinedBorder, - __experimentalToolsPanelItem as ToolsPanelItem, -} from '@wordpress/components'; +import { __experimentalHasSplitBorders as hasSplitBorders } from '@wordpress/components'; import { createHigherOrderComponent } from '@wordpress/compose'; -import { Platform } from '@wordpress/element'; +import { Platform, useCallback, useMemo } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; -import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { - BorderRadiusEdit, - hasBorderRadiusValue, - resetBorderRadius, -} from './border-radius'; 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, shouldSkipSerialization } from './utils'; +import { + cleanEmptyObject, + shouldSkipSerialization, + useBlockSettings, +} from './utils'; +import { + useHasBorderPanel, + BorderPanel as StylesBorderPanel, +} from '../components/global-styles'; 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; @@ -103,51 +66,6 @@ export const getMultiOriginColor = ( { colors, namedColor, 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 ] ) { @@ -156,150 +74,101 @@ function getColorSlugFromVariable( value ) { return null; } -export function BorderPanel( 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' ); - - const isDisabled = [ - ! isColorSupported, - ! isRadiusSupported, - ! isStyleSupported, - ! isWidthSupported, - ].every( Boolean ); - - if ( isDisabled || ! isSupported ) { - return null; +function styleToAttributes( style ) { + if ( hasSplitBorders( style?.border ) ) { + return { + style, + borderColor: undefined, + }; } - const defaultBorderControls = getBlockSupport( props.name, [ - BORDER_SUPPORT_KEY, - '__experimentalDefaultControls', - ] ); + const borderColorValue = style?.border?.color; + const borderColorSlug = borderColorValue?.startsWith( 'var:preset|color|' ) + ? borderColorSlug.substring( 'var:preset|color|'.length ) + : undefined; + const updatedStyle = { ...style }; + updatedStyle.border = { + ...updatedStyle.border, + color: borderColorSlug ? undefined : borderColorValue, + }; + return { + style: cleanEmptyObject( updatedStyle ), + borderColor: borderColorSlug, + }; +} - 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 }, +function attributesToStyle( attributes ) { + if ( hasSplitBorders( attributes.style?.border ) ) { + return attributes.style; + } + return { + ...attributes.style, + border: { + ...attributes.style?.border, + color: attributes.borderColor + ? 'var:preset|color|' + attributes.borderColor + : attributes.style?.border?.color, + }, + }; +} + +function BordersInspectorControl( { children, resetAllFilter } ) { + const attributesResetAllFilter = useCallback( + ( attributes ) => { + const existingStyle = attributesToStyle( attributes ); + const updatedStyle = resetAllFilter( existingStyle ); + return { + ...attributes, + ...styleToAttributes( updatedStyle ), }; + }, + [ resetAllFilter ] + ); - 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; - } - } + return ( + + { children } + + ); +} - // 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 }, +export function BorderPanel( props ) { + const { clientId, name, attributes, setAttributes } = props; + const settings = useBlockSettings( name ); + const isEnabled = useHasBorderPanel( settings ); + const value = useMemo( () => { + return attributesToStyle( { + style: attributes.style, + borderColor: attributes.borderColor, } ); + }, [ attributes.style, attributes.borderColor ] ); - setAttributes( { - style: newStyle, - borderColor: newBorderColor, - } ); + const onChange = ( newStyle ) => { + setAttributes( styleToAttributes( newStyle ) ); }; - const hydratedBorder = getBorderObject( attributes, colors ); + if ( ! isEnabled ) { + return null; + } + + const defaultControls = getBlockSupport( props.name, [ + BORDER_SUPPORT_KEY, + '__experimentalDefaultControls', + ] ); return ( - - { ( isWidthSupported || isColorSupported ) && ( - hasBorderValue( props ) } - label={ __( 'Border' ) } - onDeselect={ () => resetBorder( props ) } - isShownByDefault={ showBorderByDefault } - resetAllFilter={ resetBorderFilter } - panelId={ clientId } - > - - - ) } - { isRadiusSupported && ( - hasBorderRadiusValue( props ) } - label={ __( 'Radius' ) } - onDeselect={ () => resetBorderRadius( props ) } - isShownByDefault={ defaultBorderControls?.radius } - resetAllFilter={ ( newAttributes ) => ( { - ...newAttributes, - style: { - ...newAttributes.style, - border: { - ...newAttributes.style?.border, - radius: undefined, - }, - }, - } ) } - panelId={ clientId } - > - - - ) } - + ); } diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 87d666e7cb7695..3684598997a6a8 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -212,9 +212,27 @@ export function useBlockSettings( name, parentLayout ) { const units = useSetting( 'spacing.units' ); const minHeight = useSetting( 'dimensions.minHeight' ); const layout = useSetting( 'layout' ); + const borderColor = useSetting( 'border.color' ); + const borderRadius = useSetting( 'border.radius' ); + const borderStyle = useSetting( 'border.style' ); + const borderWidth = useSetting( 'border.width' ); + const customColorsEnabled = useSetting( 'color.custom' ); + const customColors = useSetting( 'color.palette.custom' ); + const themeColors = useSetting( 'color.palette.theme' ); + const defaultColors = useSetting( 'color.palette.default' ); + const defaultPalette = useSetting( 'color.defaultPalette' ); const rawSettings = useMemo( () => { return { + color: { + palette: { + custom: customColors, + theme: themeColors, + default: defaultColors, + }, + defaultPalette, + custom: customColorsEnabled, + }, typography: { fontFamilies: { custom: fontFamilies, @@ -239,6 +257,12 @@ export function useBlockSettings( name, parentLayout ) { blockGap, units, }, + border: { + color: borderColor, + radius: borderRadius, + style: borderStyle, + width: borderWidth, + }, dimensions: { minHeight, }, @@ -263,6 +287,15 @@ export function useBlockSettings( name, parentLayout ) { minHeight, layout, parentLayout, + borderColor, + borderRadius, + borderStyle, + borderWidth, + customColorsEnabled, + customColors, + themeColors, + defaultColors, + defaultPalette, ] ); return useSettingsForBlockElement( rawSettings, name ); diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 36ddedb60cf4ac..76a750343418c5 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -35,6 +35,7 @@ export { export { default as __experimentalStyleProvider } from './style-provider'; export { default as BaseControl } from './base-control'; +export { hasSplitBorders as __experimentalHasSplitBorders } from './border-box-control/utils'; export { default as TextareaControl } from './textarea-control'; export { default as PanelBody } from './panel/body'; export { default as PanelActions } from './panel/actions'; 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 07e6254e4b34ee..0926e6edd03386 100644 --- a/packages/edit-site/src/components/global-styles/border-panel.js +++ b/packages/edit-site/src/components/global-styles/border-panel.js @@ -1,215 +1,40 @@ /** * WordPress dependencies */ -import { - __experimentalBorderRadiusControl as BorderRadiusControl, - privateApis as blockEditorPrivateApis, -} from '@wordpress/block-editor'; -import { - __experimentalBorderBoxControl as BorderBoxControl, - __experimentalHasSplitBorders as hasSplitBorders, - __experimentalIsDefinedBorder as isDefinedBorder, - __experimentalToolsPanel as ToolsPanel, - __experimentalToolsPanelItem as ToolsPanelItem, -} from '@wordpress/components'; -import { useCallback } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; /** * Internal dependencies */ -import { useSupportedStyles, useColorsPerOrigin } from './hooks'; import { unlock } from '../../private-apis'; -const { useGlobalSetting, useGlobalStyle } = unlock( blockEditorPrivateApis ); - -export function useHasBorderPanel( name ) { - const controls = [ - useHasBorderColorControl( name ), - useHasBorderRadiusControl( name ), - useHasBorderStyleControl( name ), - useHasBorderWidthControl( name ), - ]; - - return controls.some( Boolean ); -} - -function useHasBorderColorControl( name ) { - const supports = useSupportedStyles( name ); - return ( - useGlobalSetting( 'border.color', name )[ 0 ] && - supports.includes( 'borderColor' ) - ); -} - -function useHasBorderRadiusControl( name ) { - const supports = useSupportedStyles( name ); - return ( - useGlobalSetting( 'border.radius', name )[ 0 ] && - supports.includes( 'borderRadius' ) - ); -} - -function useHasBorderStyleControl( name ) { - const supports = useSupportedStyles( name ); - return ( - useGlobalSetting( 'border.style', name )[ 0 ] && - supports.includes( 'borderStyle' ) - ); -} - -function useHasBorderWidthControl( name ) { - const supports = useSupportedStyles( name ); - return ( - useGlobalSetting( 'border.width', name )[ 0 ] && - supports.includes( 'borderWidth' ) - ); -} - -function applyFallbackStyle( border ) { - if ( ! border ) { - return border; - } - - if ( ! border.style && ( border.color || border.width ) ) { - return { ...border, style: 'solid' }; - } - - return border; -} - -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 ); -} +const { + useGlobalStyle, + useGlobalSetting, + useSettingsForBlockElement, + BorderPanel: StylesBorderPanel, +} = unlock( blockEditorPrivateApis ); export default function BorderPanel( { name, variation = '' } ) { - const prefix = variation ? `variations.${ variation }.` : ''; - // 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 ] = useGlobalStyle( - `${ prefix }border`, - name, - 'user' - ); - const [ border, setBorder ] = useGlobalStyle( `${ prefix }border`, name ); - const colors = useColorsPerOrigin( name ); - - const showBorderColor = useHasBorderColorControl( name ); - const showBorderStyle = useHasBorderStyleControl( name ); - const showBorderWidth = useHasBorderWidthControl( name ); - - // Border radius. - const showBorderRadius = useHasBorderRadiusControl( name ); - const [ borderRadiusValues, setBorderRadius ] = useGlobalStyle( - `${ prefix }border.radius`, - name - ); - const hasBorderRadius = () => { - const borderValues = userBorderStyles?.radius; - if ( typeof borderValues === 'object' ) { - return Object.entries( borderValues ).some( Boolean ); - } - return !! borderValues; - }; - - 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, - }; + let prefixParts = []; + if ( variation ) { + prefixParts = [ 'variations', variation ].concat( prefixParts ); + } + const prefix = prefixParts.join( '.' ); - // 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, border?.radius ] - ); + const [ style ] = useGlobalStyle( prefix, name, 'user', false ); + const [ inheritedStyle, setStyle ] = useGlobalStyle( prefix, name, 'all', { + shouldDecodeEncode: false, + } ); + const [ rawSettings ] = useGlobalSetting( '', name ); + const settings = useSettingsForBlockElement( rawSettings, name ); return ( - - { ( showBorderWidth || showBorderColor ) && ( - isDefinedBorder( userBorderStyles ) } - label={ __( 'Border' ) } - onDeselect={ () => resetBorder() } - isShownByDefault={ true } - > - - - ) } - { showBorderRadius && ( - setBorderRadius( undefined ) } - isShownByDefault={ true } - > - { - setBorderRadius( value || undefined ); - } } - /> - - ) } - + ); } diff --git a/packages/edit-site/src/components/global-styles/context-menu.js b/packages/edit-site/src/components/global-styles/context-menu.js index fd9565412ede76..89fd262d9ade4c 100644 --- a/packages/edit-site/src/components/global-styles/context-menu.js +++ b/packages/edit-site/src/components/global-styles/context-menu.js @@ -27,7 +27,6 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; /** * Internal dependencies */ -import { useHasBorderPanel } from './border-panel'; import { useHasColorPanel } from './color-utils'; import { useHasFilterPanel } from './filter-utils'; import { useHasVariationsPanel } from './variations-panel'; @@ -40,6 +39,7 @@ import { unlock } from '../../private-apis'; const { useHasDimensionsPanel, useHasTypographyPanel, + useHasBorderPanel, useGlobalSetting, useSettingsForBlockElement, } = unlock( blockEditorPrivateApis ); @@ -49,7 +49,7 @@ function ContextMenu( { name, parentMenu = '' } ) { const settings = useSettingsForBlockElement( rawSettings, name ); const hasTypographyPanel = useHasTypographyPanel( settings ); const hasColorPanel = useHasColorPanel( name ); - const hasBorderPanel = useHasBorderPanel( name ); + const hasBorderPanel = useHasBorderPanel( settings ); const hasEffectsPanel = useHasShadowControl( name ); const hasFilterPanel = useHasFilterPanel( name ); const hasDimensionsPanel = useHasDimensionsPanel( settings ); diff --git a/packages/edit-site/src/components/global-styles/hooks.js b/packages/edit-site/src/components/global-styles/hooks.js index e1c5370cd630a1..690cbd3cbcbe6e 100644 --- a/packages/edit-site/src/components/global-styles/hooks.js +++ b/packages/edit-site/src/components/global-styles/hooks.js @@ -18,7 +18,8 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { unlock } from '../../private-apis'; import { useSelect } from '@wordpress/data'; -const { useGlobalSetting } = unlock( blockEditorPrivateApis ); +const { useGlobalSetting, useColorsPerOrigin: useColorsPerOriginFromSettings } = + unlock( blockEditorPrivateApis ); // Enable colord's a11y plugin. extend( [ a11yPlugin ] ); @@ -31,41 +32,16 @@ export function useColorsPerOrigin( name ) { 'color.defaultPalette' ); - return useMemo( () => { - const result = []; - if ( themeColors && themeColors.length ) { - result.push( { - name: _x( - 'Theme', - 'Indicates this palette comes from the theme.' - ), - colors: themeColors, - } ); - } - if ( - shouldDisplayDefaultColors && - defaultColors && - defaultColors.length - ) { - result.push( { - name: _x( - 'Default', - 'Indicates this palette comes from WordPress.' - ), - colors: defaultColors, - } ); - } - if ( customColors && customColors.length ) { - result.push( { - name: _x( - 'Custom', - 'Indicates this palette is created by the user.' - ), - colors: customColors, - } ); - } - return result; - }, [ customColors, themeColors, defaultColors ] ); + return useColorsPerOriginFromSettings( { + color: { + palette: { + custom: customColors, + theme: themeColors, + default: defaultColors, + }, + defaultPalette: shouldDisplayDefaultColors, + }, + } ); } export function useGradientsPerOrigin( name ) { diff --git a/packages/edit-site/src/components/global-styles/screen-block-list.js b/packages/edit-site/src/components/global-styles/screen-block-list.js index 07a6f8554af84a..e85b3956f3e372 100644 --- a/packages/edit-site/src/components/global-styles/screen-block-list.js +++ b/packages/edit-site/src/components/global-styles/screen-block-list.js @@ -20,7 +20,6 @@ import { speak } from '@wordpress/a11y'; /** * Internal dependencies */ -import { useHasBorderPanel } from './border-panel'; import { useHasColorPanel } from './color-utils'; import { useHasVariationsPanel } from './variations-panel'; import ScreenHeader from './header'; @@ -30,6 +29,7 @@ import { unlock } from '../../private-apis'; const { useHasDimensionsPanel, useHasTypographyPanel, + useHasBorderPanel, useGlobalSetting, useSettingsForBlockElement, } = unlock( blockEditorPrivateApis ); @@ -62,7 +62,7 @@ function BlockMenuItem( { block } ) { const settings = useSettingsForBlockElement( rawSettings, block.name ); const hasTypographyPanel = useHasTypographyPanel( settings ); const hasColorPanel = useHasColorPanel( block.name ); - const hasBorderPanel = useHasBorderPanel( block.name ); + const hasBorderPanel = useHasBorderPanel( settings ); const hasDimensionsPanel = useHasDimensionsPanel( settings ); const hasLayoutPanel = hasBorderPanel || hasDimensionsPanel; const hasVariationsPanel = useHasVariationsPanel( block.name ); diff --git a/packages/edit-site/src/components/global-styles/screen-border.js b/packages/edit-site/src/components/global-styles/screen-border.js index 6a5578e20fcefa..f700f2676448f8 100644 --- a/packages/edit-site/src/components/global-styles/screen-border.js +++ b/packages/edit-site/src/components/global-styles/screen-border.js @@ -2,17 +2,24 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; /** * Internal dependencies */ import ScreenHeader from './header'; -import BorderPanel, { useHasBorderPanel } from './border-panel'; +import BorderPanel from './border-panel'; import BlockPreviewPanel from './block-preview-panel'; import { getVariationClassName } from './utils'; +import { unlock } from '../../private-apis'; + +const { useHasBorderPanel, useGlobalSetting, useSettingsForBlockElement } = + unlock( blockEditorPrivateApis ); function ScreenBorder( { name, variation = '' } ) { - const hasBorderPanel = useHasBorderPanel( name ); + const [ rawSettings ] = useGlobalSetting( '', name ); + const settings = useSettingsForBlockElement( rawSettings, name ); + const hasBorderPanel = useHasBorderPanel( settings ); const variationClassName = getVariationClassName( variation ); return ( <>