From bb8003ea0ff325790627be2149ad9c1eea03e2e3 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 2 Jan 2024 18:05:49 +1000 Subject: [PATCH] Explore applying colorways to block instances including UI updates --- lib/block-supports/settings.php | 4 + .../components/colors-gradients/style.scss | 8 + .../components/global-styles/color-panel.js | 91 +++++++- .../global-styles/colorway-dropdown.js | 197 ++++++++++++++++++ .../src/components/global-styles/style.scss | 30 +++ .../src/components/global-styles/utils.js | 92 +++++++- packages/block-editor/src/hooks/color.js | 76 ++++++- packages/block-editor/src/hooks/utils.js | 73 +++++++ packages/block-library/src/cover/editor.scss | 2 +- .../components/global-styles/screen-colors.js | 1 + 10 files changed, 562 insertions(+), 12 deletions(-) create mode 100644 packages/block-editor/src/components/global-styles/colorway-dropdown.js diff --git a/lib/block-supports/settings.php b/lib/block-supports/settings.php index b175fe778ce1b0..3fd500bac45de4 100644 --- a/lib/block-supports/settings.php +++ b/lib/block-supports/settings.php @@ -84,6 +84,10 @@ function _gutenberg_add_block_level_preset_styles( $pre_render, $block ) { } } $variables_root_selector = WP_Theme_JSON_Gutenberg::scope_selector( $class_name, $variables_root_selector ); + // TODO: Work out if there are any downsides to the adding the variables at the block's level not only on children. + // For colorways, we set styles that leverage a color palette applied in the block instance's `settings` attribute. + // Without the CSS custom properties and associated classes those colors won't take effect. + $variables_root_selector = $class_name . ',' . $variables_root_selector; // Remove any potentially unsafe styles. $theme_json_shape = WP_Theme_JSON_Gutenberg::remove_insecure_properties( diff --git a/packages/block-editor/src/components/colors-gradients/style.scss b/packages/block-editor/src/components/colors-gradients/style.scss index b3539637a9904c..3077d54a044bae 100644 --- a/packages/block-editor/src/components/colors-gradients/style.scss +++ b/packages/block-editor/src/components/colors-gradients/style.scss @@ -72,12 +72,20 @@ $swatch-gap: 12px; // Identify the first visible instance as placeholder items will not have this class. &:nth-child(1 of &) { + // Needed for backward compatibility of color gradient dropdowns before + // the merge with ToolsPanelItems. margin-top: $grid-unit-30; border-top-left-radius: $radius-block-ui; border-top-right-radius: $radius-block-ui; border-top: 1px solid $gray-300; } + // No need for the top margin when displaying colorway controls above the + // individual color options. + .block-editor-tools-panel-color-gradient-settings__label + &:nth-child(1 of &) { + margin-top: 0; + } + // Identify the last visible instance as placeholder items will not have this class. &:nth-last-child(1 of &) { border-bottom-left-radius: $radius-block-ui; diff --git a/packages/block-editor/src/components/global-styles/color-panel.js b/packages/block-editor/src/components/global-styles/color-panel.js index c34335bc9dda50..becabfd64f4d49 100644 --- a/packages/block-editor/src/components/global-styles/color-panel.js +++ b/packages/block-editor/src/components/global-styles/color-panel.js @@ -12,6 +12,7 @@ import { __experimentalHStack as HStack, __experimentalZStack as ZStack, __experimentalDropdownContentWrapper as DropdownContentWrapper, + BaseControl, ColorIndicator, Flex, FlexItem, @@ -19,15 +20,20 @@ import { Button, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { useCallback } from '@wordpress/element'; +import { useCallback, useMemo } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ import ColorGradientControl from '../colors-gradients/control'; +import ColorwayDropdown from './colorway-dropdown.js'; import { useColorsPerOrigin, useGradientsPerOrigin } from './hooks'; -import { getValueFromVariable, TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; +import { + findActiveColorway, + getValueFromVariable, + TOOLSPANEL_DROPDOWNMENU_PROPS, +} from './utils'; import { setImmutably } from '../../utils/object'; import { unlock } from '../../lock-unlock'; @@ -305,6 +311,7 @@ export default function ColorPanel( { settings, panelId, defaultControls = DEFAULT_CONTROLS, + colorways, children, } ) { const colors = useColorsPerOrigin( settings ); @@ -514,6 +521,8 @@ export default function ColorPanel( { }, {} ), }, }; + // TODO: Existing missing dep. Refactor. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [] ); const items = [ @@ -702,6 +711,68 @@ export default function ColorPanel( { } ); } ); + const currentColorway = useMemo( + () => findActiveColorway( colorways, settings, value ), + [ colorways, settings, value ] + ); + + // TODO: This might need to receive the colorway being reset if we + // need a more sophisticated approach to clearing it's styles and settings. + const resetColorway = () => { + // Passing object with undefined values so that the top-level preset + // attributes also get reset. + const newValue = setImmutably( value, [ 'color' ], { + background: undefined, + gradient: undefined, + text: undefined, + } ); + + // Reset all the element color styles. + // TODO: Determine if this is the desired behaviour or should it only + // reset elements included in the colorway? + if ( value?.elements ) { + const newElements = {}; + + Object.keys( value.elements ).forEach( ( elementName ) => { + newElements[ elementName ] = { + ...value.elements[ elementName ], + color: undefined, + }; + } ); + + newValue.elements = newElements; + } + + // TODO: Find better approach for resetting the settings object as well + // as styles. + // `null` below is to explicitly call for the resetting of the palette settings + // otherwise we'll need to update all onChange handlers so they can pass a second argument. + onChange( newValue, null ); + }; + + const setColorway = ( colorway ) => { + const newValue = setImmutably( + value, + [ 'color' ], + colorway.styles?.color + ); + + if ( colorway.styles?.elements ) { + const newElements = {}; + + Object.keys( colorway.styles?.elements ).forEach( ( element ) => { + newElements[ element ] = { + ...value.elements?.[ element ], + ...colorway.styles.elements[ element ], + }; + } ); + + newValue.elements = newElements; + } + + onChange( newValue, colorway.settings?.color?.palette?.theme ); + }; + return ( + { !! colorways?.length && ( + <> + + + { __( 'Colors' ) } + + + ) } { items.map( ( item ) => ( ; +const noop = () => undefined; + +// TODO: Work out what styling of options happens. The element colour sets +// issue shows two options that appear to have background colors but they +// don't match the swatches and the dark one has light text. +function getColorwayPaletteStyles( colorway ) { + if ( ! colorway?.styles?.color ) { + return; + } + + const styles = {}; + const { background, gradient, text } = colorway.styles.color; + + if ( background ) { + styles.backgroundColor = getValueFromVariable( + colorway, + '', + background + ); + } + + if ( gradient ) { + styles.background = getValueFromVariable( colorway, '', gradient ); + } + + if ( text ) { + styles.color = getValueFromVariable( colorway, '', text ); + } + + return styles; +} + +// TODO: Confirm which colors need indicators. Not clear from issue. +// +function ColorwayIndicator( { colorway } ) { + const { background, gradient, text } = colorway?.styles?.color || {}; + const link = colorway?.styles?.elements?.link?.color?.text; + const heading = colorway?.styles?.elements?.heading?.color?.text; + + const indicators = [ + getValueFromVariable( colorway, '', background || gradient ), + getValueFromVariable( colorway, '', text ), + getValueFromVariable( colorway, '', link ), + getValueFromVariable( colorway, '', heading ), + ]; + + const label = colorway?.title || __( 'Default' ); + + return ( + + + { indicators.map( ( indicator, index ) => ( + + + + ) ) } + + + { label } + + + ); +} + +function ColorwayDropdownToggle( { onToggle, isOpen, value } ) { + const toggleProps = { + onClick: onToggle, + className: classnames( + 'block-editor-tools-panel-colorway__dropdown-toggle', + { + 'is-open': isOpen, + } + ), + 'aria-expanded': isOpen, + 'aria-label': __( 'Colorways options' ), + style: getColorwayPaletteStyles( value ), + }; + + return ( + + ); +} + +function ColorwayDropdownContent( { colorways, activeColorway, onSelect } ) { + return ( + + { colorways.map( ( colorway, index ) => { + const paletteStyles = getColorwayPaletteStyles( colorway ); + const isSelected = activeColorway?.title === colorway.title; + + return ( + onSelect( colorway ) } + role="menuitemradio" + style={ paletteStyles } + suffix={ isSelected ? checkIcon : undefined } + > + + + ); + } ) } + + ); +} + +export default function ColorwayDropdown( { + className, + colorways = [], + value, + label, + onDeselect = noop, + onSelect = noop, + panelId, + ...props +} ) { + if ( ! colorways.length ) { + return null; + } + + const classes = classnames( + className, + 'block-editor-tools-panel-colorway__dropdown' + ); + + return ( + !! value } + label={ label } + onDeselect={ onDeselect } + isShownByDefault={ true } + panelId={ panelId } + > + + { label } + ( + + ) } + renderContent={ ( contentProps ) => ( + + ) } + /> + + + ); +} diff --git a/packages/block-editor/src/components/global-styles/style.scss b/packages/block-editor/src/components/global-styles/style.scss index 46429ea5c47765..c57e71b90bc755 100644 --- a/packages/block-editor/src/components/global-styles/style.scss +++ b/packages/block-editor/src/components/global-styles/style.scss @@ -58,3 +58,33 @@ .block-editor-global-styles-advanced-panel__custom-css-validation-icon { fill: $alert-red; } + +.block-editor-tools-panel-colorway { + margin-top: 0; + margin-bottom: $grid-unit-30; +} + +.block-editor-tools-panel-colorway__dropdown { + border: 1px solid $gray-300; + border-radius: $radius-block-ui; +} + +.block-editor-tools-panel-colorway__dropdown-group { + width: $popover-width - $grid-unit-20; + + button + button { + margin-top: $grid-unit-05; + } +} + +.block-editor-tools-panel-colorway__dropdown-toggle { + width: 100%; + height: auto; + padding-top: $grid-unit * 1.25; + padding-bottom: $grid-unit * 1.25; + + &.is-open { + background: $gray-100; + color: var(--wp-admin-theme-color); + } +} diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index 34964d3c92905b..0ad0c37678ce90 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -11,6 +11,7 @@ import { getFluidTypographyOptionsFromSettings, } from './typography-utils'; import { getValueFromObjectPath } from '../../utils/object'; +import { cleanEmptyObject } from '../../hooks/utils'; /* Supporting data. */ export const ROOT_BLOCK_NAME = 'root'; @@ -345,7 +346,7 @@ export function getValueFromVariable( features, blockName, variable ) { .slice( THEME_VALUE_PREFIX.length, -THEME_VALUE_SUFFIX.length ) .split( '--' ); } else { - // We don't know how to parse the value: either is raw of uses complex CSS such as `calc(1px * var(--wp--variable) )` + // We don't know how to parse the value: either is raw or uses complex CSS such as `calc(1px * var(--wp--variable) )` return variable; } @@ -446,3 +447,92 @@ export function areGlobalStyleConfigsEqual( original, variation ) { fastDeepEqual( original?.settings, variation?.settings ) ); } + +/** + * Collects only the color styles from an elements style object. + * + * @param {Object} elements Elements style object. + * + * @return {Object|undefined} Elements style object containing only color styles. + */ +function getElementColorStyles( elements ) { + if ( ! elements ) { + return; + } + + const elementStyles = {}; + Object.keys( elements ).forEach( ( elementName ) => { + if ( elements[ elementName ].color ) { + elementStyles[ elementName ] = { + color: elements[ elementName ].color, + }; + } + } ); + + return elementStyles; +} + +/** + * Checks whether the provided settings and styles match those contained within + * the supplied colorway. + * + * @param {Object} colorway Colorway theme.json partial. + * @param {Object} settings Current settings. + * @param {Object} styles Block style object. + * + * @return {boolean} Whether the colorway is active or not. + */ +function isColorwayActive( colorway, settings, styles ) { + // 1. Confirm that the current palette matches the colorway. + const hasMatchingPalettes = fastDeepEqual( + colorway.settings?.color?.palette?.theme, + settings.color?.palette?.theme + ); + + if ( ! hasMatchingPalettes ) { + return false; + } + + // TODO: EOD + // 2. Confirm the root color styles match the colorway's. + const colorwayColors = colorway.styles?.color; + const colors = styles.color; + + if ( ! fastDeepEqual( colorwayColors, colors ) ) { + return false; + } + + // 3. Confirm that all the element.color values match as well + const colorwayElements = colorway.styles?.elements; + const elements = styles?.elements; + + if ( colorwayElements || elements ) { + const elementColors = getElementColorStyles( elements ); + + if ( ! fastDeepEqual( colorwayElements, elementColors ) ) { + return false; + } + } + + return true; +} + +/** + * Attempts to find whether any of the supplied colorways can be considered + * active by matching against the current settings and styles. + * + * @param {Array} colorways Collection of colorways. + * @param {Object} settings Current settings. + * @param {Object} styles Block style object. + * + * @return {Object} The active colorway if found. + */ +export function findActiveColorway( colorways, settings, styles ) { + // TODO: Determine if it would be better to implement a custom comparison + // between style objects that would ignore undefined style values. + const cleanedStyles = cleanEmptyObject( styles ); + + return colorways?.find( ( colorway ) => + isColorwayActive( colorway, settings, cleanedStyles ) + ); +} diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index 5767db829d1b37..1fb1077cbbdf31 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -21,6 +21,7 @@ import { import { __experimentalGetGradientClass } from '../components/gradients'; import { cleanEmptyObject, + encodeColorway, transformStyles, shouldSkipSerialization, } from './utils'; @@ -269,14 +270,51 @@ function ColorInspectorControl( { children, resetAllFilter } ) { export function ColorEdit( { clientId, name, setAttributes, settings } ) { const isEnabled = useHasColorPanel( settings ); function selector( select ) { - const { style, textColor, backgroundColor, gradient } = - select( blockEditorStore ).getBlockAttributes( clientId ) || {}; - return { style, textColor, backgroundColor, gradient }; + const { getBlockAttributes, getSettings } = select( blockEditorStore ); + const { + style, + settings: blockSettings, + textColor, + backgroundColor, + gradient, + } = getBlockAttributes( clientId ) || {}; + const { colorways } = getSettings(); + return { + style, + textColor, + blockSettings, + backgroundColor, + gradient, + colorways, + }; } - const { style, textColor, backgroundColor, gradient } = useSelect( - selector, - [ clientId ] + const { + style, + textColor, + blockSettings, + backgroundColor, + gradient, + colorways = [], + } = useSelect( selector, [ clientId ] ); + + // TODO: Update the colorways so they match the encoded preset color styles. + // & decide whether it would be better to manipulate them at all when adding + // to the editor settings + + // TODO: Find better way of comparing color values when their formats vary. + // Alternatively, we could just resort to the extra attribute mentioned in + // the issue, it gets set when we apply the colorway and cleared in any + // other change handler that applies an individual style. + + // Encode all colorway values to `var:preset|color|${slug}`. + // 1. Process root level colors. + // 2. Process elements and their colors. + // 3. Process blocks their colors and any inner element styles??? TODO: + const encodedColorways = useMemo( + () => colorways.map( ( colorway ) => encodeColorway( colorway ) ), + [ colorways ] ); + const value = useMemo( () => { return attributesToStyle( { style, @@ -286,8 +324,29 @@ export function ColorEdit( { clientId, name, setAttributes, settings } ) { } ); }, [ style, textColor, backgroundColor, gradient ] ); - const onChange = ( newStyle ) => { - setAttributes( styleToAttributes( newStyle ) ); + const onChange = ( newStyle, newPalette ) => { + // TODO: Find a better method of handling new palette settings for + // a block instances. + let newSettings; + if ( newPalette || newPalette === null ) { + newSettings = { + settings: cleanEmptyObject( { + ...blockSettings, + color: { + ...blockSettings?.color, + palette: { + ...blockSettings?.color?.palette, + theme: newPalette ? newPalette : undefined, + }, + }, + } ), + }; + } + + setAttributes( { + ...newSettings, + ...styleToAttributes( newStyle ), + } ); }; if ( ! isEnabled ) { @@ -317,6 +376,7 @@ export function ColorEdit( { clientId, name, setAttributes, settings } ) { as={ ColorInspectorControl } panelId={ clientId } settings={ settings } + colorways={ encodedColorways } value={ value } onChange={ onChange } defaultControls={ defaultControls } diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 1c597836e9ec57..b93f8821239593 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -601,3 +601,76 @@ export function createBlockSaveFilter( features ) { } ); } + +/** + * Encodes any CSS var color values contained in the colorway styles to the + * var:preset format used in block style objects for easier comparison in the + * ColorPanel. + * + * @param {Object} unencodedColorway Colorway object to be encoded. + * + * @return {Object} Colorway with encoded color style values. + */ +export function encodeColorway( unencodedColorway ) { + const { color, elements } = unencodedColorway?.styles || {}; + + if ( ! color || ! elements ) { + return unencodedColorway; + } + + const styles = {}; + + if ( color ) { + styles.color = {}; + + Object.keys( color ).forEach( ( key ) => { + styles.color[ key ] = getColorPresetFromCSSVar( color[ key ] ); + } ); + } + + if ( elements ) { + styles.elements = {}; + + // Colorways should only have `color` properties on each element. + Object.keys( elements ).forEach( ( key ) => { + styles.elements[ key ] = { color: {} }; + + Object.keys( elements[ key ].color ).forEach( ( colorKey ) => { + styles.elements[ key ].color[ colorKey ] = + getColorPresetFromCSSVar( + elements[ key ].color[ colorKey ] + ); + } ); + } ); + } + + return { + ...unencodedColorway, + styles: { + ...unencodedColorway.styles, + ...styles, + }, + }; + // TODO: Encode block type colors and their inner element colors. +} + +/** + * Converts a CSS variable for a preset color to an encoded preset string. + * + * @param {string} colorVariable The CSS variable for the preset color. + * + * @return {string|undefined} Preset string in the form of `var:preset|color|{slug}` + */ +function getColorPresetFromCSSVar( colorVariable ) { + if ( ! colorVariable ) { + return; + } + + const slug = colorVariable.match( /var\(--wp--preset--color--(.+)\)/ ); + + if ( ! slug ) { + return colorVariable; + } + + return `var:preset|color|${ slug[ 1 ] }`; +} diff --git a/packages/block-library/src/cover/editor.scss b/packages/block-library/src/cover/editor.scss index 4e2a07ffe8e4ee..51370e64c61493 100644 --- a/packages/block-library/src/cover/editor.scss +++ b/packages/block-library/src/cover/editor.scss @@ -111,7 +111,7 @@ background-attachment: scroll; } -.color-block-support-panel__inner-wrapper > :not(.block-editor-tools-panel-color-gradient-settings__item) { +.color-block-support-panel__inner-wrapper > .block-editor-tools-panel-color-gradient-settings__item + :not(.block-editor-tools-panel-color-gradient-settings__item) { margin-top: $grid-unit-30; } diff --git a/packages/edit-site/src/components/global-styles/screen-colors.js b/packages/edit-site/src/components/global-styles/screen-colors.js index a19bd16c8839f2..35273967468047 100644 --- a/packages/edit-site/src/components/global-styles/screen-colors.js +++ b/packages/edit-site/src/components/global-styles/screen-colors.js @@ -29,6 +29,7 @@ function ScreenColors() { const [ rawSettings ] = useGlobalSetting( '' ); const settings = useSettingsForBlockElement( rawSettings ); + // TODO: Retrieve colorways. return ( <>