diff --git a/packages/block-editor/src/components/block-list/content.scss b/packages/block-editor/src/components/block-list/content.scss index a163ddaa789551..52f26217730773 100644 --- a/packages/block-editor/src/components/block-list/content.scss +++ b/packages/block-editor/src/components/block-list/content.scss @@ -85,7 +85,6 @@ .block-editor-block-list__block.is-highlighted, .block-editor-block-list__block.is-highlighted ~ .is-multi-selected, &.is-navigate-mode .block-editor-block-list__block.is-selected, - & .is-block-moving-mode.block-editor-block-list__block.has-child-selected, .block-editor-block-list__block:not([contenteditable]):focus { outline: none; @@ -113,8 +112,6 @@ // Moving blocks using keyboard (Ellipsis > Move). & .is-block-moving-mode.block-editor-block-list__block.is-selected { - box-shadow: none; - outline: none; &::after { content: ""; @@ -130,6 +127,8 @@ top: -$default-block-margin * 0.5; border-radius: $radius-block-ui; border-top: 4px solid $gray-400; + bottom: auto; + box-shadow: none; } } diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index 0b49c9299212f9..eef1678e2dfab3 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -252,6 +252,16 @@ flex-shrink: 1; } + @include break-medium() { + .block-editor-block-contextual-toolbar.is-fixed { + .components-toolbar, + .components-toolbar-group { + flex-shrink: 0; + } + } + } + + .block-editor-rich-text__inline-format-toolbar-group { .components-button + .components-button { margin-left: 6px; diff --git a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js index 7dcd88d1cfd10f..1bb4101c8053f8 100644 --- a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js +++ b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js @@ -7,7 +7,12 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useEffect, useRef, useState } from '@wordpress/element'; +import { + useLayoutEffect, + useEffect, + useRef, + useState, +} from '@wordpress/element'; import { hasBlockSupport, store as blocksStore } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; import { @@ -78,6 +83,77 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { setIsCollapsed( false ); }, [ selectedBlockClientId ] ); + const isLargerThanTabletViewport = useViewportMatch( 'large', '>=' ); + const isFullscreen = + document.body.classList.contains( 'is-fullscreen-mode' ); + + useLayoutEffect( () => { + // don't do anything if not fixed toolbar + if ( ! isFixed || ! blockType ) { + return; + } + + const blockToolbar = document.querySelector( + '.block-editor-block-contextual-toolbar' + ); + + if ( ! blockToolbar ) { + return; + } + + if ( ! isLargerThanTabletViewport ) { + // set the width of the toolbar to auto + blockToolbar.style = {}; + return; + } + + if ( isCollapsed ) { + // set the width of the toolbar to auto + blockToolbar.style.width = 'auto'; + return; + } + + // get the width of the pinned items in the post editor + const pinnedItems = document.querySelector( + '.edit-post-header__settings' + ); + + // get the width of the left header in the site editor + const leftHeader = document.querySelector( + '.edit-site-header-edit-mode__end' + ); + + const computedToolbarStyle = window.getComputedStyle( blockToolbar ); + const computedPinnedItemsStyle = pinnedItems + ? window.getComputedStyle( pinnedItems ) + : false; + const computedLeftHeaderStyle = leftHeader + ? window.getComputedStyle( leftHeader ) + : false; + + const marginLeft = parseFloat( computedToolbarStyle.marginLeft ); + const pinnedItemsWidth = computedPinnedItemsStyle + ? parseFloat( computedPinnedItemsStyle.width ) + 10 // 10 is the pinned items padding + : 0; + const leftHeaderWidth = computedLeftHeaderStyle + ? parseFloat( computedLeftHeaderStyle.width ) + : 0; + + // set the new witdth of the toolbar + blockToolbar.style.width = `calc(100% - ${ + leftHeaderWidth + + pinnedItemsWidth + + marginLeft + + ( isFullscreen ? 0 : 160 ) // the width of the admin sidebar expanded + }px)`; + }, [ + isFixed, + isLargerThanTabletViewport, + isCollapsed, + isFullscreen, + blockType, + ] ); + if ( isContentOnly || ( blockType && diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index 1f08727bbfb9b1..9be39830f3f86d 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -110,6 +110,12 @@ z-index: z-index(".block-editor-block-popover"); display: block; width: 100%; + overflow: hidden; + + .block-editor-block-toolbar { + overflow: auto; + overflow-y: hidden; + } border: none; border-bottom: $border-width solid $gray-200; @@ -137,14 +143,14 @@ // on desktop and tablet viewports the toolbar is fixed // on top of interface header + $toolbar-margin: $grid-unit-80 * 3 - 2 * $grid-unit + $grid-unit-05; @include break-medium() { &.is-fixed { - // leave room for block inserter, undo and redo, list view - margin-left: $grid-unit-80 * 3 - 2 * $grid-unit + $grid-unit-05; + margin-left: $toolbar-margin; // position on top of interface header position: fixed; - top: $admin-bar-height + $grid-unit - $border-width; + top: $admin-bar-height; // Don't fill up when empty min-height: initial; // remove the border @@ -152,18 +158,32 @@ // has to be flex for collapse button to fit display: flex; + // Mimic the height of the parent, vertically align center, and provide a max-height. + height: $header-height; + align-items: center; + &.is-collapsed { width: initial; } + &:empty { + width: initial; + } + .is-fullscreen-mode & { // leave room for block inserter, undo and redo, list view // and some margin left margin-left: $grid-unit-80 * 4 - 2 * $grid-unit; - top: $grid-unit - $border-width; + + top: 0; + &.is-collapsed { width: initial; } + + &:empty { + width: initial; + } } & > .block-editor-block-toolbar.is-showing-movers { @@ -249,7 +269,7 @@ .show-icon-labels & { - margin-left: $grid-unit-80 + 2 * $grid-unit; // inserter and margin ; + margin-left: $grid-unit-80 + 2 * $grid-unit; // inserter and margin .is-fullscreen-mode & { margin-left: $grid-unit * 18; // site hub, inserter and margin @@ -322,7 +342,12 @@ // except for the inserter on the left @include break-medium() { &.is-fixed { - width: 100%; + width: calc(100% - #{$toolbar-margin}); + + .show-icon-labels & { + width: calc(100% + 40px - #{$toolbar-margin}); //there are no undo, redo and list view buttons + } + } } @@ -332,6 +357,9 @@ @include break-large() { &.is-fixed { width: auto; + .show-icon-labels & { + width: auto; //there are no undo, redo and list view buttons + } } .is-fullscreen-mode &.is-fixed { // in full screen mode we need to account for diff --git a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js index 9b4730a7bb1fc4..ac4c04f1d53f43 100644 --- a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js +++ b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js @@ -16,7 +16,7 @@ import { import { BaseControl, Button } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { - __EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY, + __EXPERIMENTAL_STYLE_PROPERTY, getBlockType, hasBlockSupport, } from '@wordpress/blocks'; @@ -30,14 +30,23 @@ import { store as noticesStore } from '@wordpress/notices'; import { useSupportedStyles } from '../../components/global-styles/hooks'; import { unlock } from '../../lock-unlock'; -const { GlobalStylesContext, useBlockEditingMode } = unlock( +const { cleanEmptyObject, GlobalStylesContext, useBlockEditingMode } = unlock( blockEditorPrivateApis ); +// Block Gap is a special case and isn't defined within the blocks +// style properties config. We'll add it here to allow it to be pushed +// to global styles as well. +const STYLE_PROPERTY = { + ...__EXPERIMENTAL_STYLE_PROPERTY, + blockGap: { value: [ 'spacing', 'blockGap' ] }, +}; + // TODO: Temporary duplication of constant in @wordpress/block-editor. Can be // removed by moving PushChangesToGlobalStylesControl to // @wordpress/block-editor. const STYLE_PATH_TO_CSS_VAR_INFIX = { + 'border.color': 'color', 'color.background': 'color', 'color.text': 'color', 'elements.link.color.text': 'color', @@ -79,6 +88,7 @@ const STYLE_PATH_TO_CSS_VAR_INFIX = { 'elements.h6.typography.fontFamily': 'font-family', 'elements.h6.color.gradient': 'gradient', 'color.gradient': 'gradient', + blockGap: 'spacing', 'typography.fontSize': 'font-size', 'typography.fontFamily': 'font-family', }; @@ -87,6 +97,7 @@ const STYLE_PATH_TO_CSS_VAR_INFIX = { // removed by moving PushChangesToGlobalStylesControl to // @wordpress/block-editor. const STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE = { + 'border.color': 'borderColor', 'color.background': 'backgroundColor', 'color.text': 'textColor', 'color.gradient': 'gradient', @@ -96,30 +107,124 @@ const STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE = { const SUPPORTED_STYLES = [ 'border', 'color', 'spacing', 'typography' ]; -function useChangesToPush( name, attributes ) { +const flatBorderProperties = [ 'borderColor', 'borderWidth', 'borderStyle' ]; +const sides = [ 'top', 'right', 'bottom', 'left' ]; + +function getBorderStyleChanges( border, presetColor, userStyle ) { + if ( ! border && ! presetColor ) { + return []; + } + + const changes = [ + ...getFallbackBorderStyleChange( 'top', border, userStyle ), + ...getFallbackBorderStyleChange( 'right', border, userStyle ), + ...getFallbackBorderStyleChange( 'bottom', border, userStyle ), + ...getFallbackBorderStyleChange( 'left', border, userStyle ), + ]; + + // Handle a flat border i.e. all sides the same, CSS shorthand. + const { color: customColor, style, width } = border || {}; + const hasColorOrWidth = presetColor || customColor || width; + + if ( hasColorOrWidth && ! style ) { + // Global Styles need individual side configurations to overcome + // theme.json configurations which are per side as well. + sides.forEach( ( side ) => { + // Only add fallback border-style if global styles don't already + // have something set. + if ( ! userStyle?.[ side ]?.style ) { + changes.push( { + path: [ 'border', side, 'style' ], + value: 'solid', + } ); + } + } ); + } + + return changes; +} + +function getFallbackBorderStyleChange( side, border, globalBorderStyle ) { + if ( ! border?.[ side ] || globalBorderStyle?.[ side ]?.style ) { + return []; + } + + const { color, style, width } = border[ side ]; + const hasColorOrWidth = color || width; + + if ( ! hasColorOrWidth || style ) { + return []; + } + + return [ { path: [ 'border', side, 'style' ], value: 'solid' } ]; +} + +function useChangesToPush( name, attributes, userConfig ) { const supports = useSupportedStyles( name ); + const blockUserConfig = userConfig?.styles?.blocks?.[ name ]; + + return useMemo( () => { + const changes = supports.flatMap( ( key ) => { + if ( ! STYLE_PROPERTY[ key ] ) { + return []; + } + const { value: path } = STYLE_PROPERTY[ key ]; + const presetAttributeKey = path.join( '.' ); + const presetAttributeValue = + attributes[ + STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE[ presetAttributeKey ] + ]; + const value = presetAttributeValue + ? `var:preset|${ STYLE_PATH_TO_CSS_VAR_INFIX[ presetAttributeKey ] }|${ presetAttributeValue }` + : get( attributes.style, path ); - return useMemo( - () => - supports.flatMap( ( key ) => { - if ( ! STYLE_PROPERTY[ key ] ) { - return []; + // Links only have a single support entry but have two element + // style properties, color and hover color. The following check + // will add the hover color to the changes if required. + if ( key === 'linkColor' ) { + const linkChanges = value ? [ { path, value } ] : []; + const hoverPath = [ + 'elements', + 'link', + ':hover', + 'color', + 'text', + ]; + const hoverValue = get( attributes.style, hoverPath ); + + if ( hoverValue ) { + linkChanges.push( { path: hoverPath, value: hoverValue } ); } - const { value: path } = STYLE_PROPERTY[ key ]; - const presetAttributeKey = path.join( '.' ); - const presetAttributeValue = - attributes[ - STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE[ - presetAttributeKey - ] - ]; - const value = presetAttributeValue - ? `var:preset|${ STYLE_PATH_TO_CSS_VAR_INFIX[ presetAttributeKey ] }|${ presetAttributeValue }` - : get( attributes.style, path ); - return value ? [ { path, value } ] : []; - } ), - [ supports, name, attributes ] - ); + + return linkChanges; + } + + // The shorthand border styles can't be mapped directly as global + // styles requires longhand config. + if ( flatBorderProperties.includes( key ) && value ) { + // The shorthand config path is included to clear the block attribute. + const borderChanges = [ { path, value } ]; + sides.forEach( ( side ) => { + const currentPath = [ ...path ]; + currentPath.splice( -1, 0, side ); + borderChanges.push( { path: currentPath, value } ); + } ); + return borderChanges; + } + + return value ? [ { path, value } ] : []; + } ); + + // To ensure display of a visible border, global styles require a + // default border style if a border color or width is present. + getBorderStyleChanges( + attributes.style?.border, + attributes.borderColor, + blockUserConfig?.border + ).forEach( ( change ) => changes.push( change ) ); + + return changes; + }, [ supports, attributes, blockUserConfig ] ); } function cloneDeep( object ) { @@ -131,11 +236,11 @@ function PushChangesToGlobalStylesControl( { attributes, setAttributes, } ) { - const changes = useChangesToPush( name, attributes ); - const { user: userConfig, setUserConfig } = useContext( GlobalStylesContext ); + const changes = useChangesToPush( name, attributes, userConfig ); + const { __unstableMarkNextChangeAsNotPersistent } = useDispatch( blockEditorStore ); const { createSuccessNotice } = useDispatch( noticesStore ); @@ -155,12 +260,22 @@ function PushChangesToGlobalStylesControl( { set( newUserConfig, [ 'styles', 'blocks', name, ...path ], value ); } + const newBlockAttributes = { + borderColor: undefined, + backgroundColor: undefined, + textColor: undefined, + gradient: undefined, + fontSize: undefined, + fontFamily: undefined, + style: cleanEmptyObject( newBlockStyles ), + }; + // @wordpress/core-data doesn't support editing multiple entity types in // a single undo level. So for now, we disable @wordpress/core-data undo // tracking and implement our own Undo button in the snackbar // notification. __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { style: newBlockStyles } ); + setAttributes( newBlockAttributes ); setUserConfig( () => newUserConfig, { undoIgnore: true } ); createSuccessNotice( @@ -176,7 +291,7 @@ function PushChangesToGlobalStylesControl( { label: __( 'Undo' ), onClick() { __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { style: blockStyles } ); + setAttributes( attributes ); setUserConfig( () => userConfig, { undoIgnore: true, } ); @@ -185,7 +300,16 @@ function PushChangesToGlobalStylesControl( { ], } ); - }, [ changes, attributes, userConfig, name ] ); + }, [ + __unstableMarkNextChangeAsNotPersistent, + attributes, + changes, + createSuccessNotice, + name, + setAttributes, + setUserConfig, + userConfig, + ] ); return (