From 434bb66bad7a6e1ed9af6b541a4f132c46191823 Mon Sep 17 00:00:00 2001 From: Enrique Piqueras Date: Fri, 25 Oct 2019 14:25:11 -0700 Subject: [PATCH] Block Editor: Implement new colors hook. (#16781) * Block Editor: Implement new colors hook. * Block Library: Swap usage of the colors HOC with the colors hook in the heading edit component. * Use Colors: Add 'has-x-color' class names. * Use Colors: Avoid memory leaks by making caches limited in size, and tied to hook instances. * Use Colors: Support children and optional contrast checking in the color panel. * Use Colors: Expose colors panel without inspector slot/fill wrapper. * Use Colors: Mark hook as experimental. * Use Colors: Support custom colors. * Block Edit: Remove extra context values and use selectors/actions instead. * Heading: Remove unnecessary color class and set text color on save. * Use Colors: Add custom/preset color logic. * Use Colors: Fix panel bugs. * Heading Block: Detect actual background color for contrast checking. * Block Edit: Add new export to native file. * Use Colors: Change CSS "attribute" to "property". --- packages/block-editor/README.md | 4 + .../src/components/block-edit/context.js | 8 +- .../src/components/block-edit/index.js | 3 +- .../src/components/colors/index.js | 1 + .../src/components/colors/use-colors.js | 207 ++++++++++++++++++ packages/block-editor/src/components/index.js | 2 +- .../src/components/index.native.js | 2 +- packages/block-library/src/heading/edit.js | 109 ++++----- 8 files changed, 270 insertions(+), 66 deletions(-) create mode 100644 packages/block-editor/src/components/colors/use-colors.js diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index dd62423fe7f77..432d36a72ac70 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -459,6 +459,10 @@ _Related_ - +# **useBlockEditContext** + +Undocumented declaration. + # **Warning** Undocumented declaration. diff --git a/packages/block-editor/src/components/block-edit/context.js b/packages/block-editor/src/components/block-edit/context.js index 2f949882593d9..4157735cd2fa7 100644 --- a/packages/block-editor/src/components/block-edit/context.js +++ b/packages/block-editor/src/components/block-edit/context.js @@ -6,18 +6,22 @@ import { noop } from 'lodash'; /** * WordPress dependencies */ -import { createContext } from '@wordpress/element'; +import { createContext, useContext } from '@wordpress/element'; import { createHigherOrderComponent } from '@wordpress/compose'; -const { Consumer, Provider } = createContext( { +const Context = createContext( { name: '', isSelected: false, focusedElement: null, setFocusedElement: noop, clientId: null, } ); +const { Provider, Consumer } = Context; export { Provider as BlockEditContextProvider }; +export function useBlockEditContext() { + return useContext( Context ); +} /** * A Higher Order Component used to inject BlockEdit context to the diff --git a/packages/block-editor/src/components/block-edit/index.js b/packages/block-editor/src/components/block-edit/index.js index 63c475a50692f..403a5cd87898e 100644 --- a/packages/block-editor/src/components/block-edit/index.js +++ b/packages/block-editor/src/components/block-edit/index.js @@ -12,7 +12,7 @@ import { Component } from '@wordpress/element'; * Internal dependencies */ import Edit from './edit'; -import { BlockEditContextProvider } from './context'; +import { BlockEditContextProvider, useBlockEditContext } from './context'; class BlockEdit extends Component { constructor() { @@ -44,3 +44,4 @@ class BlockEdit extends Component { } export default BlockEdit; +export { useBlockEditContext }; diff --git a/packages/block-editor/src/components/colors/index.js b/packages/block-editor/src/components/colors/index.js index f6b6fac984db8..cadb34a8c9aa3 100644 --- a/packages/block-editor/src/components/colors/index.js +++ b/packages/block-editor/src/components/colors/index.js @@ -7,3 +7,4 @@ export { createCustomColorsHOC, default as withColors, } from './with-colors'; +export { default as __experimentalUseColors } from './use-colors'; diff --git a/packages/block-editor/src/components/colors/use-colors.js b/packages/block-editor/src/components/colors/use-colors.js new file mode 100644 index 0000000000000..ee3f37c8b00bf --- /dev/null +++ b/packages/block-editor/src/components/colors/use-colors.js @@ -0,0 +1,207 @@ +/** + * External dependencies + */ +import memoize from 'memize'; +import { kebabCase, camelCase, startCase } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { + useCallback, + useMemo, + Children, + cloneElement, +} from '@wordpress/element'; + +/** + * Internal dependencies + */ +import PanelColorSettings from '../panel-color-settings'; +import ContrastChecker from '../contrast-checker'; +import InspectorControls from '../inspector-controls'; +import { useBlockEditContext } from '../block-edit'; + +const ColorPanel = ( { + title, + colorSettings, + colorPanelProps, + contrastCheckerProps, + components, + panelChildren, +} ) => ( + + { contrastCheckerProps && + components.map( ( Component, index ) => ( + + ) ) } + { typeof panelChildren === 'function' ? + panelChildren( components ) : + panelChildren } + +); +const InspectorControlsColorPanel = ( props ) => ( + + + +); + +export default function __experimentalUseColors( + colorConfigs, + { + panelTitle = __( 'Color Settings' ), + colorPanelProps, + contrastCheckerProps, + panelChildren, + } = { + panelTitle: __( 'Color Settings' ), + }, + deps = [] +) { + const { clientId } = useBlockEditContext(); + const { attributes, settingsColors } = useSelect( + ( select ) => { + const { getBlockAttributes, getSettings } = select( 'core/block-editor' ); + return { + attributes: getBlockAttributes( clientId ), + settingsColors: getSettings().colors, + }; + }, + [ clientId ] + ); + const { updateBlockAttributes } = useDispatch( 'core/block-editor' ); + const setAttributes = useCallback( + ( newAttributes ) => updateBlockAttributes( clientId, newAttributes ), + [ updateBlockAttributes, clientId ] + ); + + const createComponent = useMemo( + () => + memoize( + ( property, color, colorValue, customColor ) => ( { children } ) => + // Clone children, setting the style property from the color configuration, + // if not already set explicitly through props. + Children.map( children, ( child ) => { + let className = child.props.className; + let style = child.props.style; + if ( color ) { + className = `${ child.props.className } has-${ kebabCase( + color + ) }-${ kebabCase( property ) }`; + style = { [ property ]: colorValue, ...child.props.style }; + } else if ( customColor ) { + className = `${ child.props.className } has-${ kebabCase( property ) }`; + style = { [ property ]: customColor, ...child.props.style }; + } + return cloneElement( child, { + className, + style, + } ); + } ), + { maxSize: colorConfigs.length } + ), + [ colorConfigs.length ] + ); + const createSetColor = useMemo( + () => + memoize( + ( name, colors ) => ( newColor ) => { + const color = colors.find( ( _color ) => _color.color === newColor ); + setAttributes( { + [ color ? camelCase( `custom ${ name }` ) : name ]: undefined, + } ); + setAttributes( { + [ color ? name : camelCase( `custom ${ name }` ) ]: color ? + color.slug : + newColor, + } ); + }, + { + maxSize: colorConfigs.length, + } + ), + [ setAttributes, colorConfigs.length ] + ); + + return useMemo( () => { + const colorSettings = []; + + const components = colorConfigs.reduce( ( acc, colorConfig ) => { + if ( typeof colorConfig === 'string' ) { + colorConfig = { name: colorConfig }; + } + const { + name, // E.g. 'backgroundColor'. + property = name, // E.g. 'backgroundColor'. + + panelLabel = startCase( name ), // E.g. 'Background Color'. + componentName = panelLabel.replace( /\s/g, '' ), // E.g. 'BackgroundColor'. + + color = colorConfig.color, + colors = settingsColors, + } = { + ...colorConfig, + color: attributes[ colorConfig.name ], + }; + + // We memoize the non-primitives to avoid unnecessary updates + // when they are used as props for other components. + const _color = colors.find( ( __color ) => __color.slug === color ); + acc[ componentName ] = createComponent( + property, + color, + _color && _color.color, + attributes[ camelCase( `custom ${ name }` ) ] + ); + acc[ componentName ].displayName = componentName; + acc[ componentName ].color = color; + acc[ componentName ].setColor = createSetColor( name, colors ); + + const newSettingIndex = + colorSettings.push( { + value: _color ? + _color.color : + attributes[ camelCase( `custom ${ name }` ) ], + onChange: acc[ componentName ].setColor, + label: panelLabel, + colors, + } ) - 1; + // These settings will be spread over the `colors` in + // `colorPanelProps`, so we need to unset the key here, + // if not set to an actual value, to avoid overwriting + // an actual value in `colorPanelProps`. + if ( ! colors ) { + delete colorSettings[ newSettingIndex ].colors; + } + + return acc; + }, {} ); + + const wrappedColorPanelProps = { + title: panelTitle, + colorSettings, + colorPanelProps, + contrastCheckerProps, + components: Object.values( components ), + panelChildren, + }; + return { + ...components, + ColorPanel: , + InspectorControlsColorPanel: ( + + ), + }; + }, [ attributes, setAttributes, ...deps ] ); +} diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 1c07bbd80e38e..5818e37395c8e 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -9,7 +9,7 @@ export { default as Autocomplete } from './autocomplete'; export { default as BlockAlignmentToolbar } from './block-alignment-toolbar'; export { default as BlockBreadcrumb } from './block-breadcrumb'; export { default as BlockControls } from './block-controls'; -export { default as BlockEdit } from './block-edit'; +export { default as BlockEdit, useBlockEditContext } from './block-edit'; export { default as BlockFormatControls } from './block-format-controls'; export { default as BlockIcon } from './block-icon'; export { default as BlockNavigationDropdown } from './block-navigation/dropdown'; diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 9d293b8ccc153..6baafd5d8f0bb 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -1,7 +1,7 @@ // Block Creation Components export { default as BlockAlignmentToolbar } from './block-alignment-toolbar'; export { default as BlockControls } from './block-controls'; -export { default as BlockEdit } from './block-edit'; +export { default as BlockEdit, useBlockEditContext } from './block-edit'; export { default as BlockFormatControls } from './block-format-controls'; export { default as BlockIcon } from './block-icon'; export { default as BlockVerticalAlignmentToolbar } from './block-vertical-alignment-toolbar'; diff --git a/packages/block-library/src/heading/edit.js b/packages/block-library/src/heading/edit.js index d500309a0c9aa..241a327bb42c1 100644 --- a/packages/block-library/src/heading/edit.js +++ b/packages/block-library/src/heading/edit.js @@ -12,49 +12,37 @@ import HeadingToolbar from './heading-toolbar'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { PanelBody } from '@wordpress/components'; -import { compose } from '@wordpress/compose'; +import { PanelBody, withFallbackStyles } from '@wordpress/components'; import { createBlock } from '@wordpress/blocks'; import { AlignmentToolbar, BlockControls, InspectorControls, RichText, - withColors, - PanelColorSettings, + __experimentalUseColors, } from '@wordpress/block-editor'; -import { memo } from '@wordpress/element'; -const HeadingColorUI = memo( - function( { - textColorValue, - setTextColor, - } ) { - return ( - - ); - } -); +/** + * Browser dependencies + */ +const { getComputedStyle } = window; function HeadingEdit( { + backgroundColor, attributes, setAttributes, mergeBlocks, onReplace, className, - textColor, - setTextColor, } ) { + const { TextColor, InspectorControlsColorPanel } = __experimentalUseColors( + [ { name: 'textColor', property: 'color' } ], + { + contrastCheckerProps: { backgroundColor, isLargeText: true }, + }, + [] + ); + const { align, content, level, placeholder } = attributes; const tagName = 'h' + level; @@ -71,43 +59,42 @@ function HeadingEdit( {

{ __( 'Level' ) }

setAttributes( { level: newLevel } ) } /> - - setAttributes( { content: value } ) } - onMerge={ mergeBlocks } - onSplit={ ( value ) => { - if ( ! value ) { - return createBlock( 'core/paragraph' ); - } + { InspectorControlsColorPanel } + + setAttributes( { content: value } ) } + onMerge={ mergeBlocks } + onSplit={ ( value ) => { + if ( ! value ) { + return createBlock( 'core/paragraph' ); + } - return createBlock( 'core/heading', { - ...attributes, - content: value, - } ); - } } - onReplace={ onReplace } - onRemove={ () => onReplace( [] ) } - className={ classnames( className, { - [ `has-text-align-${ align }` ]: align, - 'has-text-color': textColor.color, - [ textColor.class ]: textColor.class, - } ) } - placeholder={ placeholder || __( 'Write heading…' ) } - style={ { - color: textColor.color, - } } - /> + return createBlock( 'core/heading', { + ...attributes, + content: value, + } ); + } } + onReplace={ onReplace } + onRemove={ () => onReplace( [] ) } + className={ classnames( className, { + [ `has-text-align-${ align }` ]: align, + } ) } + placeholder={ placeholder || __( 'Write heading…' ) } + /> + ); } -export default compose( [ - withColors( 'backgroundColor', { textColor: 'color' } ), -] )( HeadingEdit ); +export default withFallbackStyles( ( node ) => { + let backgroundColor = getComputedStyle( node ).backgroundColor; + while ( backgroundColor === 'rgba(0, 0, 0, 0)' && node.parentNode ) { + node = node.parentNode; + backgroundColor = getComputedStyle( node ).backgroundColor; + } + return { backgroundColor }; +} )( HeadingEdit );