From 9c56c67e34d59289d4ef9608195f32f6bae16580 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Fri, 8 Sep 2023 18:50:17 +1000
Subject: [PATCH] Block Supports: Add background image support to Group block
(#53934)
* Media Block Support: Add backgroundImage support to Group block
* Add error handling for video uploads, refactor to a BackgroundImagePanelItem component
* Ensure styles are not serialized in save mode, and not duplicated on site frontend
* Fix upload issue
* Try using style engine for PHP output, too
* Try renaming block support and panel to Background
* Rename panel label to Background image, rename component for consistency
* Update block.json schema, fix issue with omitting styles
* Simplify block schema slightly, make the check for the background.backgroundImage property
* Update test, remove TODO comment
* Fix upload container classname
* Update hasBackgroundSupport to handle any case
* Update theme.json schema
* Store media title in attributes, update docs
---
docs/reference-guides/core-blocks.md | 2 +-
.../theme-json-reference/theme-json-living.md | 11 +
lib/block-supports/background.php | 103 +++++++
lib/class-wp-theme-json-gutenberg.php | 4 +
lib/load.php | 1 +
.../src/components/block-inspector/index.js | 4 +
.../inspector-controls-tabs/styles-tab.js | 4 +
.../components/inspector-controls/groups.js | 8 +-
packages/block-editor/src/hooks/background.js | 281 ++++++++++++++++++
.../block-editor/src/hooks/background.scss | 57 ++++
packages/block-editor/src/hooks/style.js | 13 +-
packages/block-editor/src/style.scss | 1 +
packages/block-library/src/group/block.json | 3 +
.../style-engine/class-wp-style-engine.php | 50 ++++
.../src/styles/background/index.ts | 43 +++
packages/style-engine/src/styles/index.ts | 2 +
packages/style-engine/src/styles/utils.ts | 21 ++
packages/style-engine/src/types.ts | 7 +
phpunit/style-engine/style-engine-test.php | 19 ++
schemas/json/block.json | 11 +
schemas/json/theme.json | 22 +-
21 files changed, 661 insertions(+), 6 deletions(-)
create mode 100644 lib/block-supports/background.php
create mode 100644 packages/block-editor/src/hooks/background.js
create mode 100644 packages/block-editor/src/hooks/background.scss
create mode 100644 packages/style-engine/src/styles/background/index.ts
diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md
index 5767ce77461931..b8a5390aebf549 100644
--- a/docs/reference-guides/core-blocks.md
+++ b/docs/reference-guides/core-blocks.md
@@ -302,7 +302,7 @@ Gather blocks in a layout container. ([Source](https://github.com/WordPress/gute
- **Name:** core/group
- **Category:** design
-- **Supports:** align (full, wide), anchor, ariaLabel, color (background, button, gradients, heading, link, text), dimensions (minHeight), layout (allowSizingOnChildren), position (sticky), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~
+- **Supports:** align (full, wide), anchor, ariaLabel, background (backgroundImage), color (background, button, gradients, heading, link, text), dimensions (minHeight), layout (allowSizingOnChildren), position (sticky), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~
- **Attributes:** allowedBlocks, tagName, templateLock
## Heading
diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md
index 9f9e4f68aedec4..ba80addbd1f65a 100644
--- a/docs/reference-guides/theme-json-reference/theme-json-living.md
+++ b/docs/reference-guides/theme-json-reference/theme-json-living.md
@@ -31,6 +31,7 @@ Code editors can pick up the schema and can provide helpful hints and suggestion
Setting that enables the following UI tools:
+- background: backgroundImage
- border: color, radius, style, width
- color: link
- dimensions: minHeight
@@ -97,6 +98,16 @@ Settings related to colors.
---
+### background
+
+Settings related to background.
+
+| Property | Type | Default | Props |
+| --- | --- | --- |--- |
+| backgroundImage | boolean | false | |
+
+---
+
### dimensions
Settings related to dimensions.
diff --git a/lib/block-supports/background.php b/lib/block-supports/background.php
new file mode 100644
index 00000000000000..08a9c012a31458
--- /dev/null
+++ b/lib/block-supports/background.php
@@ -0,0 +1,103 @@
+attributes ) {
+ $block_type->attributes = array();
+ }
+
+ // Check for existing style attribute definition e.g. from block.json.
+ if ( array_key_exists( 'style', $block_type->attributes ) ) {
+ return;
+ }
+
+ $has_background_support = block_has_support( $block_type, array( 'background' ), false );
+
+ if ( $has_background_support ) {
+ $block_type->attributes['style'] = array(
+ 'type' => 'object',
+ );
+ }
+}
+
+/**
+ * Renders the background styles to the block wrapper.
+ * This block support uses the `render_block` hook to ensure that
+ * it is also applied to non-server-rendered blocks.
+ *
+ * @param string $block_content Rendered block content.
+ * @param array $block Block object.
+ * @return string Filtered block content.
+ */
+function gutenberg_render_background_support( $block_content, $block ) {
+ $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
+ $block_attributes = $block['attrs'];
+ $has_background_image_support = block_has_support( $block_type, array( 'background', 'backgroundImage' ), false );
+
+ if (
+ ! $has_background_image_support ||
+ wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundImage' )
+ ) {
+ return $block_content;
+ }
+
+ $background_image_source = _wp_array_get( $block_attributes, array( 'style', 'background', 'backgroundImage', 'source' ), null );
+ $background_image_url = _wp_array_get( $block_attributes, array( 'style', 'background', 'backgroundImage', 'url' ), null );
+ $background_size = _wp_array_get( $block_attributes, array( 'style', 'background', 'backgroundSize' ), 'cover' );
+
+ $background_block_styles = array();
+
+ if (
+ 'file' === $background_image_source &&
+ $background_image_url
+ ) {
+ // Set file based background URL.
+ // TODO: In a follow-up, similar logic could be added to inject a featured image url.
+ $background_block_styles['backgroundImage']['url'] = $background_image_url;
+ // Only output the background size when an image url is set.
+ $background_block_styles['backgroundSize'] = $background_size;
+ }
+
+ $styles = gutenberg_style_engine_get_styles( array( 'background' => $background_block_styles ) );
+
+ if ( ! empty( $styles['css'] ) ) {
+ // Inject background styles to the first element, presuming it's the wrapper, if it exists.
+ $tags = new WP_HTML_Tag_Processor( $block_content );
+
+ if ( $tags->next_tag() ) {
+ $existing_style = $tags->get_attribute( 'style' );
+ $updated_style = '';
+
+ if ( ! empty( $existing_style ) && ! str_ends_with( $existing_style, ';' ) ) {
+ $updated_style = $existing_style . '; ';
+ }
+
+ $updated_style .= $styles['css'];
+ $tags->set_attribute( 'style', $updated_style );
+ }
+
+ return $tags->get_updated_html();
+ }
+
+ return $block_content;
+}
+
+// Register the block support.
+WP_Block_Supports::get_instance()->register(
+ 'background',
+ array(
+ 'register_attribute' => 'gutenberg_register_background_support',
+ )
+);
+
+add_filter( 'render_block', 'gutenberg_render_background_support', 10, 2 );
diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php
index 223fd4a7fd8b17..63de99624a0ac6 100644
--- a/lib/class-wp-theme-json-gutenberg.php
+++ b/lib/class-wp-theme-json-gutenberg.php
@@ -352,6 +352,9 @@ class WP_Theme_JSON_Gutenberg {
const VALID_SETTINGS = array(
'appearanceTools' => null,
'useRootPaddingAwareAlignments' => null,
+ 'background' => array(
+ 'backgroundImage' => null,
+ ),
'border' => array(
'color' => null,
'radius' => null,
@@ -569,6 +572,7 @@ public static function get_element_class_name( $element ) {
* @var array
*/
const APPEARANCE_TOOLS_OPT_INS = array(
+ array( 'background', 'backgroundImage' ),
array( 'border', 'color' ),
array( 'border', 'radius' ),
array( 'border', 'style' ),
diff --git a/lib/load.php b/lib/load.php
index 51a6500396650c..1f476ad8a8759d 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -229,3 +229,4 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/block-supports/duotone.php';
require __DIR__ . '/block-supports/shadow.php';
require __DIR__ . '/block-supports/behaviors.php';
+require __DIR__ . '/block-supports/background.php';
diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js
index b7155452379309..82d4fe27e7cc4e 100644
--- a/packages/block-editor/src/components/block-inspector/index.js
+++ b/packages/block-editor/src/components/block-inspector/index.js
@@ -300,6 +300,10 @@ const BlockInspectorSingleBlock = ( { clientId, blockName } ) => {
label={ __( 'Border' ) }
/>
+
diff --git a/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js b/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js
index 453b737cfbe3aa..9344cb94f8ef46 100644
--- a/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js
+++ b/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js
@@ -32,6 +32,10 @@ const StylesTab = ( { blockName, clientId, hasBlockStyles } ) => {
label={ __( 'Color' ) }
className="color-block-support-panel__inner-wrapper"
/>
+
+
+
+
+
+ { imgLabel }
+
+
+
+
+ );
+}
+
+function BackgroundImagePanelItem( props ) {
+ const { attributes, clientId, setAttributes } = props;
+
+ const { id, title, url } =
+ attributes.style?.background?.backgroundImage || {};
+
+ const { mediaUpload } = useSelect( ( select ) => {
+ return {
+ mediaUpload: select( blockEditorStore ).getSettings().mediaUpload,
+ };
+ } );
+
+ const { createErrorNotice } = useDispatch( noticesStore );
+ const onUploadError = ( message ) => {
+ createErrorNotice( message, { type: 'snackbar' } );
+ };
+
+ const onSelectMedia = ( media ) => {
+ if ( ! media || ! media.url ) {
+ const newStyle = {
+ ...attributes.style,
+ background: {
+ ...attributes.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 = {
+ ...attributes.style,
+ background: {
+ ...attributes.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 ) => {
+ return {
+ ...previousValue,
+ style: {
+ ...previousValue.style,
+ background: undefined,
+ },
+ };
+ }, [] );
+
+ return (
+ hasBackgroundImageValue( props ) }
+ label={ __( 'Background image' ) }
+ onDeselect={ () => resetBackgroundImage( props ) }
+ isShownByDefault={ true }
+ resetAllFilter={ resetAllFilter }
+ panelId={ clientId }
+ >
+
+ { !! url && (
+
+ }
+ variant="secondary"
+ />
+ ) }
+ { ! url && (
+
+ (
+
+
+
+
+ ) }
+ />
+
+ ) }
+
+
+ );
+}
+
+export function BackgroundImagePanel( props ) {
+ const isBackgroundImageSupported =
+ useSetting( 'background.backgroundImage' ) &&
+ hasBackgroundSupport( props.name, 'backgroundImage' );
+
+ if ( ! isBackgroundImageSupported ) {
+ return null;
+ }
+
+ return (
+
+ { isBackgroundImageSupported && (
+
+ ) }
+
+ );
+}
diff --git a/packages/block-editor/src/hooks/background.scss b/packages/block-editor/src/hooks/background.scss
new file mode 100644
index 00000000000000..0ac17181990191
--- /dev/null
+++ b/packages/block-editor/src/hooks/background.scss
@@ -0,0 +1,57 @@
+.block-editor-hooks__background__inspector-upload-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;
+ }
+}
+
+.block-editor-hooks__background__inspector-upload-container,
+.block-editor-hooks__background__inspector-media-replace-container {
+ button.components-button {
+ color: $gray-900;
+ box-shadow: inset 0 0 0 1px $gray-400;
+ 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;
+ }
+}
+
+.block-editor-hooks__background__inspector-media-replace-container {
+ .components-dropdown {
+ display: block;
+ }
+
+ img {
+ width: 20px;
+ min-width: 20px;
+ aspect-ratio: 1;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
+ border-radius: 50% !important;
+ }
+
+ .block-editor-hooks__background__inspector-readonly-logo-preview {
+ padding: 6px 12px;
+ display: flex;
+ height: $grid-unit-50;
+ }
+}
diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js
index e165a888822948..9c95f0a12996e1 100644
--- a/packages/block-editor/src/hooks/style.js
+++ b/packages/block-editor/src/hooks/style.js
@@ -20,6 +20,7 @@ import { getCSSRules, compileCSS } from '@wordpress/style-engine';
* Internal dependencies
*/
import BlockList from '../components/block-list';
+import { BACKGROUND_SUPPORT_KEY, BackgroundImagePanel } from './background';
import { BORDER_SUPPORT_KEY, BorderPanel } from './border';
import { COLOR_SUPPORT_KEY, ColorEdit } from './color';
import {
@@ -42,6 +43,7 @@ const styleSupportKeys = [
BORDER_SUPPORT_KEY,
COLOR_SUPPORT_KEY,
DIMENSIONS_SUPPORT_KEY,
+ BACKGROUND_SUPPORT_KEY,
SPACING_SUPPORT_KEY,
];
@@ -127,7 +129,11 @@ const skipSerializationPathsEdit = {
*/
const skipSerializationPathsSave = {
...skipSerializationPathsEdit,
- [ `${ SPACING_SUPPORT_KEY }` ]: [ 'spacing.blockGap' ],
+ [ `${ BACKGROUND_SUPPORT_KEY }` ]: [ BACKGROUND_SUPPORT_KEY ], // Skip serialization of background support in save mode.
+};
+
+const skipSerializationPathsSaveChecks = {
+ [ `${ BACKGROUND_SUPPORT_KEY }` ]: true,
};
/**
@@ -285,7 +291,9 @@ export function addSaveProps(
let { style } = attributes;
Object.entries( skipPaths ).forEach( ( [ indicator, path ] ) => {
- const skipSerialization = getBlockSupport( blockType, indicator );
+ const skipSerialization =
+ skipSerializationPathsSaveChecks[ indicator ] ||
+ getBlockSupport( blockType, indicator );
if ( skipSerialization === true ) {
style = omitStyle( style, path );
@@ -360,6 +368,7 @@ export const withBlockControls = createHigherOrderComponent(
{ shouldDisplayControls && blockEditingMode === 'default' && (
<>
+
diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss
index cdb4bb2a07abf0..7630fc3a152c77 100644
--- a/packages/block-editor/src/style.scss
+++ b/packages/block-editor/src/style.scss
@@ -47,6 +47,7 @@
@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/block-library/src/group/block.json b/packages/block-library/src/group/block.json
index 66d5a99e8bed48..4b89d865391172 100644
--- a/packages/block-library/src/group/block.json
+++ b/packages/block-library/src/group/block.json
@@ -29,6 +29,9 @@
"anchor": true,
"ariaLabel": true,
"html": false,
+ "background": {
+ "backgroundImage": true
+ },
"color": {
"gradients": true,
"heading": true,
diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php
index 283601e551c98f..6a61077c12ea24 100644
--- a/packages/style-engine/class-wp-style-engine.php
+++ b/packages/style-engine/class-wp-style-engine.php
@@ -41,6 +41,21 @@ final class WP_Style_Engine {
* @var array
*/
const BLOCK_STYLE_DEFINITIONS_METADATA = array(
+ 'background' => array(
+ 'backgroundImage' => array(
+ 'property_keys' => array(
+ 'default' => 'background-image',
+ ),
+ 'value_func' => array( self::class, 'get_url_or_value_css_declaration' ),
+ 'path' => array( 'background', 'backgroundImage' ),
+ ),
+ 'backgroundSize' => array(
+ 'property_keys' => array(
+ 'default' => 'background-size',
+ ),
+ 'path' => array( 'background', 'backgroundSize' ),
+ ),
+ ),
'color' => array(
'text' => array(
'property_keys' => array(
@@ -541,6 +556,41 @@ protected static function get_individual_property_css_declarations( $style_value
return $css_declarations;
}
+ /**
+ * Style value parser that constructs a CSS definition array comprising a single CSS property and value.
+ * If the provided value is an array containing a `url` property, the function will return a CSS definition array
+ * with a single property and value, with `url` escaped and injected into a CSS `url()` function,
+ * e.g., array( 'background-image' => "url( '...' )" ).
+ *
+ * @param array $style_value A single raw style value from $block_styles array.
+ * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA.
+ *
+ * @return string[] An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ).
+ */
+ protected static function get_url_or_value_css_declaration( $style_value, $style_definition ) {
+ if ( empty( $style_value ) ) {
+ return array();
+ }
+
+ $css_declarations = array();
+
+ if ( isset( $style_definition['property_keys']['default'] ) ) {
+ $value = null;
+
+ if ( ! empty( $style_value['url'] ) ) {
+ $value = "url('" . $style_value['url'] . "')";
+ } elseif ( is_string( $style_value ) ) {
+ $value = $style_value;
+ }
+
+ if ( null !== $value ) {
+ $css_declarations[ $style_definition['property_keys']['default'] ] = $value;
+ }
+ }
+
+ return $css_declarations;
+ }
+
/**
* Returns compiled CSS from css_declarations.
*
diff --git a/packages/style-engine/src/styles/background/index.ts b/packages/style-engine/src/styles/background/index.ts
new file mode 100644
index 00000000000000..899a3b5b8f0f4c
--- /dev/null
+++ b/packages/style-engine/src/styles/background/index.ts
@@ -0,0 +1,43 @@
+/**
+ * Internal dependencies
+ */
+import type { GeneratedCSSRule, Style, StyleOptions } from '../../types';
+import { safeDecodeURI } from '../utils';
+
+const backgroundImage = {
+ name: 'backgroundImage',
+ generate: ( style: Style, options: StyleOptions ) => {
+ const _backgroundImage = style?.background?.backgroundImage;
+ const _backgroundSize = style?.background?.backgroundSize;
+
+ const styleRules: GeneratedCSSRule[] = [];
+
+ if ( ! _backgroundImage ) {
+ return styleRules;
+ }
+
+ if ( _backgroundImage?.source === 'file' && _backgroundImage?.url ) {
+ styleRules.push( {
+ selector: options.selector,
+ key: 'backgroundImage',
+ // Passed `url` may already be encoded. To prevent double encoding, decodeURI is executed to revert to the original string.
+ value: `url( '${ encodeURI(
+ safeDecodeURI( _backgroundImage.url )
+ ) }' )`,
+ } );
+ }
+
+ // If no background size is set, but an image is, default to cover.
+ if ( ! _backgroundSize ) {
+ styleRules.push( {
+ selector: options.selector,
+ key: 'backgroundSize',
+ value: 'cover',
+ } );
+ }
+
+ return styleRules;
+ },
+};
+
+export default [ backgroundImage ];
diff --git a/packages/style-engine/src/styles/index.ts b/packages/style-engine/src/styles/index.ts
index 5dc1b8743e2ab4..f59935ee3fc571 100644
--- a/packages/style-engine/src/styles/index.ts
+++ b/packages/style-engine/src/styles/index.ts
@@ -4,6 +4,7 @@
import border from './border';
import color from './color';
import dimensions from './dimensions';
+import background from './background';
import shadow from './shadow';
import outline from './outline';
import spacing from './spacing';
@@ -17,4 +18,5 @@ export const styleDefinitions = [
...spacing,
...typography,
...shadow,
+ ...background,
];
diff --git a/packages/style-engine/src/styles/utils.ts b/packages/style-engine/src/styles/utils.ts
index 06f278cb7c1287..00e9dab8b5892b 100644
--- a/packages/style-engine/src/styles/utils.ts
+++ b/packages/style-engine/src/styles/utils.ts
@@ -180,3 +180,24 @@ export function camelCaseJoin( strings: string[] ): string {
const [ firstItem, ...rest ] = strings;
return firstItem.toLowerCase() + rest.map( upperFirst ).join( '' );
}
+
+/**
+ * Safely decodes a URI with `decodeURI`. Returns the URI unmodified if
+ * `decodeURI` throws an error.
+ *
+ * @param {string} uri URI to decode.
+ *
+ * @example
+ * ```js
+ * const badUri = safeDecodeURI( '%z' ); // does not throw an Error, simply returns '%z'
+ * ```
+ *
+ * @return {string} Decoded URI if possible.
+ */
+export function safeDecodeURI( uri: string ): string {
+ try {
+ return decodeURI( uri );
+ } catch ( uriError ) {
+ return uri;
+ }
+}
diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts
index 4b6a9aa72257ed..f09f2c213eabb9 100644
--- a/packages/style-engine/src/types.ts
+++ b/packages/style-engine/src/types.ts
@@ -21,6 +21,13 @@ export interface BorderIndividualStyles< T extends BoxEdge > {
}
export interface Style {
+ background?: {
+ backgroundImage: {
+ url?: CSSProperties[ 'backgroundImage' ];
+ source?: string;
+ };
+ backgroundSize?: CSSProperties[ 'backgroundSize' ];
+ };
border?: {
color?: CSSProperties[ 'borderColor' ];
radius?:
diff --git a/phpunit/style-engine/style-engine-test.php b/phpunit/style-engine/style-engine-test.php
index fae95b995ee44d..7e71deb0dbf96a 100644
--- a/phpunit/style-engine/style-engine-test.php
+++ b/phpunit/style-engine/style-engine-test.php
@@ -493,6 +493,25 @@ public function data_wp_style_engine_get_styles() {
),
),
),
+
+ 'inline_background_image_url_with_background_size' => array(
+ 'block_styles' => array(
+ 'background' => array(
+ 'backgroundImage' => array(
+ 'url' => 'https://example.com/image.jpg',
+ ),
+ 'backgroundSize' => 'cover',
+ ),
+ ),
+ 'options' => array(),
+ 'expected_output' => array(
+ 'css' => "background-image:url('https://example.com/image.jpg');background-size:cover;",
+ 'declarations' => array(
+ 'background-image' => "url('https://example.com/image.jpg')",
+ 'background-size' => 'cover',
+ ),
+ ),
+ ),
);
}
diff --git a/schemas/json/block.json b/schemas/json/block.json
index 41434f58e4727b..f20fb5b0dea972 100644
--- a/schemas/json/block.json
+++ b/schemas/json/block.json
@@ -258,6 +258,17 @@
"description": "ARIA-labels let you define an accessible label for elements. This property allows enabling the definition of an aria-label for the block, without exposing a UI field.",
"default": false
},
+ "background": {
+ "type": "object",
+ "description": "This value signals that a block supports some of the properties related to background, especially background image. When it does, the block editor will show UI controls for the user to set their values.\n\nWhen the block declares support for a specific background property, its attributes definition is extended to include the style attribute.",
+ "properties": {
+ "backgroundImage": {
+ "type": "boolean",
+ "description": "Exposes a control that allows setting a background image. Background size defaults to `cover`.",
+ "default": false
+ }
+ }
+ },
"behaviors": {
"type": "object",
"description": "Behaviors are a way to add additional functionality to a block. They are defined as an object with a name and a set of properties. Curently, only one behavior is supported: lightbox.",
diff --git a/schemas/json/theme.json b/schemas/json/theme.json
index 8cdf2ba520f7ab..f7b3fdb819fc22 100644
--- a/schemas/json/theme.json
+++ b/schemas/json/theme.json
@@ -43,7 +43,7 @@
"type": "object",
"properties": {
"appearanceTools": {
- "description": "Setting that enables the following UI tools:\n\n- border: color, radius, style, width\n- color: link\n- dimensions: minHeight\n- position: sticky\n- spacing: blockGap, margin, padding\n- typography: lineHeight",
+ "description": "Setting that enables the following UI tools:\n\n- background: backgroundImage\n- border: color, radius, style, width\n- color: link\n- dimensions: minHeight\n- position: sticky\n- spacing: blockGap, margin, padding\n- typography: lineHeight",
"type": "boolean",
"default": false
}
@@ -260,6 +260,23 @@
}
}
},
+ "settingsPropertiesBackground": {
+ "type": "object",
+ "properties": {
+ "background": {
+ "description": "Settings related to background.",
+ "type": "object",
+ "properties": {
+ "backgroundImage": {
+ "description": "Allow users to set a background image.",
+ "type": "boolean",
+ "default": false
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
"settingsPropertiesDimensions": {
"type": "object",
"properties": {
@@ -705,6 +722,7 @@
"settingsProperties": {
"allOf": [
{ "$ref": "#/definitions/settingsPropertiesAppearanceTools" },
+ { "$ref": "#/definitions/settingsPropertiesBackground" },
{ "$ref": "#/definitions/settingsPropertiesBehaviors" },
{ "$ref": "#/definitions/settingsPropertiesBorder" },
{ "$ref": "#/definitions/settingsPropertiesColor" },
@@ -726,6 +744,7 @@
{
"properties": {
"appearanceTools": {},
+ "background": {},
"behaviors": {},
"border": {},
"color": {},
@@ -2212,6 +2231,7 @@
"useRootPaddingAwareAlignments": {
"$ref": "#/definitions/settingsPropertiesUseRootPaddingAwareAlignments/properties/useRootPaddingAwareAlignments"
},
+ "background": {},
"color": {},
"layout": {},
"spacing": {},