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 df21d82dda5e2..87ef64deccc6f 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 c360ed4e87c6c..e4482693699ba 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(); @@ -61,25 +59,23 @@ 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 c911bc43670b6..fcbd79e92596b 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. @@ -370,6 +489,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, }, ] : [] ), @@ -405,6 +533,13 @@ export function ColorEdit( props ) { onGradientChange: hasGradientColor ? onChangeGradient : undefined, + isShownByDefault: + defaultColorControls?.background, + hasValue: () => + hasColor( 'background' )( props ), + onDeselect: () => + resetBackgroundAndGradient( props ), + resetAllFilter: clearBackgroundAndGradient, }, ] : [] ), @@ -419,6 +554,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-test-utils/README.md b/packages/e2e-test-utils/README.md index 66ab7b10da8d1..180dbbdf2defb 100644 --- a/packages/e2e-test-utils/README.md +++ b/packages/e2e-test-utils/README.md @@ -581,6 +581,10 @@ Navigates the site editor back Goes back until it gets to the root +### openColorToolsPanelMenu + +Opens the Color tools panel menu provided via block supports. + ### openDocumentSettingsSidebar Clicks on the button in the header which opens Document Settings sidebar when it is closed. diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js index c10e761a5d58b..493bac3595507 100644 --- a/packages/e2e-test-utils/src/index.js +++ b/packages/e2e-test-utils/src/index.js @@ -64,6 +64,7 @@ export { } from './observe-focus-loss'; export { openDocumentSettingsSidebar } from './open-document-settings-sidebar'; export { openPublishPanel } from './open-publish-panel'; +export { openColorToolsPanelMenu } from './open-color-tools-panel-menu'; export { openTypographyToolsPanelMenu } from './open-typography-tools-panel-menu'; export { trashAllPosts } from './posts'; export { pressKeyTimes } from './press-key-times'; diff --git a/packages/e2e-test-utils/src/open-color-tools-panel-menu.js b/packages/e2e-test-utils/src/open-color-tools-panel-menu.js new file mode 100644 index 0000000000000..0b358c7560b0e --- /dev/null +++ b/packages/e2e-test-utils/src/open-color-tools-panel-menu.js @@ -0,0 +1,9 @@ +/** + * Opens the Color tools panel menu provided via block supports. + */ +export async function openColorToolsPanelMenu() { + 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(); +} diff --git a/packages/e2e-tests/specs/editor/blocks/heading.test.js b/packages/e2e-tests/specs/editor/blocks/heading.test.js index 14a06dfb8c505..d344d5cf4223b 100644 --- a/packages/e2e-tests/specs/editor/blocks/heading.test.js +++ b/packages/e2e-tests/specs/editor/blocks/heading.test.js @@ -5,19 +5,18 @@ import { clickBlockAppender, createNewPost, getEditedPostContent, + openColorToolsPanelMenu, pressKeyWithModifier, } from '@wordpress/e2e-test-utils'; describe( 'Heading', () => { const COLOR_ITEM_SELECTOR = - '.block-editor-panel-color-gradient-settings__item'; + '.block-editor-panel-color-gradient-settings__dropdown'; const CUSTOM_COLOR_BUTTON_X_SELECTOR = `.components-color-palette__custom-color`; const CUSTOM_COLOR_DETAILS_BUTTON_SELECTOR = '.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 +72,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 +98,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 diff --git a/packages/e2e-tests/specs/editor/various/keep-styles-on-block-transforms.test.js b/packages/e2e-tests/specs/editor/various/keep-styles-on-block-transforms.test.js index b79c71167318b..ae2e8f696ccec 100644 --- a/packages/e2e-tests/specs/editor/various/keep-styles-on-block-transforms.test.js +++ b/packages/e2e-tests/specs/editor/various/keep-styles-on-block-transforms.test.js @@ -4,9 +4,10 @@ import { clickBlockAppender, createNewPost, + getEditedPostContent, + openColorToolsPanelMenu, pressKeyWithModifier, transformBlockTo, - getEditedPostContent, } from '@wordpress/e2e-test-utils'; describe( 'Keep styles on block transforms', () => { @@ -17,13 +18,11 @@ describe( 'Keep styles on block transforms', () => { it( 'Should keep colors during a transform', async () => { await clickBlockAppender(); await page.keyboard.type( '## Heading' ); - const [ colorPanelToggle ] = await page.$x( - "//button[./span[contains(text(),'Color')]]" - ); - await colorPanelToggle.click(); + await openColorToolsPanelMenu(); + await page.click( 'button[aria-label="Show Text"]' ); const textColorButton = await page.waitForSelector( - '.block-editor-panel-color-gradient-settings__item' + '.block-editor-panel-color-gradient-settings__dropdown' ); await textColorButton.click();