diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index c2a1b313fcd76..e3061f0004af4 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -67,6 +67,10 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => {
+ ) } + + ( + + ) } + renderContent={ () => ( + + ) } + /> + + ); +} diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 800e86a493593..37c309757ed19 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -51,6 +51,7 @@ export { default as __experimentalTextTransformControl } from './text-transform- export { default as __experimentalColorGradientControl } from './colors-gradients/control'; export { default as __experimentalColorGradientSettingsDropdown } from './colors-gradients/dropdown'; export { default as __experimentalPanelColorGradientSettings } from './colors-gradients/panel-color-gradient-settings'; +export { default as __experimentalToolsPanelColorDropdown } from './colors-gradients/tools-panel-color-dropdown'; export { default as __experimentalImageEditor, ImageEditingProvider as __experimentalImageEditingProvider, diff --git a/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js b/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js index 354ee0c14e496..d582548b5d64e 100644 --- a/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js +++ b/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js @@ -70,6 +70,8 @@ export default function BlockSupportToolsPanel( { children, group, label } ) { panelId={ panelId } hasInnerWrapper={ true } shouldRenderPlaceholderItems={ true } // Required to maintain fills ordering. + __experimentalFirstVisibleItemClass="first" + __experimentalLastVisibleItemClass="last" > { children } diff --git a/packages/block-editor/src/components/inspector-controls/groups.js b/packages/block-editor/src/components/inspector-controls/groups.js index 61de91ef1e9e4..0b7d1d2f4479f 100644 --- a/packages/block-editor/src/components/inspector-controls/groups.js +++ b/packages/block-editor/src/components/inspector-controls/groups.js @@ -6,6 +6,7 @@ import { createSlotFill } from '@wordpress/components'; const InspectorControlsDefault = createSlotFill( 'InspectorControls' ); const InspectorControlsAdvanced = createSlotFill( 'InspectorAdvancedControls' ); const InspectorControlsBorder = createSlotFill( 'InspectorControlsBorder' ); +const InspectorControlsColor = createSlotFill( 'InspectorControlsColor' ); const InspectorControlsDimensions = createSlotFill( 'InspectorControlsDimensions' ); @@ -17,6 +18,7 @@ const groups = { default: InspectorControlsDefault, advanced: InspectorControlsAdvanced, border: InspectorControlsBorder, + color: InspectorControlsColor, dimensions: InspectorControlsDimensions, typography: InspectorControlsTypography, }; diff --git a/packages/block-editor/src/hooks/border-color.js b/packages/block-editor/src/hooks/border-color.js index 07b34fa6ba795..e26af52e364f5 100644 --- a/packages/block-editor/src/hooks/border-color.js +++ b/packages/block-editor/src/hooks/border-color.js @@ -68,7 +68,7 @@ export function BorderColorEdit( props ) { // Detect changes in the color attributes and update the colorValue to keep the // UI in sync. This is necessary for situations when border controls interact with - // eachother: eg, setting the border width to zero causes the color and style + // each other: eg, setting the border width to zero causes the color and style // selections to be cleared. useEffect( () => { setColorValue( diff --git a/packages/block-editor/src/hooks/color-panel.js b/packages/block-editor/src/hooks/color-panel.js index 72ec01fd326ce..deffc5192309a 100644 --- a/packages/block-editor/src/hooks/color-panel.js +++ b/packages/block-editor/src/hooks/color-panel.js @@ -2,13 +2,12 @@ * WordPress dependencies */ import { useState, useEffect } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import PanelColorGradientSettings from '../components/colors-gradients/panel-color-gradient-settings'; import ContrastChecker from '../components/contrast-checker'; +import ToolsPanelColorDropdown from '../components/colors-gradients/tools-panel-color-dropdown'; import InspectorControls from '../components/inspector-controls'; import { __unstableUseBlockRef as useBlockRef } from '../components/block-list/use-block-props/use-block-refs'; @@ -21,7 +20,6 @@ export default function ColorPanel( { settings, clientId, enableContrastChecking = true, - showTitle = true, } ) { const [ detectedBackgroundColor, setDetectedBackgroundColor ] = useState(); const [ detectedColor, setDetectedColor ] = useState(); @@ -55,24 +53,22 @@ export default function ColorPanel( { } ); return ( - - - { enableContrastChecking && ( - - ) } - + + { settings.map( ( setting, index ) => ( + + ) ) } + { enableContrastChecking && ( + + ) } ); } diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index 5d92c321daf1a..573f2620a654e 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -77,6 +77,125 @@ const hasTextColorSupport = ( blockType ) => { return colorSupport && colorSupport.text !== false; }; +/** + * Checks whether a color has been set either with a named preset color in + * a top level block attribute or as a custom value within the style attribute + * object. + * + * @param {string} name Name of the color to check. + * @return {boolean} Whether or not a color has a value. + */ +const hasColor = ( name ) => ( props ) => { + if ( name === 'background' ) { + return ( + !! props.attributes.backgroundColor || + !! props.attributes.style?.color?.background || + !! props.attributes.gradient || + !! props.attributes.style?.color?.gradient + ); + } + + if ( name === 'link' ) { + return !! props.attributes.style?.elements?.link?.color?.text; + } + + return ( + !! props.attributes[ `${ name }Color` ] || + !! props.attributes.style?.color?.[ name ] + ); +}; + +/** + * Clears a single color property from a style object. + * + * @param {Array} path Path to color property to clear within styles object. + * @param {Object} style Block attributes style object. + * @return {Object} Styles with the color property omitted. + */ +const clearColorFromStyles = ( path, style ) => + cleanEmptyObject( immutableSet( style, path, undefined ) ); + +/** + * Resets the block attributes for text color. + * + * @param {Object} props Current block props. + * @param {Object} props.attributes Block attributes. + * @param {Function} props.setAttributes Block's setAttributes prop used to apply reset. + */ +const resetTextColor = ( { attributes, setAttributes } ) => { + setAttributes( { + textColor: undefined, + style: clearColorFromStyles( [ 'color', 'text' ], attributes.style ), + } ); +}; + +/** + * Clears text color related properties from supplied attributes. + * + * @param {Object} attributes Block attributes. + * @return {Object} Update block attributes with text color properties omitted. + */ +const resetAllTextFilter = ( attributes ) => ( { + textColor: undefined, + style: clearColorFromStyles( [ 'color', 'text' ], attributes.style ), +} ); + +/** + * Resets the block attributes for link color. + * + * @param {Object} props Current block props. + * @param {Object} props.attributes Block attributes. + * @param {Function} props.setAttributes Block's setAttributes prop used to apply reset. + */ +const resetLinkColor = ( { attributes, setAttributes } ) => { + const path = [ 'elements', 'link', 'color', 'text' ]; + setAttributes( { style: clearColorFromStyles( path, attributes.style ) } ); +}; + +/** + * Clears link color related properties from supplied attributes. + * + * @param {Object} attributes Block attributes. + * @return {Object} Update block attributes with link color properties omitted. + */ +const resetAllLinkFilter = ( attributes ) => ( { + style: clearColorFromStyles( + [ 'elements', 'link', 'color', 'text' ], + attributes.style + ), +} ); + +/** + * Clears all background color related properties including gradients from + * supplied block attributes. + * + * @param {Object} attributes Block attributes. + * @return {Object} Block attributes with background and gradient omitted. + */ +const clearBackgroundAndGradient = ( attributes ) => ( { + backgroundColor: undefined, + gradient: undefined, + style: { + ...attributes.style, + color: { + ...attributes.style?.color, + background: undefined, + gradient: undefined, + }, + }, +} ); + +/** + * Resets the block attributes for both background color and gradient. + * + * @param {Object} props Current block props. + * @param {Object} props.attributes Block attributes. + * @param {Function} props.setAttributes Block's setAttributes prop used to apply reset. + */ +const resetBackgroundAndGradient = ( { attributes, setAttributes } ) => { + setAttributes( clearBackgroundAndGradient( attributes ) ); +}; + /** * Filters registered block settings, extending attributes to include * `backgroundColor` and `textColor` attribute. @@ -136,7 +255,7 @@ export function addSaveProps( props, blockType, attributes ) { const hasGradient = hasGradientSupport( blockType ); - // I'd have prefered to avoid the "style" attribute usage here + // I'd have preferred to avoid the "style" attribute usage here const { backgroundColor, textColor, gradient, style } = attributes; const backgroundClass = getColorClassName( @@ -168,7 +287,7 @@ export function addSaveProps( props, blockType, attributes ) { } /** - * Filters registered block settings to extand the block edit wrapper + * Filters registered block settings to extend the block edit wrapper * to apply the desired styles and classnames properly. * * @param {Object} settings Original block settings. @@ -374,6 +493,11 @@ export function ColorEdit( props ) { const enableContrastChecking = Platform.OS === 'web' && ! gradient && ! style?.color?.gradient; + const defaultColorControls = getBlockSupport( props.name, [ + COLOR_SUPPORT_KEY, + '__experimentalDefaultControls', + ] ); + return ( hasColor( 'text' )( props ), + onDeselect: () => resetTextColor( props ), + resetAllFilter: resetAllTextFilter, }, ] : [] ), @@ -409,6 +537,13 @@ export function ColorEdit( props ) { onGradientChange: hasGradientColor ? onChangeGradient : undefined, + isShownByDefault: + defaultColorControls?.background, + hasValue: () => + hasColor( 'background' )( props ), + onDeselect: () => + resetBackgroundAndGradient( props ), + resetAllFilter: clearBackgroundAndGradient, }, ] : [] ), @@ -423,6 +558,10 @@ export function ColorEdit( props ) { ), clearable: !! style?.elements?.link?.color ?.text, + isShownByDefault: defaultColorControls?.link, + hasValue: () => hasColor( 'link' )( props ), + onDeselect: () => resetLinkColor( props ), + resetAllFilter: resetAllLinkFilter, }, ] : [] ), diff --git a/packages/block-editor/src/hooks/color.scss b/packages/block-editor/src/hooks/color.scss new file mode 100644 index 0000000000000..42e6ff928d3dc --- /dev/null +++ b/packages/block-editor/src/hooks/color.scss @@ -0,0 +1,85 @@ +.color-block-support-panel { + .block-editor-contrast-checker { + /** + * Contrast checkers are forced to the bottom of the panel so all + * injected color controls can appear as a single item group without + * the contrast checkers suddenly appearing between items. + */ + order: 9999; + grid-column: span 2; + margin-top: $grid-unit-20; + + .components-notice__content { + margin-right: 0; + } + } + + /* Increased specificity required to remove the slot wrapper's row gap */ + &#{&} { + .color-block-support-panel__inner-wrapper { + row-gap: 0; + } + } + + /** + * The following styles replicate the separated border of the + * `ItemGroup` component but allows for hidden items. This is because + * to maintain the order of `ToolsPanel` controls, each `ToolsPanelItem` + * must at least render a placeholder which would otherwise interfere + * with the `:last-child` styles. + */ + .block-editor-tools-panel-color-gradient-settings__item { + padding: 0; + + // Border styles. + border-left: 1px solid rgba(0, 0, 0, 0.1); + border-right: 1px solid rgba(0, 0, 0, 0.1); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + + &.first { + border-top-left-radius: 2px; + border-top-right-radius: 2px; + border-top: 1px solid rgba(0, 0, 0, 0.1); + } + + &.last { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + } + + > div, + > div > button { + border-radius: inherit; + } + } + + .block-editor-panel-color-gradient-settings__color-indicator { + // Show a diagonal line (crossed out) for empty swatches. + background: linear-gradient(-45deg, transparent 48%, $gray-300 48%, $gray-300 52%, transparent 52%); + } + + /** + * The following few styles fix the layout and spacing for the due to the + * introduced wrapper element by the `Item` component. + */ + .block-editor-tools-panel-color-dropdown { + display: block; + padding: 0; + + > button { + height: 46px; + + &.is-open { + background: $gray-100; + color: var(--wp-admin-theme-color); + } + } + } + + .color-block-support-panel__item-group { + > div { + grid-column: span 2; + border-radius: inherit; + } + } +} diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 6f20a3b1b2901..fae1c7053c8e6 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -57,6 +57,7 @@ @import "./hooks/border.scss"; @import "./hooks/dimensions.scss"; @import "./hooks/typography.scss"; +@import "./hooks/color.scss"; @import "./components/block-toolbar/style.scss"; @import "./components/inserter/style.scss"; diff --git a/packages/e2e-tests/specs/editor/blocks/heading.test.js b/packages/e2e-tests/specs/editor/blocks/heading.test.js index 14a06dfb8c505..385b90ac1839a 100644 --- a/packages/e2e-tests/specs/editor/blocks/heading.test.js +++ b/packages/e2e-tests/specs/editor/blocks/heading.test.js @@ -8,6 +8,13 @@ import { pressKeyWithModifier, } from '@wordpress/e2e-test-utils'; +const openColorToolsPanelMenu = async () => { + const toggleSelector = + "//div[contains(@class, 'color-block-support-panel')]//button[contains(@class, 'components-dropdown-menu__toggle')]"; + const toggle = await page.waitForXPath( toggleSelector ); + return toggle.click(); +}; + describe( 'Heading', () => { const COLOR_ITEM_SELECTOR = '.block-editor-panel-color-gradient-settings__item'; @@ -16,8 +23,6 @@ describe( 'Heading', () => { '.components-color-picker button[aria-label="Show detailed inputs"]'; const COLOR_INPUT_FIELD_SELECTOR = '.components-color-picker .components-input-control__input'; - const COLOR_PANEL_TOGGLE_X_SELECTOR = - "//button[./span[contains(text(),'Color')]]"; beforeEach( async () => { await createNewPost(); @@ -73,10 +78,8 @@ describe( 'Heading', () => { it( 'should correctly apply custom colors', async () => { await clickBlockAppender(); await page.keyboard.type( '### Heading' ); - const colorPanelToggle = await page.waitForXPath( - COLOR_PANEL_TOGGLE_X_SELECTOR - ); - await colorPanelToggle.click(); + await openColorToolsPanelMenu(); + await page.click( 'button[aria-label="Show Text"]' ); const textColorButton = await page.waitForSelector( COLOR_ITEM_SELECTOR @@ -101,10 +104,8 @@ describe( 'Heading', () => { it( 'should correctly apply named colors', async () => { await clickBlockAppender(); await page.keyboard.type( '## Heading' ); - const [ colorPanelToggle ] = await page.$x( - COLOR_PANEL_TOGGLE_X_SELECTOR - ); - await colorPanelToggle.click(); + await openColorToolsPanelMenu(); + await page.click( 'button[aria-label="Show Text"]' ); const textColorButton = await page.waitForSelector( COLOR_ITEM_SELECTOR