Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Background image: Add backgroundSize and repeat features #57005

Merged
merged 12 commits into from
Dec 22, 2023
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 @@ -104,6 +104,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 =
Copy link
Member

Choose a reason for hiding this comment

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

Because it's a bool, also possible to do something like:

const repeatCheckedValue = repeatValue !== 'no-repeat' && !(currentValueForToggle === 'cover' && repeatValue === undefined);

Just an observation. No need to change if it works! 😄

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' }
Copy link
Member

Choose a reason for hiding this comment

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

Definitely a 'later' thing, but do you reckon it'd be useful follow up to have width and height controls? E.g., be able to produce values like background-size: 50px 100px;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes — I was imagining in follow-ups we could introduce a split control. I imagine in most cases folks will want to only use a single size value so that the image's proportions are maintained.

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
Loading