Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/block-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
- Background block support: Fix gradients not being applied to a block when a theme opts out of `settings.background.gradient` in `theme.json` ([#81056](https://github.com/WordPress/gutenberg/pull/81056)).
- `LinkControl`: Restore the preview title underline by slightly increasing the title's line height, which was too tight for the underline to be visible ([#81083](https://github.com/WordPress/gutenberg/pull/81083)).
- `URLInput`: Skip link search requests while an IME composition is in progress; the search now fires once with the confirmed value on `compositionend` ([#80602](https://github.com/WordPress/gutenberg/pull/80602)).
- Style states: Allow unsetting a background image in a viewport or pseudo state by persisting `background-image: none` and exposing the default-state image as inherited ([#81448](https://github.com/WordPress/gutenberg/pull/81448)).

## 16.1.0 (2026-07-29)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,12 @@ export default function BackgroundImagePanel( {
settings,
defaultValues = {},
showInheritanceLabelIndicators = isGlobalStylesInheritanceEnabled(),
/**
* When true, media Reset writes an explicit `backgroundImage: 'none'`
* instead of clearing the value. Needed for viewport/pseudo style states
* so the default-state image does not return through the cascade.
*/
persistImageUnsetOnReset = false,
} ) {
/*
* Resolve inherited `ref` pointers for background controls.
Expand Down Expand Up @@ -692,19 +698,39 @@ export default function BackgroundImagePanel( {
return resolvedValues;
}, [ globalStyles, _links, inheritedValue ] );

const resetBackground = () =>
// Clear local override so the inherited/default-state image returns.
const clearBackgroundImage = () =>
onChange(
setImmutably( value, [ 'background' ], {
gradient: value?.background?.gradient,
} )
);

const { title, url } = value?.background?.backgroundImage || {
...resolvedInheritedValue?.background?.backgroundImage,
};
// Explicitly unset for the current style state.
const unsetBackgroundImage = () =>
onChange(
setImmutably( value, [ 'background' ], {
gradient: value?.background?.gradient,
backgroundImage: 'none',
} )
);

const resetMediaImage = persistImageUnsetOnReset
? unsetBackgroundImage
: clearBackgroundImage;

const localBackgroundImage = value?.background?.backgroundImage;
const hasExplicitImageUnset = localBackgroundImage === 'none';
const { title, url } = hasExplicitImageUnset
? {}
: localBackgroundImage || {
...resolvedInheritedValue?.background?.backgroundImage,
};
const localHasImageValue = hasBackgroundImageValue( value );
const hasImageValue =
localHasImageValue || hasBackgroundImageValue( resolvedInheritedValue );
! hasExplicitImageUnset &&
( localHasImageValue ||
hasBackgroundImageValue( resolvedInheritedValue ) );
// The blue-dot local-override affordance is part of the inherited-value
// treatment. When that treatment is disabled (e.g. in the Global Styles
// panel, where the edited value *is* the global style rather than a local
Expand All @@ -714,9 +740,9 @@ export default function BackgroundImagePanel( {
localHasImageValue &&
hasBackgroundImageValue( resolvedInheritedValue );

const imageValue =
value?.background?.backgroundImage ||
inheritedValue?.background?.backgroundImage;
const imageValue = hasExplicitImageUnset
? 'none'
: localBackgroundImage || inheritedValue?.background?.backgroundImage;

const shouldShowBackgroundImageControls =
hasImageValue &&
Expand Down Expand Up @@ -746,7 +772,9 @@ export default function BackgroundImagePanel( {
onToggle={ setIsDropDownOpen }
hasImageValue={ hasImageValue }
hasLocalOverride={ hasLocalOverride }
onReset={ localHasImageValue ? resetBackground : undefined }
onReset={
localHasImageValue ? clearBackgroundImage : undefined
}
containerRef={ containerRef }
>
<VStack spacing={ 3 } className="single-column">
Expand All @@ -757,7 +785,7 @@ export default function BackgroundImagePanel( {
displayInPanel
onResetImage={ () => {
setIsDropDownOpen( false );
resetBackground();
resetMediaImage();
} }
onRemoveImage={ () => setIsDropDownOpen( false ) }
defaultValues={ defaultValues }
Expand All @@ -774,12 +802,24 @@ export default function BackgroundImagePanel( {
) : (
<BackgroundImageControls
onChange={ onChange }
style={ value }
inheritedValue={ resolvedInheritedValue }
style={
hasExplicitImageUnset
? setImmutably(
value,
[ 'background', 'backgroundImage' ],
undefined
)
: value
}
inheritedValue={
hasExplicitImageUnset
? undefined
: resolvedInheritedValue
}
defaultValues={ defaultValues }
onResetImage={ () => {
setIsDropDownOpen( false );
resetBackground();
resetMediaImage();
} }
onRemoveImage={ () => setIsDropDownOpen( false ) }
containerRef={ containerRef }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import {
InheritanceToolsPanelItem,
isGlobalStylesInheritanceEnabled,
} from './inheritance';
import {
DEFAULT_BLOCK_STYLE_STATE,
isDefaultBlockStyleState,
} from '../../hooks/block-style-state';

const DEFAULT_CONTROLS = {
backgroundImage: true,
Expand Down Expand Up @@ -164,7 +168,9 @@ export default function BackgroundImagePanel( {
headerLabel = __( 'Background' ),
contrastWarning,
showInheritanceLabelIndicators = isGlobalStylesInheritanceEnabled(),
styleState = DEFAULT_BLOCK_STYLE_STATE,
} ) {
const isStyleStateSelected = ! isDefaultBlockStyleState( styleState );
const {
colors,
gradients,
Expand Down Expand Up @@ -207,12 +213,17 @@ export default function BackgroundImagePanel( {
const clearsColorBackground = showBackgroundColorControl;
const clearsColorGradient =
hasBackgroundGradientControl || showLegacyColorGradientControl;
// Persist an explicit unset under a style state so the default-state
// background image does not return through the cascade.
const background = isStyleStateSelected
? { backgroundImage: 'none' }
: {};
if ( ! clearsColorBackground && ! clearsColorGradient ) {
return { ...previousValue, background: {} };
return { ...previousValue, background };
}
return {
...previousValue,
background: {},
background,
color: {
...previousValue?.color,
...( clearsColorBackground && { background: undefined } ),
Expand All @@ -222,6 +233,7 @@ export default function BackgroundImagePanel( {
},
[
hasBackgroundGradientControl,
isStyleStateSelected,
showBackgroundColorControl,
showLegacyColorGradientControl,
]
Expand All @@ -241,7 +253,7 @@ export default function BackgroundImagePanel( {
setImmutably(
value,
[ 'background', 'backgroundImage' ],
undefined
isStyleStateSelected ? 'none' : undefined
)
);

Expand Down Expand Up @@ -373,6 +385,7 @@ export default function BackgroundImagePanel( {
showInheritanceLabelIndicators={
showInheritanceLabelIndicators
}
persistImageUnsetOnReset={ isStyleStateSelected }
/>
</InheritanceToolsPanelItem>
) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,65 @@ describe( 'hasBackgroundImageValue', () => {
} )
).toBe( false );
} );

it( 'should return `true` for an explicit none unset value', () => {
expect(
hasBackgroundImageValue( {
background: { backgroundImage: 'none' },
} )
).toBe( true );
} );
} );

describe( 'BackgroundPanel — style state background image reset', () => {
it( 'writes background-image none when deselecting under a viewport state', async () => {
const user = userEvent.setup();
const onChange = jest.fn();

render(
<BackgroundPanel
value={ {
background: {
backgroundImage: {
id: 2,
url: 'http://example.com/mobile.jpg',
},
},
} }
inheritedValue={ {
background: {
backgroundImage: {
id: 1,
url: 'http://example.com/desktop.jpg',
},
},
} }
settings={ {
background: {
backgroundImage: true,
},
} }
onChange={ onChange }
panelId="test-panel"
styleState={ { viewport: '@mobile', pseudo: 'default' } }
/>
);

await user.click(
screen.getByRole( 'button', { name: /Background options/i } )
);
await user.click(
screen.getByRole( 'menuitem', { name: /Reset Image/i } )
);

expect( onChange ).toHaveBeenCalledWith(
expect.objectContaining( {
background: expect.objectContaining( {
backgroundImage: 'none',
} ),
} )
);
} );
} );

describe( 'hasBackgroundGradientValue', () => {
Expand Down
19 changes: 17 additions & 2 deletions packages/block-editor/src/components/inspector-controls/fill.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import warning from '@wordpress/warning';
import deprecated from '@wordpress/deprecated';
import { useEffect, useContext, useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import {
useBlockEditContext,
mayDisplayControlsKey,
Expand All @@ -13,10 +14,12 @@ import {
} from '../block-edit/context';
import groups from './groups';
import {
DEFAULT_BLOCK_STYLE_STATE,
scopeResetAllFilterToState,
useBlockStyleState,
} from '../../hooks/block-style-state';
import { ListViewContentFill } from './list-view-content-popover';
import { store as blockEditorStore } from '../../store';
import { unlock } from '../../lock-unlock';

const PATTERN_EDITING_GROUPS = [ 'content', 'list' ];

Expand Down Expand Up @@ -104,7 +107,19 @@ export default function InspectorControlsFill( {
function RegisterResetAll( { resetAllFilter, children } ) {
const { registerResetAllFilter, deregisterResetAllFilter } =
useContext( ToolsPanelContext );
const selectedState = useBlockStyleState();
// Read from the store rather than BlockStyleState context. Fill content is
// portaled into the inspector slot, and relying on context here can miss
// the selected viewport — unscoped resets then clear default-state styles.
const selectedState = useSelect( ( select ) => {
const { getSelectedBlockClientId } = select( blockEditorStore );
const { getSelectedBlockStyleState } = unlock(
select( blockEditorStore )
);
const clientId = getSelectedBlockClientId();
return clientId
? getSelectedBlockStyleState( clientId )
: DEFAULT_BLOCK_STYLE_STATE;
}, [] );
const scopedResetAllFilter = useMemo(
() => scopeResetAllFilterToState( selectedState, resetAllFilter ),
[ resetAllFilter, selectedState ]
Expand Down
Loading
Loading