-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Block Editor: Implement new colors hook. #16781
Changes from all commits
652a37d
5c10afe
ead8a38
7809eaf
ee203c8
3316f3b
9dcbb9f
86efb01
69865dd
b3d600e
81c60f0
25053b5
fa6c34f
da24e6c
90990ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} ) => ( | ||
<PanelColorSettings | ||
title={ title } | ||
initialOpen={ false } | ||
colorSettings={ colorSettings } | ||
{ ...colorPanelProps } | ||
> | ||
{ contrastCheckerProps && | ||
components.map( ( Component, index ) => ( | ||
<ContrastChecker | ||
key={ Component.displayName } | ||
textColor={ colorSettings[ index ].value } | ||
{ ...contrastCheckerProps } | ||
/> | ||
) ) } | ||
{ typeof panelChildren === 'function' ? | ||
panelChildren( components ) : | ||
panelChildren } | ||
</PanelColorSettings> | ||
); | ||
const InspectorControlsColorPanel = ( props ) => ( | ||
<InspectorControls> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we don't need to assume the panel is nested inside InspectorControls and we can left the choice for the user. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where else would you render it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In a placeholder for example as part of a flow to initialize a block. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rather expose that under |
||
<ColorPanel { ...props } /> | ||
</InspectorControls> | ||
); | ||
|
||
export default function __experimentalUseColors( | ||
colorConfigs, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should have a useColors or a useColor hook used multiple times. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Plural, because we want one panel inspector controls per list of colors, not per color. |
||
{ | ||
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have a small function that computes the class name from the color slug There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That one works slightly differently. It takes a kebab cased context name and the slug. Here we have both the color and attribute name in camel case and need to convert one or both depending on whether it's a custom color or not. |
||
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 ] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I take it that relying on I can see why it works, but it's strictly speaking not correct. Just noting it here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For each use of the hook we need a |
||
); | ||
const createSetColor = useMemo( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To avoid calling |
||
() => | ||
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: <ColorPanel { ...wrappedColorPanelProps } />, | ||
InspectorControlsColorPanel: ( | ||
<InspectorControlsColorPanel { ...wrappedColorPanelProps } /> | ||
), | ||
}; | ||
}, [ attributes, setAttributes, ...deps ] ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should color config be a dependency here? colorConfig may totally change the expected output of the component. I guess we did not added it as a dependency because if users pass an array inline on each re-render a new reference is passed, could we automatically generate the dependencies from the config e.g: generate an array of [ name, attribute, name, attribute ...], If we also pass all properties of panel setting props we could avoid the need for dependencies. Currently, we are relying on devs passing the dependencies and I think it will be something developers will easily miss. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ export { default as AlignmentToolbar } from './alignment-toolbar'; | |
export { default as Autocomplete } from './autocomplete'; | ||
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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add this export also to the native version of the file packages/block-editor/src/components/index.native.js? Otherwise, headings on mobile will break. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, good catch! |
||
export { default as BlockFormatControls } from './block-format-controls'; | ||
export { default as BlockIcon } from './block-icon'; | ||
export { default as BlockNavigationDropdown } from './block-navigation/dropdown'; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need the changes in this file, since you can access
clientId
from the context, you can callupdateBlockAttributes
andgetBlockAttributes
without extra context values.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That wouldn't subscribe the consumer to changes though, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useSelect
wouldThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right. I was thinking of the Block API functions. I'll change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It just feels a lot clunkier to have to use the context to get the
id
and then pass it touseSelect
.Don't you think a
useBlockEditContext
with attributes and the setter will be very useful for people building custom hooks that need access to attributes?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so personally, I feel like the context should contain the minimum possible things because
if we add these, the question becomes: Will we add a new value there each time we need another selector/action?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we could, because it would be limited to the props that
BlockEdit
receives. I just tend to favor context over selector/subscription boilerplate, but I can see why this aligns more with the rest of the codebase and see the value in having a single way of doing things:69865dd