diff --git a/backport-changelog/7.1/12489.md b/backport-changelog/7.1/12489.md new file mode 100644 index 00000000000000..cf16320d0fe190 --- /dev/null +++ b/backport-changelog/7.1/12489.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/12489 + +* https://github.com/WordPress/gutenberg/pull/80037 \ No newline at end of file diff --git a/lib/block-supports/states.php b/lib/block-supports/states.php index cae27815655e94..571647cfa7dc40 100644 --- a/lib/block-supports/states.php +++ b/lib/block-supports/states.php @@ -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; diff --git a/packages/block-editor/src/components/block-controls/groups.js b/packages/block-editor/src/components/block-controls/groups.js index 9b9dfec8d8d450..59c523be6b1a2d 100644 --- a/packages/block-editor/src/components/block-controls/groups.js +++ b/packages/block-editor/src/components/block-controls/groups.js @@ -8,6 +8,7 @@ const BlockControlsBlock = createSlotFill( 'BlockControlsBlock' ); const BlockControlsInline = createSlotFill( 'BlockFormatControls' ); const BlockControlsOther = createSlotFill( 'BlockControlsOther' ); const BlockControlsParent = createSlotFill( 'BlockControlsParent' ); +const BlockControlsStyleState = createSlotFill( 'BlockControlsStyleState' ); const groups = { default: BlockControlsDefault, @@ -15,6 +16,7 @@ const groups = { inline: BlockControlsInline, other: BlockControlsOther, parent: BlockControlsParent, + 'style-state': BlockControlsStyleState, }; export default groups; diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 632b085e79f73b..fa0d78c17cb14e 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -79,6 +79,7 @@ export function PrivateBlockToolbar( { showBlockVisibilityButton, showSwitchSectionStyleButton, areSelectedBlocksHiddenOnViewport, + showStyleStateSlot, canEdit, } = useSelect( ( select ) => { const { canEditBlock } = select( blockEditorStore ); @@ -167,6 +168,7 @@ export function PrivateBlockToolbar( { hasContentOnlyLocking: _hasTemplateLock, showShuffleButton: _isZoomOut, showSlots: ! _isZoomOut && ! _isEditingResponsiveStyleState, + showStyleStateSlot: ! _isZoomOut && _isEditingResponsiveStyleState, showGroupButtons: ! _isZoomOut, showLockButtons: ! _isZoomOut, showBlockVisibilityButton: ! _isZoomOut, @@ -269,31 +271,44 @@ export function PrivateBlockToolbar( { ) } { ! areSelectedBlocksHiddenOnViewport && - shouldShowVisualToolbar && - showSlots && ( + shouldShowVisualToolbar && ( <> { ! isSectionContainer && ( + <> + { showSlots && ( + <> + + + + + + ) } + { showStyleStateSlot && ( + + ) } + + ) } + { showSlots && ( <> - - - + <__unstableBlockToolbarLastItem.Slot /> ) } - - <__unstableBlockToolbarLastItem.Slot /> ) } diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 09737bf8a90104..740c14c685d461 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -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 ); + return { + activeBlockVariation: select( + blocksStore + ).getActiveBlockVariation( + blockName, + getBlockAttributes( clientId ) || {}, + 'block' + ), + themeSupportsLayout: getSettings().supportsLayout, + selectedState: getSelectedBlockStyleState( clientId ), + isResponsiveEditing: getIsResponsiveEditing(), + }; + }, + [ blockName, clientId ] + ); const blockEditingMode = useBlockEditingMode(); const isViewportLayoutState = @@ -573,6 +578,13 @@ function LayoutPanelPure( { layout={ usedLayout } onChange={ onChangeLayout } layoutBlockSupport={ layoutBlockSupport } + controlsGroup={ + isResponsiveEditing && + hasViewportBlockStyleState( selectedState ) && + ! hasPseudoBlockStyleState( selectedState ) + ? 'style-state' + : 'block' + } name={ blockName } clientId={ clientId } /> diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 64186ac7d8e3ff..9311e10b3eebe3 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -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 @@ -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; + } + + const declaration = `text-align: ${ getCSSValueFromRawStyle( + textAlign + ) } !important`; + return selector + ? `${ selector } { ${ declaration }; }` + : `${ declaration };`; +} + /** * Generates CSS for a block instance state style object. * @@ -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' ); } diff --git a/packages/block-editor/src/hooks/test/style.js b/packages/block-editor/src/hooks/test/style.js index 7ae724719bc81e..92f4dbb488e995 100644 --- a/packages/block-editor/src/hooks/test/style.js +++ b/packages/block-editor/src/hooks/test/style.js @@ -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', () => { @@ -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( diff --git a/packages/block-editor/src/hooks/test/text-align.js b/packages/block-editor/src/hooks/test/text-align.js index 711fc08b09eb9e..f9bcb1ea68e722 100644 --- a/packages/block-editor/src/hooks/test/text-align.js +++ b/packages/block-editor/src/hooks/test/text-align.js @@ -10,7 +10,11 @@ import { /** * Internal dependencies */ -import { getValidTextAlignments, addAssignedTextAlign } from '../text-align'; +import { + getValidTextAlignments, + addAssignedTextAlign, + getTextAlignControlGroup, +} from '../text-align'; const noop = () => {}; @@ -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 ); diff --git a/packages/block-editor/src/hooks/text-align.js b/packages/block-editor/src/hooks/text-align.js index af20404d79eccf..69600dc1f2a356 100644 --- a/packages/block-editor/src/hooks/text-align.js +++ b/packages/block-editor/src/hooks/text-align.js @@ -9,6 +9,7 @@ import clsx from 'clsx'; import { __ } from '@wordpress/i18n'; import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; import { alignLeft, alignRight, alignCenter } from '@wordpress/icons'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -16,11 +17,14 @@ import { alignLeft, alignRight, alignCenter } from '@wordpress/icons'; import { AlignmentControl, BlockControls } from '../components'; import { useBlockEditingMode } from '../components/block-editing-mode'; import { - cleanEmptyObject, - shouldSkipSerialization, - useBlockSettings, -} from './utils'; + getStyleForState, + hasViewportBlockStyleState, + setStyleForState, +} from './block-style-state'; +import { shouldSkipSerialization, useBlockSettings } from './utils'; import { TYPOGRAPHY_SUPPORT_KEY } from './typography'; +import { store as blockEditorStore } from '../store'; +import { unlock } from '../lock-unlock'; export const TEXT_ALIGN_SUPPORT_KEY = 'typography.textAlign'; @@ -64,7 +68,14 @@ export function getValidTextAlignments( blockTextAlign ) { return blockTextAlign === true ? VALID_TEXT_ALIGNMENTS : NO_TEXT_ALIGNMENTS; } +export function getTextAlignControlGroup( isResponsiveEditing, selectedState ) { + return isResponsiveEditing && hasViewportBlockStyleState( selectedState ) + ? 'style-state' + : 'block'; +} + function BlockEditTextAlignmentToolbarControlsPure( { + clientId, style, name: blockName, setAttributes, @@ -72,6 +83,19 @@ function BlockEditTextAlignmentToolbarControlsPure( { const settings = useBlockSettings( blockName ); const hasTextAlignControl = settings?.typography?.textAlign; const blockEditingMode = useBlockEditingMode(); + const { selectedState, isResponsiveEditing } = useSelect( + ( select ) => { + const { + getSelectedBlockStyleState, + isResponsiveEditing: getIsResponsiveEditing, + } = unlock( select( blockEditorStore ) ); + return { + selectedState: getSelectedBlockStyleState( clientId ), + isResponsiveEditing: getIsResponsiveEditing(), + }; + }, + [ clientId ] + ); if ( ! hasTextAlignControl || blockEditingMode !== 'default' ) { return null; @@ -87,23 +111,30 @@ function BlockEditTextAlignmentToolbarControlsPure( { const textAlignmentControls = TEXT_ALIGNMENT_OPTIONS.filter( ( control ) => validTextAlignments.includes( control.align ) ); + const stateStyle = getStyleForState( style, selectedState ); + const controlsGroup = getTextAlignControlGroup( + isResponsiveEditing, + selectedState + ); const onChange = ( newTextAlignValue ) => { - const newStyle = { - ...style, + const newStateStyle = { + ...stateStyle, typography: { - ...style?.typography, + ...stateStyle?.typography, textAlign: newTextAlignValue, }, }; - setAttributes( { style: cleanEmptyObject( newStyle ) } ); + setAttributes( { + style: setStyleForState( style, selectedState, newStateStyle ), + } ); }; return ( - + diff --git a/packages/block-editor/src/layouts/constrained.js b/packages/block-editor/src/layouts/constrained.js index 9e9bd659ceaa81..96adfcf08c6874 100644 --- a/packages/block-editor/src/layouts/constrained.js +++ b/packages/block-editor/src/layouts/constrained.js @@ -219,6 +219,7 @@ export default { layout = {}, onChange, layoutBlockSupport, + controlsGroup = 'block', } ) { const { allowJustification = true } = layoutBlockSupport; @@ -226,7 +227,10 @@ export default { return null; } return ( - + + { allowJustification && ( ensure_block_registered( 'core/paragraph' ); + + $block_content = '

Hello

'; + $block = array( + 'blockName' => 'core/paragraph', + 'attrs' => array( + 'style' => array( + 'typography' => array( + 'textAlign' => 'left', + ), + '@mobile' => array( + 'typography' => array( + 'textAlign' => 'right', + ), + ), + ), + ), + ); + + $actual = gutenberg_render_block_states_support( $block_content, $block ); + + $this->assertMatchesRegularExpression( + '/^

Hello<\/p>$/', + $actual + ); + preg_match( '/wp-states-[a-f0-9]{8}/', $actual, $matches ); + $actual_stylesheet = gutenberg_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); + + $this->assertStringContainsString( + '@media (width <= 480px){.' . $matches[0] . '{text-align:right !important;}}', + $actual_stylesheet + ); + } + public function test_legacy_responsive_root_state_generates_media_query_scoped_css() { $this->ensure_block_registered( 'core/paragraph' );