diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 0ad663064fe9ff..bf55d08c1d4b33 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -286,26 +286,31 @@ class WP_Theme_JSON_Gutenberg { * * Indirect properties are not output directly by `compute_style_properties`, * but are used elsewhere in the processing of global styles. The indirect - * property is used to validate whether or not a style value is allowed. + * property is used to validate whether a style value is allowed. * * @since 6.2.0 + * @since 6.6.0 Added background-image properties. * * @var array */ const INDIRECT_PROPERTIES_METADATA = array( - 'gap' => array( + 'gap' => array( array( 'spacing', 'blockGap' ), ), - 'column-gap' => array( + 'column-gap' => array( array( 'spacing', 'blockGap', 'left' ), ), - 'row-gap' => array( + 'row-gap' => array( array( 'spacing', 'blockGap', 'top' ), ), - 'max-width' => array( + 'max-width' => array( array( 'layout', 'contentSize' ), array( 'layout', 'wideSize' ), ), + 'background-image' => array( + array( 'background', 'backgroundImage', 'url' ), + array( 'background', 'backgroundImage', 'source' ), + ), ); /** @@ -1359,7 +1364,7 @@ public function get_block_custom_css_nodes() { * * @since 6.6.0 * - * @param array $css The block css node. + * @param array $css The block css node. * @param string $selector The block selector. * * @return string The global styles custom CSS for the block. diff --git a/packages/block-editor/src/components/global-styles/background-panel.js b/packages/block-editor/src/components/global-styles/background-panel.js new file mode 100644 index 00000000000000..1288ff823b46cd --- /dev/null +++ b/packages/block-editor/src/components/global-styles/background-panel.js @@ -0,0 +1,591 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, + ToggleControl, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, + __experimentalUnitControl as UnitControl, + __experimentalVStack as VStack, + DropZone, + FlexItem, + FocalPointPicker, + MenuItem, + VisuallyHidden, + __experimentalItemGroup as ItemGroup, + __experimentalHStack as HStack, + __experimentalTruncate as Truncate, +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { getFilename } from '@wordpress/url'; +import { useCallback, Platform, useRef } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { focus } from '@wordpress/dom'; +import { isBlobURL } from '@wordpress/blob'; + +/** + * Internal dependencies + */ +import { TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; +import { setImmutably } from '../../utils/object'; +import MediaReplaceFlow from '../media-replace-flow'; +import { store as blockEditorStore } from '../../store'; + +const IMAGE_BACKGROUND_TYPE = 'image'; + +/** + * Checks site settings to see if the background panel may be used. + * `settings.background.backgroundSize` exists also, + * but can only be used if settings?.background?.backgroundImage is `true`. + * + * @param {Object} settings Site settings + * @return {boolean} Whether site settings has activated background panel. + */ +export function useHasBackgroundPanel( settings ) { + return Platform.OS === 'web' && settings?.background?.backgroundImage; +} + +/** + * Checks if there is a current value in the background size block support + * attributes. Background size values include background size as well + * as background position. + * + * @param {Object} style Style attribute. + * @return {boolean} Whether the block has a background size value set. + */ +export function hasBackgroundSizeValue( style ) { + return ( + style?.background?.backgroundPosition !== undefined || + style?.background?.backgroundSize !== undefined + ); +} + +/** + * Checks if there is a current value in the background image block support + * attributes. + * + * @param {Object} style Style attribute. + * @return {boolean} Whether the block has a background image value set. + */ +export function hasBackgroundImageValue( style ) { + return ( + !! style?.background?.backgroundImage?.id || + !! style?.background?.backgroundImage?.url + ); +} + +/** + * Get the help text for the background size control. + * + * @param {string} value backgroundSize value. + * @return {string} Translated help text. + */ +function backgroundSizeHelpText( value ) { + if ( value === 'cover' || value === undefined ) { + return __( 'Image covers the space evenly.' ); + } + if ( value === 'contain' ) { + return __( 'Image is contained without distortion.' ); + } + return __( 'Specify a fixed width.' ); +} + +/** + * Converts decimal x and y coords from FocalPointPicker to percentage-based values + * to use as backgroundPosition value. + * + * @param {{x?:number, y?:number}} value FocalPointPicker coords. + * @return {string} backgroundPosition value. + */ +export const coordsToBackgroundPosition = ( value ) => { + if ( ! value || ( isNaN( value.x ) && isNaN( value.y ) ) ) { + return undefined; + } + + const x = isNaN( value.x ) ? 0.5 : value.x; + const y = isNaN( value.y ) ? 0.5 : value.y; + + return `${ x * 100 }% ${ y * 100 }%`; +}; + +/** + * Converts backgroundPosition value to x and y coords for FocalPointPicker. + * + * @param {string} value backgroundPosition value. + * @return {{x?:number, y?:number}} FocalPointPicker coords. + */ +export const backgroundPositionToCoords = ( value ) => { + if ( ! value ) { + return { x: undefined, y: undefined }; + } + + let [ x, y ] = value.split( ' ' ).map( ( v ) => parseFloat( v ) / 100 ); + x = isNaN( x ) ? undefined : x; + y = isNaN( y ) ? x : y; + + return { x, y }; +}; + +function InspectorImagePreview( { label, filename, url: imgUrl } ) { + const imgLabel = label || getFilename( imgUrl ); + return ( + + + + { imgUrl && ( + + ) } + + + + { imgLabel } + + + { filename + ? sprintf( + /* translators: %s: file name */ + __( 'Selected image: %s' ), + filename + ) + : __( 'No image selected' ) } + + + + + ); +} + +function BackgroundImageToolsPanelItem( { + panelId, + isShownByDefault, + onChange, + style, + inheritedValue, +} ) { + const mediaUpload = useSelect( + ( select ) => select( blockEditorStore ).getSettings().mediaUpload, + [] + ); + + const { id, title, url } = style?.background?.backgroundImage || { + ...inheritedValue?.background?.backgroundImage, + }; + + const replaceContainerRef = useRef(); + + const { createErrorNotice } = useDispatch( noticesStore ); + const onUploadError = ( message ) => { + createErrorNotice( message, { type: 'snackbar' } ); + }; + + const resetBackgroundImage = () => + onChange( + setImmutably( + style, + [ 'background', 'backgroundImage' ], + undefined + ) + ); + + const onSelectMedia = ( media ) => { + if ( ! media || ! media.url ) { + resetBackgroundImage(); + return; + } + + if ( isBlobURL( media.url ) ) { + return; + } + + // For media selections originated from a file upload. + if ( + ( media.media_type && + media.media_type !== IMAGE_BACKGROUND_TYPE ) || + ( ! media.media_type && + media.type && + media.type !== IMAGE_BACKGROUND_TYPE ) + ) { + onUploadError( + __( 'Only images can be used as a background image.' ) + ); + return; + } + + onChange( + setImmutably( style, [ 'background', 'backgroundImage' ], { + url: media.url, + id: media.id, + source: 'file', + title: media.title || undefined, + } ) + ); + }; + + const onFilesDrop = ( filesList ) => { + mediaUpload( { + allowedTypes: [ 'image' ], + filesList, + onFileChange( [ image ] ) { + if ( isBlobURL( image?.url ) ) { + return; + } + onSelectMedia( image ); + }, + onError: onUploadError, + } ); + }; + + const resetAllFilter = useCallback( ( previousValue ) => { + return { + ...previousValue, + style: { + ...previousValue.style, + background: undefined, + }, + }; + }, [] ); + + const hasValue = + hasBackgroundImageValue( style ) || + hasBackgroundImageValue( inheritedValue ); + + return ( + hasValue } + label={ __( 'Background image' ) } + onDeselect={ resetBackgroundImage } + isShownByDefault={ isShownByDefault } + resetAllFilter={ resetAllFilter } + panelId={ panelId } + > +
+ + } + variant="secondary" + > + { hasValue && ( + { + const [ toggleButton ] = focus.tabbable.find( + replaceContainerRef.current + ); + // Focus the toggle button and close the dropdown menu. + // This ensures similar behaviour as to selecting an image, where the dropdown is + // closed and focus is redirected to the dropdown toggle button. + toggleButton?.focus(); + toggleButton?.click(); + resetBackgroundImage(); + } } + > + { __( 'Reset ' ) } + + ) } + + +
+
+ ); +} + +function BackgroundSizeToolsPanelItem( { + panelId, + isShownByDefault, + onChange, + style, + inheritedValue, + defaultValues, +} ) { + const sizeValue = + style?.background?.backgroundSize || + inheritedValue?.background?.backgroundSize; + const repeatValue = + style?.background?.backgroundRepeat || + inheritedValue?.background?.backgroundRepeat; + const imageValue = + style?.background?.backgroundImage?.url || + inheritedValue?.background?.backgroundImage?.url; + const positionValue = + style?.background?.backgroundPosition || + inheritedValue?.background?.backgroundPosition; + + /* + * An `undefined` value is replaced with any supplied + * default control value for the toggle group control. + * An empty string is treated as `auto` - this allows a user + * to select "Size" and then enter a custom value, with an + * empty value being treated as `auto`. + */ + const currentValueForToggle = + ( sizeValue !== undefined && + sizeValue !== 'cover' && + sizeValue !== 'contain' ) || + sizeValue === '' + ? 'auto' + : sizeValue || defaultValues?.backgroundSize; + + /* + * If the current value is `cover` and the repeat value is `undefined`, then + * the toggle should be unchecked as the default state. Otherwise, the toggle + * should reflect the current repeat value. + */ + const repeatCheckedValue = ! ( + repeatValue === 'no-repeat' || + ( currentValueForToggle === 'cover' && repeatValue === undefined ) + ); + + const hasValue = hasBackgroundSizeValue( style ); + + const resetAllFilter = useCallback( ( previousValue ) => { + return { + ...previousValue, + style: { + ...previousValue.style, + background: { + ...previousValue.style?.background, + backgroundRepeat: undefined, + backgroundSize: undefined, + }, + }, + }; + }, [] ); + + const updateBackgroundSize = ( next ) => { + // When switching to 'contain' toggle the repeat off. + let nextRepeat = repeatValue; + + if ( next === 'contain' ) { + nextRepeat = 'no-repeat'; + } + + if ( next === 'cover' ) { + nextRepeat = undefined; + } + + if ( + ( currentValueForToggle === 'cover' || + currentValueForToggle === 'contain' ) && + next === 'auto' + ) { + nextRepeat = undefined; + } + + onChange( + setImmutably( style, [ 'background' ], { + ...style?.background, + backgroundRepeat: nextRepeat, + backgroundSize: next, + } ) + ); + }; + + const updateBackgroundPosition = ( next ) => { + onChange( + setImmutably( + style, + [ 'background', 'backgroundPosition' ], + coordsToBackgroundPosition( next ) + ) + ); + }; + + const toggleIsRepeated = () => + onChange( + setImmutably( + style, + [ 'background', 'backgroundRepeat' ], + repeatCheckedValue === true ? 'no-repeat' : undefined + ) + ); + + const resetBackgroundSize = () => + onChange( + setImmutably( style, [ 'background' ], { + ...style?.background, + backgroundPosition: undefined, + backgroundRepeat: undefined, + backgroundSize: undefined, + } ) + ); + + return ( + hasValue } + label={ __( 'Size' ) } + onDeselect={ resetBackgroundSize } + isShownByDefault={ isShownByDefault } + resetAllFilter={ resetAllFilter } + panelId={ panelId } + > + + + + + + + { sizeValue !== undefined && + sizeValue !== 'cover' && + sizeValue !== 'contain' ? ( + + ) : null } + { currentValueForToggle !== 'cover' && ( + + ) } + + ); +} + +function BackgroundToolsPanel( { + resetAllFilter, + onChange, + value, + panelId, + children, +} ) { + const resetAll = () => { + const updatedValue = resetAllFilter( value ); + onChange( updatedValue ); + }; + + return ( + + { children } + + ); +} + +const DEFAULT_CONTROLS = { + backgroundImage: true, + backgroundSize: true, +}; + +export default function BackgroundPanel( { + as: Wrapper = BackgroundToolsPanel, + value, + onChange, + inheritedValue = value, + settings, + panelId, + defaultControls = DEFAULT_CONTROLS, + defaultValues = {}, +} ) { + const resetAllFilter = useCallback( ( previousValue ) => { + return { + ...previousValue, + background: {}, + }; + }, [] ); + const shouldShowBackgroundSizeControls = + settings?.background?.backgroundSize; + + return ( + + + { shouldShowBackgroundSizeControls && ( + + ) } + + ); +} diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index 6be5481a633daa..bdda9563edae02 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -27,6 +27,7 @@ const VALID_SETTINGS = [ 'background.backgroundImage', 'background.backgroundRepeat', 'background.backgroundSize', + 'background.backgroundPosition', 'border.color', 'border.radius', 'border.style', diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js index ca8d1168d02d0f..7ad192fac9b4f5 100644 --- a/packages/block-editor/src/components/global-styles/index.js +++ b/packages/block-editor/src/components/global-styles/index.js @@ -31,5 +31,9 @@ export { useHasImageSettingsPanel, } from './image-settings-panel'; export { default as AdvancedPanel } from './advanced-panel'; +export { + default as BackgroundPanel, + useHasBackgroundPanel, +} from './background-panel'; export { areGlobalStyleConfigsEqual } from './utils'; export { default as getGlobalStylesChanges } from './get-global-styles-changes'; diff --git a/packages/block-editor/src/components/global-styles/style.scss b/packages/block-editor/src/components/global-styles/style.scss index 712590921c0406..ab4407cd9b911b 100644 --- a/packages/block-editor/src/components/global-styles/style.scss +++ b/packages/block-editor/src/components/global-styles/style.scss @@ -70,3 +70,80 @@ /*rtl:ignore*/ direction: ltr; } + +.block-editor-global-styles-background-panel__inspector-media-replace-container { + position: relative; + // Since there is no option to skip rendering the drag'n'drop icon in drop + // zone, we hide it for now. + .components-drop-zone__content-icon { + display: none; + } + + button.components-button { + color: $gray-900; + box-shadow: inset 0 0 0 $border-width $gray-300; + width: 100%; + display: block; + height: $grid-unit-50; + + &:hover { + color: var(--wp-admin-theme-color); + } + + &:focus { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + } + } + + .block-editor-global-styles-background-panel__inspector-media-replace-title { + word-break: break-all; + // The Button component is white-space: nowrap, and that won't work with line-clamp. + white-space: normal; + + // Without this, the ellipsis can sometimes be partially hidden by the Button padding. + text-align: start; + text-align-last: center; + } + + .components-dropdown { + display: block; + } +} + +.block-editor-global-styles-background-panel__inspector-image-indicator-wrapper { + background: #fff linear-gradient(-45deg, transparent 48%, $gray-300 48%, $gray-300 52%, transparent 52%); // Show a diagonal line (crossed out) for empty background image. + border-radius: $radius-round !important; // Override the default border-radius inherited from FlexItem. + box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.2); + display: block; + width: 20px; + height: 20px; + flex: none; + + &.has-image { + background: #fff; // No diagonal line for non-empty background image. A background color is in use to account for partially transparent images. + } +} + +.block-editor-global-styles-background-panel__inspector-image-indicator { + background-size: cover; + border-radius: $radius-round; + width: 20px; + height: 20px; + display: block; + position: relative; +} + +.block-editor-global-styles-background-panel__inspector-image-indicator::after { + content: ""; + position: absolute; + top: -1px; + left: -1px; + bottom: -1px; + right: -1px; + border-radius: $radius-round; + box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.2); + // Show a thin outline in Windows high contrast mode, otherwise the button is invisible. + border: 1px solid transparent; + box-sizing: inherit; +} + diff --git a/packages/block-editor/src/hooks/test/background.js b/packages/block-editor/src/components/global-styles/test/background-panel.js similarity index 60% rename from packages/block-editor/src/hooks/test/background.js rename to packages/block-editor/src/components/global-styles/test/background-panel.js index cbc9033c2256f6..d0b3a8ad601700 100644 --- a/packages/block-editor/src/hooks/test/background.js +++ b/packages/block-editor/src/components/global-styles/test/background-panel.js @@ -5,7 +5,8 @@ import { backgroundPositionToCoords, coordsToBackgroundPosition, -} from '../background'; + hasBackgroundImageValue, +} from '../background-panel'; describe( 'backgroundPositionToCoords', () => { it( 'should return the correct coordinates for a percentage value using 2-value syntax', () => { @@ -48,3 +49,37 @@ describe( 'coordsToBackgroundPosition', () => { expect( coordsToBackgroundPosition( {} ) ).toBeUndefined(); } ); } ); + +describe( 'hasBackgroundImageValue', () => { + it( 'should return `true` when id and url exist', () => { + expect( + hasBackgroundImageValue( { + background: { backgroundImage: { id: 1, url: 'url' } }, + } ) + ).toBe( true ); + } ); + + it( 'should return `true` when only url exists', () => { + expect( + hasBackgroundImageValue( { + background: { backgroundImage: { url: 'url' } }, + } ) + ).toBe( true ); + } ); + + it( 'should return `true` when only id exists', () => { + expect( + hasBackgroundImageValue( { + background: { backgroundImage: { id: 1 } }, + } ) + ).toBe( true ); + } ); + + it( 'should return `false` when id and url do not exist', () => { + expect( + hasBackgroundImageValue( { + background: { backgroundImage: {} }, + } ) + ).toBe( false ); + } ); +} ); diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index 5a1a00306973fd..7c8d62d5dd5a99 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -1,77 +1,28 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ -import { isBlobURL } from '@wordpress/blob'; import { getBlockSupport } from '@wordpress/blocks'; -import { focus } from '@wordpress/dom'; -import { - ToggleControl, - __experimentalToggleGroupControl as ToggleGroupControl, - __experimentalToggleGroupControlOption as ToggleGroupControlOption, - __experimentalToolsPanelItem as ToolsPanelItem, - __experimentalUnitControl as UnitControl, - __experimentalVStack as VStack, - DropZone, - FlexItem, - FocalPointPicker, - MenuItem, - VisuallyHidden, - __experimentalItemGroup as ItemGroup, - __experimentalHStack as HStack, - __experimentalTruncate as Truncate, -} from '@wordpress/components'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { Platform, useCallback, useRef } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; -import { getFilename } from '@wordpress/url'; +import { useSelect } from '@wordpress/data'; +import { useCallback } from '@wordpress/element'; /** * Internal dependencies */ import InspectorControls from '../components/inspector-controls'; -import MediaReplaceFlow from '../components/media-replace-flow'; -import { useSettings } from '../components/use-settings'; import { cleanEmptyObject } from './utils'; import { store as blockEditorStore } from '../store'; +import { + default as StylesBackgroundPanel, + useHasBackgroundPanel, + hasBackgroundImageValue, +} from '../components/global-styles/background-panel'; export const BACKGROUND_SUPPORT_KEY = 'background'; -export const IMAGE_BACKGROUND_TYPE = 'image'; - -/** - * Checks if there is a current value in the background image block support - * attributes. - * - * @param {Object} style Style attribute. - * @return {boolean} Whether or not the block has a background image value set. - */ -export function hasBackgroundImageValue( style ) { - const hasValue = - !! style?.background?.backgroundImage?.id || - !! style?.background?.backgroundImage?.url; - - return hasValue; -} -/** - * Checks if there is a current value in the background size block support - * attributes. Background size values include background size as well - * as background position. - * - * @param {Object} style Style attribute. - * @return {boolean} Whether or not the block has a background size value set. - */ -export function hasBackgroundSizeValue( style ) { - return ( - style?.background?.backgroundPosition !== undefined || - style?.background?.backgroundSize !== undefined - ); -} +// Initial control values where no block style is set. +const BACKGROUND_BLOCK_DEFAULT_VALUES = { + backgroundSize: 'cover', +}; /** * Determine whether there is block support for background. @@ -82,10 +33,6 @@ export function hasBackgroundSizeValue( style ) { * @return {boolean} Whether there is support. */ export function hasBackgroundSupport( blockName, feature = 'any' ) { - if ( Platform.OS !== 'web' ) { - return false; - } - const support = getBlockSupport( blockName, BACKGROUND_SUPPORT_KEY ); if ( support === true ) { @@ -103,84 +50,54 @@ export function hasBackgroundSupport( blockName, feature = 'any' ) { return !! support?.[ feature ]; } -function useBlockProps( { name, style } ) { - if ( - ! hasBackgroundSupport( name ) || - ! style?.background?.backgroundImage - ) { +export function setBackgroundStyleDefaults( backgroundStyle ) { + if ( ! backgroundStyle ) { return; } - const backgroundImage = style?.background?.backgroundImage; - let props; + const backgroundImage = backgroundStyle?.backgroundImage; + let backgroundStylesWithDefaults; // Set block background defaults. if ( backgroundImage?.source === 'file' && !! backgroundImage?.url ) { - if ( ! style?.background?.backgroundSize ) { - props = { - style: { - backgroundSize: 'cover', - }, + if ( ! backgroundStyle?.backgroundSize ) { + backgroundStylesWithDefaults = { + backgroundSize: 'cover', }; } if ( - 'contain' === style?.background?.backgroundSize && - ! style?.background?.backgroundPosition + 'contain' === backgroundStyle?.backgroundSize && + ! backgroundStyle?.backgroundPosition ) { - props = { - style: { - backgroundPosition: 'center', - }, + backgroundStylesWithDefaults = { + backgroundPosition: 'center', }; } } - if ( ! props ) { + return backgroundStylesWithDefaults; +} + +function useBlockProps( { name, style } ) { + if ( + ! hasBackgroundSupport( name ) || + ! style?.background?.backgroundImage + ) { return; } - return props; -} + const backgroundStyles = setBackgroundStyleDefaults( style?.background ); -/** - * Resets the background image block support attributes. This can be used when disabling - * the background image controls for a block via a `ToolsPanel`. - * - * @param {Object} style Style attribute. - * @param {Function} setAttributes Function to set block's attributes. - */ -export function resetBackgroundImage( style = {}, setAttributes ) { - setAttributes( { - style: cleanEmptyObject( { - ...style, - background: { - ...style?.background, - backgroundImage: undefined, - }, - } ), - } ); -} + if ( ! backgroundStyles ) { + return; + } -/** - * Resets the background size block support attributes. This can be used when disabling - * the background size controls for a block via a `ToolsPanel`. - * - * @param {Object} style Style attribute. - * @param {Function} setAttributes Function to set block's attributes. - */ -function resetBackgroundSize( style = {}, setAttributes ) { - setAttributes( { - style: cleanEmptyObject( { - ...style, - background: { - ...style?.background, - backgroundPosition: undefined, - backgroundRepeat: undefined, - backgroundSize: undefined, - }, - } ), - } ); + return { + style: { + ...backgroundStyles, + }, + }; } /** @@ -194,252 +111,28 @@ export function getBackgroundImageClasses( style ) { return hasBackgroundImageValue( style ) ? 'has-background' : ''; } -function InspectorImagePreview( { label, filename, url: imgUrl } ) { - const imgLabel = label || getFilename( imgUrl ); - return ( - - - - { imgUrl && ( - - ) } - - - - { imgLabel } - - - { filename - ? sprintf( - /* translators: %s: file name */ - __( 'Selected image: %s' ), - filename - ) - : __( 'No image selected' ) } - - - - - ); -} - -function BackgroundImagePanelItem( { - clientId, - isShownByDefault, - setAttributes, -} ) { - const { style, mediaUpload } = useSelect( - ( select ) => { - const { getBlockAttributes, getSettings } = - select( blockEditorStore ); - - return { - style: getBlockAttributes( clientId )?.style, - mediaUpload: getSettings().mediaUpload, - }; - }, - [ clientId ] - ); - const { id, title, url } = style?.background?.backgroundImage || {}; - - const replaceContainerRef = useRef(); - - const { createErrorNotice } = useDispatch( noticesStore ); - const onUploadError = ( message ) => { - createErrorNotice( message, { type: 'snackbar' } ); - }; - - const onSelectMedia = ( media ) => { - if ( ! media || ! media.url ) { - const newStyle = { - ...style, - background: { - ...style?.background, - backgroundImage: undefined, - }, - }; - - const newAttributes = { - style: cleanEmptyObject( newStyle ), - }; - - setAttributes( newAttributes ); - return; - } - - if ( isBlobURL( media.url ) ) { - return; - } - - // For media selections originated from a file upload. - if ( - ( media.media_type && - media.media_type !== IMAGE_BACKGROUND_TYPE ) || - ( ! media.media_type && - media.type && - media.type !== IMAGE_BACKGROUND_TYPE ) - ) { - onUploadError( - __( 'Only images can be used as a background image.' ) - ); - return; - } - - const newStyle = { - ...style, - background: { - ...style?.background, - backgroundImage: { - url: media.url, - id: media.id, - source: 'file', - title: media.title || undefined, - }, - }, - }; - - const newAttributes = { - style: cleanEmptyObject( newStyle ), - }; - - setAttributes( newAttributes ); - }; - - const onFilesDrop = ( filesList ) => { - mediaUpload( { - allowedTypes: [ 'image' ], - filesList, - onFileChange( [ image ] ) { - if ( isBlobURL( image?.url ) ) { - return; - } - onSelectMedia( image ); - }, - onError: onUploadError, - } ); - }; - - const resetAllFilter = useCallback( ( previousValue ) => { +function BackgroundInspectorControl( { children } ) { + const resetAllFilter = useCallback( ( attributes ) => { return { - ...previousValue, + ...attributes, style: { - ...previousValue.style, + ...attributes.style, background: undefined, }, }; }, [] ); - - const hasValue = hasBackgroundImageValue( style ); - return ( - hasValue } - label={ __( 'Background image' ) } - onDeselect={ () => resetBackgroundImage( style, setAttributes ) } - isShownByDefault={ isShownByDefault } - resetAllFilter={ resetAllFilter } - panelId={ clientId } - > -
- - } - variant="secondary" - > - { hasValue && ( - { - const [ toggleButton ] = focus.tabbable.find( - replaceContainerRef.current - ); - // Focus the toggle button and close the dropdown menu. - // This ensures similar behaviour as to selecting an image, where the dropdown is - // closed and focus is redirected to the dropdown toggle button. - toggleButton?.focus(); - toggleButton?.click(); - resetBackgroundImage( style, setAttributes ); - } } - > - { __( 'Reset ' ) } - - ) } - - -
-
+ + { children } + ); } -function backgroundSizeHelpText( value ) { - if ( value === 'cover' || value === undefined ) { - return __( 'Image covers the space evenly.' ); - } - if ( value === 'contain' ) { - return __( 'Image is contained without distortion.' ); - } - return __( 'Specify a fixed width.' ); -} - -export const coordsToBackgroundPosition = ( value ) => { - if ( ! value || ( isNaN( value.x ) && isNaN( value.y ) ) ) { - return undefined; - } - - const x = isNaN( value.x ) ? 0.5 : value.x; - const y = isNaN( value.y ) ? 0.5 : value.y; - - return `${ x * 100 }% ${ y * 100 }%`; -}; - -export const backgroundPositionToCoords = ( value ) => { - if ( ! value ) { - return { x: undefined, y: undefined }; - } - - let [ x, y ] = value.split( ' ' ).map( ( v ) => parseFloat( v ) / 100 ); - x = isNaN( x ) ? undefined : x; - y = isNaN( y ) ? x : y; - - return { x, y }; -}; - -function BackgroundSizePanelItem( { +export function BackgroundImagePanel( { clientId, - isShownByDefault, + name, setAttributes, + settings, } ) { const style = useSelect( ( select ) => @@ -447,198 +140,44 @@ function BackgroundSizePanelItem( { [ clientId ] ); - const sizeValue = style?.background?.backgroundSize; - const repeatValue = style?.background?.backgroundRepeat; - - // An `undefined` value is treated as `cover` by the toggle group control. - // An empty string is treated as `auto` by the toggle group control. This - // allows a user to select "Size" and then enter a custom value, with an - // empty value being treated as `auto`. - const currentValueForToggle = - ( sizeValue !== undefined && - sizeValue !== 'cover' && - sizeValue !== 'contain' ) || - sizeValue === '' - ? 'auto' - : sizeValue || 'cover'; - - // If the current value is `cover` and the repeat value is `undefined`, then - // the toggle should be unchecked as the default state. Otherwise, the toggle - // should reflect the current repeat value. - const repeatCheckedValue = ! ( - repeatValue === 'no-repeat' || - ( currentValueForToggle === 'cover' && repeatValue === undefined ) - ); - - const hasValue = hasBackgroundSizeValue( style ); - - const resetAllFilter = useCallback( ( previousValue ) => { - return { - ...previousValue, - style: { - ...previousValue.style, - background: { - ...previousValue.style?.background, - backgroundRepeat: undefined, - backgroundSize: undefined, - }, - }, - }; - }, [] ); - - const updateBackgroundSize = ( next ) => { - // When switching to 'contain' toggle the repeat off. - let nextRepeat = repeatValue; - - if ( next === 'contain' ) { - nextRepeat = 'no-repeat'; - } - - if ( - ( currentValueForToggle === 'cover' || - currentValueForToggle === 'contain' ) && - next === 'auto' - ) { - nextRepeat = undefined; - } - - setAttributes( { - style: cleanEmptyObject( { - ...style, - background: { - ...style?.background, - backgroundRepeat: nextRepeat, - backgroundSize: next, - }, - } ), - } ); - }; - - const updateBackgroundPosition = ( next ) => { - setAttributes( { - style: cleanEmptyObject( { - ...style, - background: { - ...style?.background, - backgroundPosition: coordsToBackgroundPosition( next ), - }, - } ), - } ); - }; - - const toggleIsRepeated = () => { - setAttributes( { - style: cleanEmptyObject( { - ...style, - background: { - ...style?.background, - backgroundRepeat: - repeatCheckedValue === true ? 'no-repeat' : undefined, - }, - } ), - } ); - }; - - return ( - hasValue } - label={ __( 'Size' ) } - onDeselect={ () => resetBackgroundSize( style, setAttributes ) } - isShownByDefault={ isShownByDefault } - resetAllFilter={ resetAllFilter } - panelId={ clientId } - > - - - - - - - { sizeValue !== undefined && - sizeValue !== 'cover' && - sizeValue !== 'contain' ? ( - - ) : null } - { currentValueForToggle !== 'cover' && ( - - ) } - - ); -} - -export function BackgroundImagePanel( props ) { - const [ backgroundImage, backgroundSize ] = useSettings( - 'background.backgroundImage', - 'background.backgroundSize' - ); - if ( - ! backgroundImage || - ! hasBackgroundSupport( props.name, 'backgroundImage' ) + ! useHasBackgroundPanel( settings ) || + ! hasBackgroundSupport( name, 'backgroundImage' ) ) { return null; } - const showBackgroundSize = !! ( - backgroundSize && hasBackgroundSupport( props.name, 'backgroundSize' ) - ); - - const defaultControls = getBlockSupport( props.name, [ + const defaultControls = getBlockSupport( name, [ BACKGROUND_SUPPORT_KEY, '__experimentalDefaultControls', ] ); + const onChange = ( newStyle ) => { + setAttributes( { + style: cleanEmptyObject( newStyle ), + } ); + }; + + const updatedSettings = { + ...settings, + background: { + ...settings.background, + backgroundSize: + settings?.background?.backgroundSize && + hasBackgroundSupport( name, 'backgroundSize' ), + }, + }; + return ( - - - { showBackgroundSize && ( - - ) } - + ); } diff --git a/packages/block-editor/src/hooks/background.scss b/packages/block-editor/src/hooks/background.scss deleted file mode 100644 index a81b6acfce2de7..00000000000000 --- a/packages/block-editor/src/hooks/background.scss +++ /dev/null @@ -1,75 +0,0 @@ -.block-editor-hooks__background__inspector-media-replace-container { - position: relative; - // Since there is no option to skip rendering the drag'n'drop icon in drop - // zone, we hide it for now. - .components-drop-zone__content-icon { - display: none; - } - - button.components-button { - color: $gray-900; - box-shadow: inset 0 0 0 $border-width $gray-300; - width: 100%; - display: block; - height: $grid-unit-50; - - &:hover { - color: var(--wp-admin-theme-color); - } - - &:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - } - } - - .block-editor-hooks__background__inspector-media-replace-title { - word-break: break-all; - // The Button component is white-space: nowrap, and that won't work with line-clamp. - white-space: normal; - - // Without this, the ellipsis can sometimes be partially hidden by the Button padding. - text-align: start; - text-align-last: center; - } - - .components-dropdown { - display: block; - } -} - -.block-editor-hooks__background__inspector-image-indicator-wrapper { - background: #fff linear-gradient(-45deg, transparent 48%, $gray-300 48%, $gray-300 52%, transparent 52%); // Show a diagonal line (crossed out) for empty background image. - border-radius: $radius-round !important; // Override the default border-radius inherited from FlexItem. - box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.2); - display: block; - width: 20px; - height: 20px; - flex: none; - - &.has-image { - background: #fff; // No diagonal line for non-empty background image. A background color is in use to account for partially transparent images. - } -} - -.block-editor-hooks__background__inspector-image-indicator { - background-size: cover; - border-radius: $radius-round; - width: 20px; - height: 20px; - display: block; - position: relative; -} - -.block-editor-hooks__background__inspector-image-indicator::after { - content: ""; - position: absolute; - top: -1px; - left: -1px; - bottom: -1px; - right: -1px; - border-radius: $radius-round; - box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.2); - // Show a thin outline in Windows high contrast mode, otherwise the button is invisible. - border: 1px solid transparent; - box-sizing: inherit; -} diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 015cffde42a239..f3a38490be986c 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -49,7 +49,6 @@ @import "./components/url-popover/style.scss"; @import "./hooks/anchor.scss"; @import "./hooks/block-hooks.scss"; -@import "./hooks/background.scss"; @import "./hooks/border.scss"; @import "./hooks/color.scss"; @import "./hooks/dimensions.scss"; diff --git a/packages/edit-site/src/components/global-styles/background-panel.js b/packages/edit-site/src/components/global-styles/background-panel.js new file mode 100644 index 00000000000000..e4760a810ecbc2 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/background-panel.js @@ -0,0 +1,34 @@ +/** + * WordPress dependencies + */ +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +const { + useGlobalStyle, + useGlobalSetting, + BackgroundPanel: StylesBackgroundPanel, +} = unlock( blockEditorPrivateApis ); + +export default function BackgroundPanel() { + const [ style ] = useGlobalStyle( '', undefined, 'user', { + shouldDecodeEncode: false, + } ); + const [ inheritedStyle, setStyle ] = useGlobalStyle( '', undefined, 'all', { + shouldDecodeEncode: false, + } ); + const [ settings ] = useGlobalSetting( '' ); + + return ( + + ); +} diff --git a/packages/edit-site/src/components/global-styles/root-menu.js b/packages/edit-site/src/components/global-styles/root-menu.js index 9edfd064acbf73..97598635f7b859 100644 --- a/packages/edit-site/src/components/global-styles/root-menu.js +++ b/packages/edit-site/src/components/global-styles/root-menu.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; -import { typography, color, layout } from '@wordpress/icons'; +import { typography, color, layout, image } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; @@ -18,6 +18,7 @@ const { useHasColorPanel, useGlobalSetting, useSettingsForBlockElement, + useHasBackgroundPanel, } = unlock( blockEditorPrivateApis ); function RootMenu() { @@ -27,6 +28,7 @@ function RootMenu() { const hasColorPanel = useHasColorPanel( settings ); const hasDimensionsPanel = useHasDimensionsPanel( settings ); const hasLayoutPanel = hasDimensionsPanel; + const hasBackgroundPanel = useHasBackgroundPanel( settings ); return ( <> @@ -58,6 +60,15 @@ function RootMenu() { { __( 'Layout' ) } ) } + { hasBackgroundPanel && ( + + { __( 'Background' ) } + + ) } ); diff --git a/packages/edit-site/src/components/global-styles/screen-background.js b/packages/edit-site/src/components/global-styles/screen-background.js new file mode 100644 index 00000000000000..5e8a7832a42b46 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/screen-background.js @@ -0,0 +1,29 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import BackgroundPanel from './background-panel'; +import ScreenHeader from './header'; +import { unlock } from '../../lock-unlock'; + +const { useHasBackgroundPanel, useGlobalSetting } = unlock( + blockEditorPrivateApis +); + +function ScreenBackground() { + const [ settings ] = useGlobalSetting( '' ); + const hasBackgroundPanel = useHasBackgroundPanel( settings ); + return ( + <> + + { hasBackgroundPanel && } + + ); +} + +export default ScreenBackground; diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index cdaadb1d1acb37..b50e09550f7079 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -40,6 +40,7 @@ import ScreenStyleVariations from './screen-style-variations'; import StyleBook from '../style-book'; import ScreenCSS from './screen-css'; import ScreenRevisions from './screen-revisions'; +import ScreenBackground from './screen-background'; import { unlock } from '../../lock-unlock'; import { store as editSiteStore } from '../../store'; @@ -344,6 +345,10 @@ function GlobalStylesUI() { + + + + { blocks.map( ( block ) => (