Skip to content

Commit

Permalink
Background image: Add backgroundSize and repeat features (#57005)
Browse files Browse the repository at this point in the history
* Background image block support: Add backgroundSize feature and controls

* Rename control to Repeat

* Allow setting a fixed size

* Add default controls support

* Allow empty Size to be treated as auto

* Change helptext to refer to width instead of size

* Add background-repeat option

* Rename toggle to Fixed

* Try combining the repeat image toggle with the size controls

* Fix whitespace

* Tidy up resets

* Hide repeat toggle when using cover, ensure updating fixed size doesn't change repeat value
  • Loading branch information
andrewserong authored Dec 22, 2023
1 parent 1df4fd6 commit 071082c
Show file tree
Hide file tree
Showing 13 changed files with 360 additions and 14 deletions.
2 changes: 1 addition & 1 deletion docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ Gather blocks in a layout container. ([Source](https://github.com/WordPress/gute

- **Name:** core/group
- **Category:** design
- **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~~
- **Supports:** align (full, wide), anchor, ariaLabel, background (backgroundImage, backgroundSize), 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
Expand Down
13 changes: 11 additions & 2 deletions lib/block-supports/background.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ function gutenberg_render_background_support( $block_content, $block ) {
$background_image_source = $block_attributes['style']['background']['backgroundImage']['source'] ?? null;
$background_image_url = $block_attributes['style']['background']['backgroundImage']['url'] ?? null;
$background_size = $block_attributes['style']['background']['backgroundSize'] ?? 'cover';
$background_position = $block_attributes['style']['background']['backgroundPosition'] ?? null;
$background_repeat = $block_attributes['style']['background']['backgroundRepeat'] ?? null;

$background_block_styles = array();

Expand All @@ -64,8 +66,15 @@ function gutenberg_render_background_support( $block_content, $block ) {
// 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;
// Only output the background size and repeat when an image url is set.
$background_block_styles['backgroundSize'] = $background_size;
$background_block_styles['backgroundRepeat'] = $background_repeat;
$background_block_styles['backgroundPosition'] = $background_position;

// If the background size is set to `contain` and no position is set, set the position to `center`.
if ( 'contain' === $background_size && ! isset( $background_position ) ) {
$background_block_styles['backgroundPosition'] = 'center';
}
}

$styles = gutenberg_style_engine_get_styles( array( 'background' => $background_block_styles ) );
Expand Down
2 changes: 2 additions & 0 deletions lib/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ class WP_Theme_JSON_Gutenberg {
'useRootPaddingAwareAlignments' => null,
'background' => array(
'backgroundImage' => null,
'backgroundSize' => null,
),
'border' => array(
'color' => null,
Expand Down Expand Up @@ -650,6 +651,7 @@ public static function get_element_class_name( $element ) {
*/
const APPEARANCE_TOOLS_OPT_INS = array(
array( 'background', 'backgroundImage' ),
array( 'background', 'backgroundSize' ),
array( 'border', 'color' ),
array( 'border', 'radius' ),
array( 'border', 'style' ),
Expand Down
18 changes: 18 additions & 0 deletions lib/compat/wordpress-6.5/kses.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
/**
* Temporary compatibility shims for block APIs present in Gutenberg.
*
* @package gutenberg
*/

/**
* Update allowed inline style attributes list.
*
* @param string[] $attrs Array of allowed CSS attributes.
* @return string[] CSS attributes.
*/
function gutenberg_safe_style_attrs_6_5( $attrs ) {
$attrs[] = 'background-repeat';
return $attrs;
}
add_filter( 'safe_style_css', 'gutenberg_safe_style_attrs_6_5' );
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ function gutenberg_is_experiment_enabled( $name ) {
// WordPress 6.5 compat.
require __DIR__ . '/compat/wordpress-6.5/block-patterns.php';
require __DIR__ . '/compat/wordpress-6.5/class-wp-navigation-block-renderer.php';
require __DIR__ . '/compat/wordpress-6.5/kses.php';

// Experimental features.
require __DIR__ . '/experimental/block-editor-settings-mobile.php';
Expand Down
3 changes: 3 additions & 0 deletions packages/block-editor/src/components/global-styles/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const EMPTY_CONFIG = { settings: {}, styles: {} };
const VALID_SETTINGS = [
'appearanceTools',
'useRootPaddingAwareAlignments',
'background.backgroundImage',
'background.backgroundRepeat',
'background.backgroundSize',
'border.color',
'border.radius',
'border.style',
Expand Down
236 changes: 231 additions & 5 deletions packages/block-editor/src/hooks/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ 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,
MenuItem,
Expand Down Expand Up @@ -52,6 +57,17 @@ export function hasBackgroundImageValue( style ) {
return hasValue;
}

/**
* Checks if there is a current value in the background size block support
* attributes.
*
* @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?.backgroundSize !== undefined;
}

/**
* Determine whether there is block support for background.
*
Expand All @@ -72,7 +88,11 @@ export function hasBackgroundSupport( blockName, feature = 'any' ) {
}

if ( feature === 'any' ) {
return !! support?.backgroundImage;
return (
!! support?.backgroundImage ||
!! support?.backgroundSize ||
!! support?.backgroundRepeat
);
}

return !! support?.[ feature ];
Expand All @@ -97,6 +117,26 @@ export function resetBackgroundImage( style = {}, setAttributes ) {
} );
}

/**
* 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,
backgroundRepeat: undefined,
backgroundSize: undefined,
},
} ),
} );
}

function InspectorImagePreview( { label, filename, url: imgUrl } ) {
const imgLabel = label || getFilename( imgUrl );
return (
Expand Down Expand Up @@ -142,7 +182,11 @@ function InspectorImagePreview( { label, filename, url: imgUrl } ) {
);
}

function BackgroundImagePanelItem( { clientId, setAttributes } ) {
function BackgroundImagePanelItem( {
clientId,
isShownByDefault,
setAttributes,
} ) {
const { style, mediaUpload } = useSelect(
( select ) => {
const { getBlockAttributes, getSettings } =
Expand Down Expand Up @@ -252,7 +296,7 @@ function BackgroundImagePanelItem( { clientId, setAttributes } ) {
hasValue={ () => hasValue }
label={ __( 'Background image' ) }
onDeselect={ () => resetBackgroundImage( style, setAttributes ) }
isShownByDefault={ true }
isShownByDefault={ isShownByDefault }
resetAllFilter={ resetAllFilter }
panelId={ clientId }
>
Expand Down Expand Up @@ -302,18 +346,200 @@ function BackgroundImagePanelItem( { clientId, setAttributes } ) {
);
}

function backgroundSizeHelpText( value ) {
if ( value === 'cover' || value === undefined ) {
return __( 'Stretch image to cover the block.' );
}
if ( value === 'contain' ) {
return __( 'Resize image to fit without cropping.' );
}
return __( 'Set a fixed width.' );
}

function BackgroundSizePanelItem( {
clientId,
isShownByDefault,
setAttributes,
} ) {
const style = useSelect(
( select ) =>
select( blockEditorStore ).getBlockAttributes( clientId )?.style,
[ 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 )
? false
: true;

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 toggleIsRepeated = () => {
setAttributes( {
style: cleanEmptyObject( {
...style,
background: {
...style?.background,
backgroundRepeat:
repeatCheckedValue === true ? 'no-repeat' : undefined,
},
} ),
} );
};

return (
<VStack
as={ ToolsPanelItem }
spacing={ 2 }
className="single-column"
hasValue={ () => hasValue }
label={ __( 'Size' ) }
onDeselect={ () => resetBackgroundSize( style, setAttributes ) }
isShownByDefault={ isShownByDefault }
resetAllFilter={ resetAllFilter }
panelId={ clientId }
>
<ToggleGroupControl
__nextHasNoMarginBottom
size={ '__unstable-large' }
label={ __( 'Size' ) }
value={ currentValueForToggle }
onChange={ updateBackgroundSize }
isBlock={ true }
help={ backgroundSizeHelpText( sizeValue ) }
>
<ToggleGroupControlOption
key={ 'cover' }
value={ 'cover' }
label={ __( 'Cover' ) }
/>
<ToggleGroupControlOption
key={ 'contain' }
value={ 'contain' }
label={ __( 'Contain' ) }
/>
<ToggleGroupControlOption
key={ 'fixed' }
value={ 'auto' }
label={ __( 'Fixed' ) }
/>
</ToggleGroupControl>
{ sizeValue !== undefined &&
sizeValue !== 'cover' &&
sizeValue !== 'contain' ? (
<UnitControl
size={ '__unstable-large' }
onChange={ updateBackgroundSize }
value={ sizeValue }
/>
) : null }
{ currentValueForToggle !== 'cover' && (
<ToggleControl
__nextHasNoMarginBottom
label={ __( 'Repeat image' ) }
checked={ repeatCheckedValue }
onChange={ toggleIsRepeated }
/>
) }
</VStack>
);
}

export function BackgroundImagePanel( props ) {
const [ backgroundImage ] = useSettings( 'background.backgroundImage' );
const [ backgroundImage, backgroundSize ] = useSettings(
'background.backgroundImage',
'background.backgroundSize'
);

if (
! backgroundImage ||
! hasBackgroundSupport( props.name, 'backgroundImage' )
) {
return null;
}

const showBackgroundSize = !! (
backgroundSize && hasBackgroundSupport( props.name, 'backgroundSize' )
);

const defaultControls = getBlockSupport( props.name, [
BACKGROUND_SUPPORT_KEY,
'__experimentalDefaultControls',
] );

return (
<InspectorControls group="background">
<BackgroundImagePanelItem { ...props } />
<BackgroundImagePanelItem
isShownByDefault={ defaultControls?.backgroundImage }
{ ...props }
/>
{ showBackgroundSize && (
<BackgroundSizePanelItem
isShownByDefault={ defaultControls?.backgroundSize }
{ ...props }
/>
) }
</InspectorControls>
);
}
Loading

1 comment on commit 071082c

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in 071082c.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/7294636961
📝 Reported issues:

Please sign in to comment.