Skip to content
Merged
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
3 changes: 3 additions & 0 deletions backport-changelog/7.1/12489.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/12489

* https://github.com/WordPress/gutenberg/pull/80037
12 changes: 9 additions & 3 deletions lib/block-supports/states.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,18 +344,24 @@ function gutenberg_add_block_state_style_rule( &$css_rules, $state, $selector, $

$style = gutenberg_get_state_style_with_fallback_dimension_styles( $style );

$compiled = gutenberg_style_engine_get_styles(
$compiled = gutenberg_style_engine_get_styles(
gutenberg_normalize_state_style_for_css_output( $style )
);
$declarations = $compiled['declarations'] ?? array();
$text_align = $style['typography']['textAlign'] ?? null;
// Base text alignment is class-based, so state styles need a declaration.
if ( is_string( $text_align ) && '' !== trim( $text_align ) ) {
$declarations['text-align'] = $text_align;
}

if ( empty( $compiled['declarations'] ) ) {
if ( empty( $declarations ) ) {
return;
}

$css_rules[] = array(
'state' => $state,
'selector' => $selector,
'declarations' => $compiled['declarations'],
'declarations' => $declarations,
);
if ( ! empty( $rules_group ) ) {
$css_rules[ count( $css_rules ) - 1 ]['rules_group'] = $rules_group;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ const BlockControlsBlock = createSlotFill( 'BlockControlsBlock' );
const BlockControlsInline = createSlotFill( 'BlockFormatControls' );
const BlockControlsOther = createSlotFill( 'BlockControlsOther' );
const BlockControlsParent = createSlotFill( 'BlockControlsParent' );
const BlockControlsStyleState = createSlotFill( 'BlockControlsStyleState' );

const groups = {
default: BlockControlsDefault,
block: BlockControlsBlock,
inline: BlockControlsInline,
other: BlockControlsOther,
parent: BlockControlsParent,
'style-state': BlockControlsStyleState,
};

export default groups;
49 changes: 32 additions & 17 deletions packages/block-editor/src/components/block-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function PrivateBlockToolbar( {
showBlockVisibilityButton,
showSwitchSectionStyleButton,
areSelectedBlocksHiddenOnViewport,
showStyleStateSlot,
canEdit,
} = useSelect( ( select ) => {
const { canEditBlock } = select( blockEditorStore );
Expand Down Expand Up @@ -167,6 +168,7 @@ export function PrivateBlockToolbar( {
hasContentOnlyLocking: _hasTemplateLock,
showShuffleButton: _isZoomOut,
showSlots: ! _isZoomOut && ! _isEditingResponsiveStyleState,
showStyleStateSlot: ! _isZoomOut && _isEditingResponsiveStyleState,
showGroupButtons: ! _isZoomOut,
showLockButtons: ! _isZoomOut,
showBlockVisibilityButton: ! _isZoomOut,
Expand Down Expand Up @@ -269,31 +271,44 @@ export function PrivateBlockToolbar( {
<SwitchSectionStyle clientId={ blockClientIds[ 0 ] } />
) }
{ ! areSelectedBlocksHiddenOnViewport &&
shouldShowVisualToolbar &&
showSlots && (
shouldShowVisualToolbar && (
<>
{ ! isSectionContainer && (
<>
{ showSlots && (
<>
<BlockControls.Slot
group="parent"
className="block-editor-block-toolbar__slot"
/>
<BlockControls.Slot
group="block"
className="block-editor-block-toolbar__slot"
/>
<BlockControls.Slot className="block-editor-block-toolbar__slot" />
<BlockControls.Slot
group="inline"
className="block-editor-block-toolbar__slot"
/>
</>
) }
{ showStyleStateSlot && (
<BlockControls.Slot
group="style-state"
className="block-editor-block-toolbar__slot"
/>
) }
</>
) }
{ showSlots && (
<>
<BlockControls.Slot
group="parent"
className="block-editor-block-toolbar__slot"
/>
<BlockControls.Slot
group="block"
className="block-editor-block-toolbar__slot"
/>
<BlockControls.Slot className="block-editor-block-toolbar__slot" />
<BlockControls.Slot
group="inline"
group="other"
className="block-editor-block-toolbar__slot"
/>
<__unstableBlockToolbarLastItem.Slot />
</>
) }
<BlockControls.Slot
group="other"
className="block-editor-block-toolbar__slot"
/>
<__unstableBlockToolbarLastItem.Slot />
</>
) }
<BlockEditVisuallyButton clientIds={ blockClientIds } />
Expand Down
58 changes: 35 additions & 23 deletions packages/block-editor/src/hooks/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,29 +301,34 @@ function LayoutPanelPure( {
const settings = useBlockSettings( blockName );
// Block settings come from theme.json under settings.[blockName].
const { layout: layoutSettings } = settings;
const { themeSupportsLayout, activeBlockVariation, selectedState } =
useSelect(
( select ) => {
const blockEditorSelect = select( blockEditorStore );
const { getBlockAttributes, getSettings } = blockEditorSelect;
const { getSelectedBlockStyleState } =
unlock( blockEditorSelect );
return {
activeBlockVariation: select(
blocksStore
).getActiveBlockVariation(
blockName,
getBlockAttributes( clientId ) || {},
'block'
),
themeSupportsLayout: getSettings().supportsLayout,
selectedState:
getSelectedBlockStyleState?.( clientId ) ??
DEFAULT_BLOCK_STYLE_STATE,
};
},
[ blockName, clientId ]
);
const {
themeSupportsLayout,
activeBlockVariation,
selectedState,
isResponsiveEditing,
} = useSelect(
( select ) => {
const blockEditorSelect = select( blockEditorStore );
const { getBlockAttributes, getSettings } = blockEditorSelect;
const {
getSelectedBlockStyleState,
isResponsiveEditing: getIsResponsiveEditing,
} = unlock( blockEditorSelect );
Comment on lines +311 to +316

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry, this caught my eye, and I couldn't help myself, so here's a minor follow-up #80176.

return {
activeBlockVariation: select(
blocksStore
).getActiveBlockVariation(
blockName,
getBlockAttributes( clientId ) || {},
'block'
),
themeSupportsLayout: getSettings().supportsLayout,
selectedState: getSelectedBlockStyleState( clientId ),
isResponsiveEditing: getIsResponsiveEditing(),
};
},
[ blockName, clientId ]
);

const blockEditingMode = useBlockEditingMode();
const isViewportLayoutState =
Expand Down Expand Up @@ -573,6 +578,13 @@ function LayoutPanelPure( {
layout={ usedLayout }
onChange={ onChangeLayout }
layoutBlockSupport={ layoutBlockSupport }
controlsGroup={
isResponsiveEditing &&
hasViewportBlockStyleState( selectedState ) &&
! hasPseudoBlockStyleState( selectedState )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Layout doesn't support pseudo states (it hasn't had to so far because no block with pseudo states has layout support; this may change in the future of course)

@talldan talldan Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should be a little bit careful about these clauses, as when other style states are introduced (e.g. custom/current), the dev possibly has to remember to add && ! hasCustomBlockStyleState( selectedState ) in multiple places. I think there are a couple of other places that have this same condition.

Possibly we should have a selector hasOnlyViewportStyleState or similar. Or we have some way of declaring which features support which types of style state.

Not urgent though, I think we might want to tidy some of these things up post 7.1 to avoid refactors / merge conflicts right now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

While for pseudo states (and for "current" I guess) we can be fairly sure we don't need layout controls, if other custom states are introduced some of them might require layout support. Probably better to reexamine this later 😄

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good point, some declarative way of opting in per support probably makes more sense then.

? 'style-state'
: 'block'
}
name={ blockName }
clientId={ clientId }
/>
Expand Down
35 changes: 33 additions & 2 deletions packages/block-editor/src/hooks/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import {
__EXPERIMENTAL_ELEMENTS as ELEMENTS,
} from '@wordpress/blocks';
import { useInstanceId } from '@wordpress/compose';
import { getCSSRules, compileCSS } from '@wordpress/style-engine';
import {
getCSSRules,
compileCSS,
getCSSValueFromRawStyle,
} from '@wordpress/style-engine';

/**
* Internal dependencies
Expand Down Expand Up @@ -201,6 +205,32 @@ function getStateFallbackDimensionStyles( stateStyles ) {
}
}

/**
* Returns CSS for text alignment in a block instance state style object.
*
* Text alignment is not generated by the style engine because base block
* alignment is serialized as a class name. State styles need a declaration
* instead so viewport and pseudo-state values can override the default class.
*
* @param {Object} stateStyles State style object.
* @param {string} selector CSS selector for the generated style.
* @return {string|undefined} CSS string with the text alignment declaration.
*/
function getStateTextAlignCSS( stateStyles, selector ) {
const textAlign = stateStyles?.typography?.textAlign;

if ( typeof textAlign !== 'string' || ! textAlign ) {
return undefined;
}
Comment on lines +222 to +224

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is such a tiny nit, and likely doesn't even matter because I don't think textAlign will ever be a string with empty spaces, but in the PHP code we trim the string. Worth doing here?

(If you're not making any other changes to the PR, don't worry about it)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it might be that we can remove the trim from the PHP instead 😄
I'll merge this for now and follow up later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Definitely not a blocker!!


const declaration = `text-align: ${ getCSSValueFromRawStyle(
textAlign
) } !important`;
return selector
? `${ selector } { ${ declaration }; }`
: `${ declaration };`;
}

/**
* Generates CSS for a block instance state style object.
*
Expand All @@ -224,12 +254,13 @@ export function getStateStylesCSS( stateStyles, selector ) {
const fallbackCSS = fallbackBorderStyles
? compileCSS( fallbackBorderStyles, { selector } )
: undefined;
const textAlignCSS = getStateTextAlignCSS( stateStyles, selector );
const backgroundResetCSS = getStateBackgroundResetCSS(
stateStyles,
selector
);

return [ importantCSS, fallbackCSS, backgroundResetCSS ]
return [ importantCSS, textAlignCSS, fallbackCSS, backgroundResetCSS ]
.filter( Boolean )
.join( '\n' );
}
Expand Down
29 changes: 29 additions & 0 deletions packages/block-editor/src/hooks/test/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,19 @@ describe( 'getStateStylesCSS', () => {
'.wp-block-test { height: 20rem !important; aspect-ratio: unset !important; }'
);
} );

it( 'adds important text alignment declarations for state styles', () => {
expect(
getStateStylesCSS(
{
typography: {
textAlign: 'center',
},
},
'.wp-block-test'
)
).toBe( '.wp-block-test { text-align: center !important; }' );
} );
} );

describe( 'getBlockStateStylesCSS', () => {
Expand Down Expand Up @@ -414,6 +427,22 @@ describe( 'getResponsiveStateCSSRules', () => {
] );
} );

it( 'generates media-query scoped text alignment styles for viewport states', () => {
expect(
getResponsiveStateCSSRules(
{
'@mobile': {
typography: { textAlign: 'right' },
},
},
'core/paragraph',
'.wp-elements-1'
)
).toEqual( [
'@media (width <= 480px){.wp-elements-1 { text-align: right !important; }}',
] );
} );

it( 'routes viewport styles through feature selectors', () => {
expect(
getResponsiveStateCSSRules(
Expand Down
35 changes: 34 additions & 1 deletion packages/block-editor/src/hooks/test/text-align.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import {
/**
* Internal dependencies
*/
import { getValidTextAlignments, addAssignedTextAlign } from '../text-align';
import {
getValidTextAlignments,
addAssignedTextAlign,
getTextAlignControlGroup,
} from '../text-align';

const noop = () => {};

Expand Down Expand Up @@ -62,6 +66,35 @@ describe( 'textAlign', () => {
} );
} );

describe( 'getTextAlignControlGroup()', () => {
it( 'uses the regular block slot by default', () => {
expect(
getTextAlignControlGroup( false, {
viewport: 'default',
pseudo: 'default',
} )
).toBe( 'block' );
} );

it( 'uses the regular block slot when responsive editing has no viewport state', () => {
expect(
getTextAlignControlGroup( true, {
viewport: 'default',
pseudo: 'default',
} )
).toBe( 'block' );
} );

it( 'uses the style-state slot when responsive editing has a viewport state', () => {
expect(
getTextAlignControlGroup( true, {
viewport: '@mobile',
pseudo: 'default',
} )
).toBe( 'style-state' );
} );
} );

describe( 'addAssignedTextAlign', () => {
it( 'should do nothing if block does not support text align', () => {
registerBlockType( 'core/foo', blockSettings );
Expand Down
Loading
Loading