From 192bcf42d2b618120ce17f1b1e4c9d39bf53c9ce Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 24 Mar 2020 14:15:17 +0100 Subject: [PATCH] Introduce a support key for support global style colors in blocks (#21021) --- .../components/block-list/block-wrapper.js | 7 +- .../src/components/block-list/block.js | 2 +- packages/block-editor/src/hooks/color.js | 263 ++++++++++++++++++ packages/block-editor/src/hooks/index.js | 2 + packages/block-editor/src/hooks/style.js | 145 ++++++++++ packages/block-editor/src/hooks/test/color.js | 12 + packages/block-editor/src/hooks/test/style.js | 32 +++ packages/block-library/src/group/block.json | 16 +- .../block-library/src/group/deprecated.js | 75 +++++ packages/block-library/src/group/edit.js | 66 ++--- packages/block-library/src/group/index.js | 9 +- packages/block-library/src/group/save.js | 33 +-- packages/block-library/src/group/style.scss | 4 + packages/block-library/src/style.scss | 1 + .../fixtures/blocks/core__group.html | 2 +- .../blocks/core__group.serialized.html | 2 +- .../blocks/core__group__deprecated.html | 2 +- .../core__group__deprecated.serialized.html | 2 +- 18 files changed, 575 insertions(+), 100 deletions(-) create mode 100644 packages/block-editor/src/hooks/color.js create mode 100644 packages/block-editor/src/hooks/style.js create mode 100644 packages/block-editor/src/hooks/test/color.js create mode 100644 packages/block-editor/src/hooks/test/style.js create mode 100644 packages/block-library/src/group/style.scss diff --git a/packages/block-editor/src/components/block-list/block-wrapper.js b/packages/block-editor/src/components/block-list/block-wrapper.js index c728b2a8a0136..8d19c38e3e0b1 100644 --- a/packages/block-editor/src/components/block-list/block-wrapper.js +++ b/packages/block-editor/src/components/block-list/block-wrapper.js @@ -199,7 +199,11 @@ const BlockComponent = forwardRef( { ...props } id={ blockElementId } ref={ wrapper } - className={ classnames( className, props.className ) } + className={ classnames( + className, + props.className, + wrapperProps && wrapperProps.className + ) } data-block={ clientId } data-type={ name } data-title={ blockTitle } @@ -209,6 +213,7 @@ const BlockComponent = forwardRef( onMouseLeave={ isSelected ? onMouseLeave : undefined } tabIndex="0" style={ { + ...( wrapperProps ? wrapperProps.style : {} ), ...( props.style || {} ), ...animationStyle, } } diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 2ae2354576a6f..78d7f4fe399dc 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -88,7 +88,7 @@ function BlockListBlock( { isDraggingBlocks && ( isSelected || isPartOfMultiSelection ); // Determine whether the block has props to apply to the wrapper. - if ( ! lightBlockWrapper && blockType.getEditWrapperProps ) { + if ( blockType.getEditWrapperProps ) { wrapperProps = { ...wrapperProps, ...blockType.getEditWrapperProps( attributes ), diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js new file mode 100644 index 0000000000000..30762674ffec9 --- /dev/null +++ b/packages/block-editor/src/hooks/color.js @@ -0,0 +1,263 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { pickBy, isEqual, isObject, identity, mapValues } from 'lodash'; + +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { hasBlockSupport } from '@wordpress/blocks'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; +import { useState, useEffect } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { + getColorClassName, + getColorObjectByColorValue, + getColorObjectByAttributeValues, +} from '../components/colors'; +import PanelColorSettings from '../components/panel-color-settings'; +import ContrastChecker from '../components/contrast-checker'; +import InspectorControls from '../components/inspector-controls'; +import { getBlockDOMNode } from '../utils/dom'; + +export const COLOR_SUPPORT_KEY = '__experimentalColor'; + +export const cleanEmptyObject = ( object ) => { + if ( ! isObject( object ) ) { + return object; + } + const cleanedNestedObjects = pickBy( + mapValues( object, cleanEmptyObject ), + identity + ); + return isEqual( cleanedNestedObjects, {} ) + ? undefined + : cleanedNestedObjects; +}; + +/** + * Filters registered block settings, extending attributes to include + * `backgroundColor` and `textColor` attribute. + * + * @param {Object} settings Original block settings + * @return {Object} Filtered block settings + */ +function addAttributes( settings ) { + if ( ! hasBlockSupport( settings, COLOR_SUPPORT_KEY ) ) { + return settings; + } + + // allow blocks to specify their own attribute definition with default values if needed. + if ( ! settings.attributes.backgroundColor ) { + Object.assign( settings.attributes, { + backgroundColor: { + type: 'string', + }, + } ); + } + if ( ! settings.attributes.textColor ) { + Object.assign( settings.attributes, { + textColor: { + type: 'string', + }, + } ); + } + + return settings; +} + +/** + * Override props assigned to save component to inject colors classnames. + * + * @param {Object} props Additional props applied to save element + * @param {Object} blockType Block type + * @param {Object} attributes Block attributes + * @return {Object} Filtered props applied to save element + */ +export function addSaveProps( props, blockType, attributes ) { + if ( ! hasBlockSupport( blockType, COLOR_SUPPORT_KEY ) ) { + return props; + } + + // I'd have prefered to avoid the "style" attribute usage here + const { backgroundColor, textColor, style } = attributes; + + const backgroundClass = getColorClassName( + 'background-color', + backgroundColor + ); + const textClass = getColorClassName( 'color', textColor ); + props.className = classnames( props.className, backgroundClass, textClass, { + 'has-text-color': textColor || style?.color?.text, + 'has-background': backgroundColor || style?.color?.background, + } ); + + return props; +} + +/** + * Filters registered block settings to extand the block edit wrapper + * to apply the desired styles and classnames properly. + * + * @param {Object} settings Original block settings + * @return {Object} Filtered block settings + */ +export function addEditProps( settings ) { + if ( ! hasBlockSupport( settings, COLOR_SUPPORT_KEY ) ) { + return settings; + } + const existingGetEditWrapperProps = settings.getEditWrapperProps; + settings.getEditWrapperProps = ( attributes ) => { + let props = {}; + if ( existingGetEditWrapperProps ) { + props = existingGetEditWrapperProps( attributes ); + } + return addSaveProps( props, settings, attributes ); + }; + + return settings; +} + +const ColorPanel = ( { colorSettings, clientId } ) => { + const { getComputedStyle, Node } = window; + + const [ detectedBackgroundColor, setDetectedBackgroundColor ] = useState(); + const [ detectedColor, setDetectedColor ] = useState(); + + useEffect( () => { + const colorsDetectionElement = getBlockDOMNode( clientId ); + setDetectedColor( getComputedStyle( colorsDetectionElement ).color ); + + let backgroundColorNode = colorsDetectionElement; + let backgroundColor = getComputedStyle( backgroundColorNode ) + .backgroundColor; + while ( + backgroundColor === 'rgba(0, 0, 0, 0)' && + backgroundColorNode.parentNode && + backgroundColorNode.parentNode.nodeType === Node.ELEMENT_NODE + ) { + backgroundColorNode = backgroundColorNode.parentNode; + backgroundColor = getComputedStyle( backgroundColorNode ) + .backgroundColor; + } + + setDetectedBackgroundColor( backgroundColor ); + } ); + + return ( + + + + + + ); +}; + +/** + * Override the default edit UI to include new inspector controls for block + * color, if block defines support. + * + * @param {Function} BlockEdit Original component + * @return {Function} Wrapped component + */ +export const withBlockControls = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const { name: blockName } = props; + const colors = useSelect( ( select ) => { + return select( 'core/block-editor' ).getSettings().colors; + }, [] ); + + if ( ! hasBlockSupport( blockName, COLOR_SUPPORT_KEY ) ) { + return ; + } + const { style, textColor, backgroundColor } = props.attributes; + + const onChangeColor = ( name ) => ( value ) => { + const colorObject = getColorObjectByColorValue( colors, value ); + const attributeName = name + 'Color'; + const newStyle = { + ...style, + color: { + ...style?.color, + [ name ]: colorObject?.slug ? undefined : value, + }, + }; + const newNamedColor = colorObject?.slug + ? colorObject.slug + : undefined; + props.setAttributes( { + style: cleanEmptyObject( newStyle ), + [ attributeName ]: newNamedColor, + } ); + }; + + return [ + , + , + ]; + }, + 'withToolbarControls' +); + +addFilter( + 'blocks.registerBlockType', + 'core/color/addAttribute', + addAttributes +); + +addFilter( + 'blocks.getSaveContent.extraProps', + 'core/color/addSaveProps', + addSaveProps +); + +addFilter( + 'blocks.registerBlockType', + 'core/color/addEditProps', + addEditProps +); + +addFilter( + 'editor.BlockEdit', + 'core/color/with-block-controls', + withBlockControls +); diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index ab3ae2756ce70..76897c0966921 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -5,5 +5,7 @@ import { AlignmentHookSettingsProvider } from './align'; import './anchor'; import './custom-class-name'; import './generated-class-name'; +import './style'; +import './color'; export { AlignmentHookSettingsProvider }; diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js new file mode 100644 index 0000000000000..5eea58cea5b9f --- /dev/null +++ b/packages/block-editor/src/hooks/style.js @@ -0,0 +1,145 @@ +/** + * External dependencies + */ +import { mapKeys, kebabCase, isObject, entries } from 'lodash'; + +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { hasBlockSupport } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { COLOR_SUPPORT_KEY } from './color'; + +const styleSupportKeys = [ COLOR_SUPPORT_KEY ]; + +const hasStyleSupport = ( blockType ) => + styleSupportKeys.some( ( key ) => hasBlockSupport( blockType, key ) ); + +/** + * Flatten a nested Global styles config and generates the corresponding + * flattened CSS variables. + * + * @param {Object} styles Styles configuration + * @return {Object} Flattened CSS variables declaration + */ +export function getCSSVariables( styles = {} ) { + const prefix = '--wp'; + const token = '--'; + const getNestedCSSVariables = ( config ) => { + let result = {}; + entries( config ).forEach( ( [ key, value ] ) => { + if ( ! isObject( value ) ) { + result[ kebabCase( key ) ] = value; + return; + } + + result = { + ...result, + ...mapKeys( + getNestedCSSVariables( value ), + ( _, subkey ) => kebabCase( key ) + token + subkey + ), + }; + } ); + + return result; + }; + + return mapKeys( + getNestedCSSVariables( styles ), + ( _, key ) => prefix + token + key + ); +} + +/** + * Filters registered block settings, extending attributes to include `style` attribute. + * + * @param {Object} settings Original block settings + * @return {Object} Filtered block settings + */ +function addAttribute( settings ) { + if ( ! hasStyleSupport( settings ) ) { + return settings; + } + + // allow blocks to specify their own attribute definition with default values if needed. + if ( ! settings.attributes.style ) { + Object.assign( settings.attributes, { + style: { + type: 'object', + }, + } ); + } + + return settings; +} + +/** + * Override props assigned to save component to inject the CSS variables definition. + * + * @param {Object} props Additional props applied to save element + * @param {Object} blockType Block type + * @param {Object} attributes Block attributes + * @return {Object} Filtered props applied to save element + */ +export function addSaveProps( props, blockType, attributes ) { + if ( ! hasStyleSupport( blockType ) ) { + return props; + } + + const { style } = attributes; + + props.style = { + ...getCSSVariables( style ), + ...props.style, + }; + + return props; +} + +/** + * Filters registered block settings to extand the block edit wrapper + * to apply the desired styles and classnames properly. + * + * @param {Object} settings Original block settings + * @return {Object} Filtered block settings + */ +export function addEditProps( settings ) { + if ( ! hasStyleSupport( settings ) ) { + return settings; + } + + const existingGetEditWrapperProps = settings.getEditWrapperProps; + settings.getEditWrapperProps = ( attributes ) => { + let props = {}; + if ( existingGetEditWrapperProps ) { + props = existingGetEditWrapperProps( attributes ); + } + + return addSaveProps( props, settings, attributes ); + }; + + return settings; +} + +addFilter( + 'blocks.registerBlockType', + 'core/style/addAttribute', + addAttribute +); + +addFilter( + 'blocks.getSaveContent.extraProps', + 'core/style/addSaveProps', + addSaveProps +); + +addFilter( + 'blocks.registerBlockType', + 'core/style/addEditProps', + addEditProps +); diff --git a/packages/block-editor/src/hooks/test/color.js b/packages/block-editor/src/hooks/test/color.js new file mode 100644 index 0000000000000..d8644c931a3e5 --- /dev/null +++ b/packages/block-editor/src/hooks/test/color.js @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import { cleanEmptyObject } from '../color'; + +describe( 'cleanEmptyObject', () => { + it( 'should remove nested keys', () => { + expect( cleanEmptyObject( { color: { text: undefined } } ) ).toEqual( + undefined + ); + } ); +} ); diff --git a/packages/block-editor/src/hooks/test/style.js b/packages/block-editor/src/hooks/test/style.js new file mode 100644 index 0000000000000..886777ae8aa6c --- /dev/null +++ b/packages/block-editor/src/hooks/test/style.js @@ -0,0 +1,32 @@ +/** + * Internal dependencies + */ +import { getCSSVariables } from '../style'; + +describe( 'getCSSVariables', () => { + it( 'should return an empty object when called with undefined', () => { + expect( getCSSVariables() ).toEqual( {} ); + } ); + + it( 'should return the correct simple CSS variables', () => { + expect( getCSSVariables( { color: 'red' } ) ).toEqual( { + '--wp--color': 'red', + } ); + } ); + + it( 'should omit CSS variables when the provided value is falsy', () => { + expect( getCSSVariables( { color: undefined } ) ).toEqual( {} ); + } ); + + it( 'should flatten nested style config', () => { + expect( + getCSSVariables( { + color: { text: 'red' }, + typography: { lineHeight: 1.5 }, + } ) + ).toEqual( { + '--wp--color--text': 'red', + '--wp--typography--line-height': 1.5, + } ); + } ); +} ); diff --git a/packages/block-library/src/group/block.json b/packages/block-library/src/group/block.json index 8883720bbc2aa..9709929cb3427 100644 --- a/packages/block-library/src/group/block.json +++ b/packages/block-library/src/group/block.json @@ -1,18 +1,4 @@ { "name": "core/group", - "category": "layout", - "attributes": { - "backgroundColor": { - "type": "string" - }, - "customBackgroundColor": { - "type": "string" - }, - "textColor": { - "type": "string" - }, - "customTextColor": { - "type": "string" - } - } + "category": "layout" } diff --git a/packages/block-library/src/group/deprecated.js b/packages/block-library/src/group/deprecated.js index 31440c003f866..e828b399d23a9 100644 --- a/packages/block-library/src/group/deprecated.js +++ b/packages/block-library/src/group/deprecated.js @@ -8,7 +8,80 @@ import classnames from 'classnames'; */ import { InnerBlocks, getColorClassName } from '@wordpress/block-editor'; +const migrateCustomColors = ( attributes ) => { + if ( ! attributes.customTextColor && ! attributes.customBackgroundColor ) { + return attributes; + } + const style = { color: {} }; + if ( attributes.customTextColor ) { + style.color.text = attributes.customTextColor; + } + if ( attributes.customBackgroundColor ) { + style.color.background = attributes.customBackgroundColor; + } + return { + ...attributes, + style, + }; +}; + const deprecated = [ + // Version of the block without global styles support + { + attributes: { + backgroundColor: { + type: 'string', + }, + customBackgroundColor: { + type: 'string', + }, + textColor: { + type: 'string', + }, + customTextColor: { + type: 'string', + }, + }, + supports: { + align: [ 'wide', 'full' ], + anchor: true, + html: false, + }, + migrate: migrateCustomColors, + save( { attributes } ) { + const { + backgroundColor, + customBackgroundColor, + textColor, + customTextColor, + } = attributes; + + const backgroundClass = getColorClassName( + 'background-color', + backgroundColor + ); + const textClass = getColorClassName( 'color', textColor ); + const className = classnames( backgroundClass, textClass, { + 'has-text-color': textColor || customTextColor, + 'has-background': backgroundColor || customBackgroundColor, + } ); + + const styles = { + backgroundColor: backgroundClass + ? undefined + : customBackgroundColor, + color: textClass ? undefined : customTextColor, + }; + + return ( +
+
+ +
+
+ ); + }, + }, // Version of the group block with a bug that made text color class not applied. { attributes: { @@ -25,6 +98,7 @@ const deprecated = [ type: 'string', }, }, + migrate: migrateCustomColors, supports: { align: [ 'wide', 'full' ], anchor: true, @@ -79,6 +153,7 @@ const deprecated = [ anchor: true, html: false, }, + migrate: migrateCustomColors, save( { attributes } ) { const { backgroundColor, customBackgroundColor } = attributes; diff --git a/packages/block-library/src/group/edit.js b/packages/block-library/src/group/edit.js index c0d195c997f01..37224c57e425d 100644 --- a/packages/block-library/src/group/edit.js +++ b/packages/block-library/src/group/edit.js @@ -1,61 +1,33 @@ /** * WordPress dependencies */ -import { withSelect } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; +import { useSelect } from '@wordpress/data'; import { InnerBlocks, - __experimentalUseColors, __experimentalBlock as Block, } from '@wordpress/block-editor'; -import { useRef } from '@wordpress/element'; -function GroupEdit( { hasInnerBlocks, className } ) { - const ref = useRef(); - const { - TextColor, - BackgroundColor, - InspectorControlsColorPanel, - } = __experimentalUseColors( - [ - { name: 'textColor', property: 'color' }, - { name: 'backgroundColor', className: 'has-background' }, - ], - { - contrastCheckers: [ { backgroundColor: true, textColor: true } ], - colorDetector: { targetRef: ref }, - } +function GroupEdit( { className, clientId } ) { + const hasInnerBlocks = useSelect( + ( select ) => { + const { getBlock } = select( 'core/block-editor' ); + const block = getBlock( clientId ); + return !! ( block && block.innerBlocks.length ); + }, + [ clientId ] ); return ( - <> - { InspectorControlsColorPanel } - - - -
- -
-
-
-
- + +
+ +
+
); } -export default compose( [ - withSelect( ( select, { clientId } ) => { - const { getBlock } = select( 'core/block-editor' ); - - const block = getBlock( clientId ); - - return { - hasInnerBlocks: !! ( block && block.innerBlocks.length ), - }; - } ), -] )( GroupEdit ); +export default GroupEdit; diff --git a/packages/block-library/src/group/index.js b/packages/block-library/src/group/index.js index c977195156b73..25e01544e42fa 100644 --- a/packages/block-library/src/group/index.js +++ b/packages/block-library/src/group/index.js @@ -29,8 +29,12 @@ export const settings = { ], example: { attributes: { - customBackgroundColor: '#ffffff', - customTextColor: '#000000', + style: { + color: { + text: '#000000', + background: '#ffffff', + }, + }, }, innerBlocks: [ { @@ -88,6 +92,7 @@ export const settings = { anchor: true, html: false, lightBlockWrapper: true, + __experimentalColor: true, }, transforms: { from: [ diff --git a/packages/block-library/src/group/save.js b/packages/block-library/src/group/save.js index 6d248efbfc257..e411b3a253c85 100644 --- a/packages/block-library/src/group/save.js +++ b/packages/block-library/src/group/save.js @@ -1,38 +1,11 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ -import { InnerBlocks, getColorClassName } from '@wordpress/block-editor'; - -export default function save( { attributes } ) { - const { - backgroundColor, - customBackgroundColor, - textColor, - customTextColor, - } = attributes; - - const backgroundClass = getColorClassName( - 'background-color', - backgroundColor - ); - const textClass = getColorClassName( 'color', textColor ); - const className = classnames( backgroundClass, textClass, { - 'has-text-color': textColor || customTextColor, - 'has-background': backgroundColor || customBackgroundColor, - } ); - - const styles = { - backgroundColor: backgroundClass ? undefined : customBackgroundColor, - color: textClass ? undefined : customTextColor, - }; +import { InnerBlocks } from '@wordpress/block-editor'; +export default function save() { return ( -
+
diff --git a/packages/block-library/src/group/style.scss b/packages/block-library/src/group/style.scss new file mode 100644 index 0000000000000..d71f534fd2d97 --- /dev/null +++ b/packages/block-library/src/group/style.scss @@ -0,0 +1,4 @@ +.wp-block-group { + background-color: var(--wp--color--background); + color: var(--wp--color--text); +} diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index bd8ab8a093bb5..ab0c0e47ff22e 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -13,6 +13,7 @@ @import "./embed/style.scss"; @import "./file/style.scss"; @import "./gallery/style.scss"; +@import "./group/style.scss"; @import "./image/style.scss"; @import "./latest-comments/style.scss"; @import "./latest-posts/style.scss"; diff --git a/packages/e2e-tests/fixtures/blocks/core__group.html b/packages/e2e-tests/fixtures/blocks/core__group.html index 09fd95cfc9d5a..e5df0f2fce926 100644 --- a/packages/e2e-tests/fixtures/blocks/core__group.html +++ b/packages/e2e-tests/fixtures/blocks/core__group.html @@ -1,4 +1,4 @@ - +
diff --git a/packages/e2e-tests/fixtures/blocks/core__group.serialized.html b/packages/e2e-tests/fixtures/blocks/core__group.serialized.html index 2c398b530dedc..8ac236255f9f6 100644 --- a/packages/e2e-tests/fixtures/blocks/core__group.serialized.html +++ b/packages/e2e-tests/fixtures/blocks/core__group.serialized.html @@ -1,4 +1,4 @@ - +

This is a group block.

diff --git a/packages/e2e-tests/fixtures/blocks/core__group__deprecated.html b/packages/e2e-tests/fixtures/blocks/core__group__deprecated.html index bd111993515f1..9b71121607e43 100644 --- a/packages/e2e-tests/fixtures/blocks/core__group__deprecated.html +++ b/packages/e2e-tests/fixtures/blocks/core__group__deprecated.html @@ -1,4 +1,4 @@ - +

test

diff --git a/packages/e2e-tests/fixtures/blocks/core__group__deprecated.serialized.html b/packages/e2e-tests/fixtures/blocks/core__group__deprecated.serialized.html index 891c48e13ef04..b9f1dc3ba37e1 100644 --- a/packages/e2e-tests/fixtures/blocks/core__group__deprecated.serialized.html +++ b/packages/e2e-tests/fixtures/blocks/core__group__deprecated.serialized.html @@ -1,4 +1,4 @@ - +

test