diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index a50e1fb8837178..295304acbcd550 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -461,6 +461,117 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { } add_filter( 'render_block', 'gutenberg_render_layout_support_flag', 10, 2 ); +/** + * Generates the CSS for layout position support from the style object. + * + * @param string $selector CSS selector. + * @param array $style Style object. + * @return string CSS styles on success. Else, empty string. + */ +function gutenberg_get_layout_position_style( $selector, $style ) { + $position_styles = array(); + $position_type = _wp_array_get( $style, array( 'layout', 'position' ), '' ); + + if ( + in_array( $position_type, array( 'fixed', 'sticky' ), true ) + ) { + $sides = array( 'top', 'right', 'bottom', 'left' ); + + foreach ( $sides as $side ) { + $side_value = _wp_array_get( $style, array( 'layout', $side ) ); + if ( null !== $side_value ) { + /* + * For fixed or sticky top positions, + * ensure the value includes an offset for the logged in admin bar. + */ + if ( + 'top' === $side && + ( 'fixed' === $position_type || 'sticky' === $position_type ) + ) { + // Ensure 0 values can be used in `calc()` calculations. + if ( '0' === $side_value || 0 === $side_value ) { + $side_value = '0px'; + } + + // Ensure current side value also factors in the height of the logged in admin bar. + $side_value = "calc($side_value + var(--wp-admin--admin-bar--height, 0px))"; + } + + $position_styles[] = + array( + 'selector' => "$selector", + 'declarations' => array( + $side => $side_value, + ), + ); + } + } + + $position_styles[] = + array( + 'selector' => "$selector", + 'declarations' => array( + 'position' => $position_type, + 'z-index' => '250', // TODO: This hard-coded value should live somewhere else. + ), + ); + } + + if ( ! empty( $position_styles ) ) { + /* + * Add to the style engine store to enqueue and render layout styles. + */ + return gutenberg_style_engine_get_stylesheet_from_css_rules( + $position_styles, + array( + 'context' => 'block-supports', + 'prettify' => false, + ) + ); + } + + return ''; +} + +/** + * Renders layout position styles to the block wrapper. + * + * Position related styles should always be applied to the outer wrapper, + * so this logic is separate from the layout support's innerBlocks wrapper logic. + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function gutenberg_render_layout_position_support( $block_content, $block ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $has_layout_position_support = block_has_support( $block_type, array( '__experimentalLayout', 'allowPosition' ), false ); + + if ( + ! $has_layout_position_support || + empty( $block['attrs']['style']['layout'] ) + ) { + return $block_content; + } + + $style_attribute = _wp_array_get( $block, array( 'attrs', 'style' ), null ); + $class_name = wp_unique_id( 'wp-container-' ); + $style = gutenberg_get_layout_position_style( ".$class_name.$class_name", $style_attribute ); + + if ( ! empty( $style ) ) { + $content = new WP_HTML_Tag_Processor( $block_content ); + $content->next_tag(); + $content->add_class( $class_name ); + return (string) $content; + } + return $block_content; +} + +if ( function_exists( 'wp_render_layout_position_support' ) ) { + remove_filter( 'render_block', 'wp_render_layout_position_support' ); +} +add_filter( 'render_block', 'gutenberg_render_layout_position_support', 10, 2 ); + /** * For themes without theme.json file, make sure * to restore the inner div for the group block diff --git a/lib/compat/wordpress-6.1/blocks.php b/lib/compat/wordpress-6.1/blocks.php index cd61b70a07f09b..c4f6a04c88bb74 100644 --- a/lib/compat/wordpress-6.1/blocks.php +++ b/lib/compat/wordpress-6.1/blocks.php @@ -20,6 +20,12 @@ function gutenberg_safe_style_attrs_6_1( $attrs ) { $attrs[] = 'margin-block-end'; $attrs[] = 'margin-inline-start'; $attrs[] = 'margin-inline-end'; + $attrs[] = 'position'; + $attrs[] = 'top'; + $attrs[] = 'right'; + $attrs[] = 'bottom'; + $attrs[] = 'left'; + $attrs[] = 'z-index'; return $attrs; } diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php index d7f934ef2eb305..611278d40ac2a1 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php @@ -98,6 +98,19 @@ class WP_Theme_JSON_6_1 extends WP_Theme_JSON_6_0 { 'box-shadow' => array( 'shadow' ), ); + const APPEARANCE_TOOLS_OPT_INS = array( + array( 'border', 'color' ), + array( 'border', 'radius' ), + array( 'border', 'style' ), + array( 'border', 'width' ), + array( 'color', 'link' ), + array( 'layout', 'position' ), + array( 'spacing', 'blockGap' ), + array( 'spacing', 'margin' ), + array( 'spacing', 'padding' ), + array( 'typography', 'lineHeight' ), + ); + /** * The valid elements that can be found under styles. * @@ -1327,6 +1340,7 @@ protected static function get_property_value( $styles, $path, $theme_json = null 'layout' => array( 'contentSize' => null, 'definitions' => null, + 'position' => null, 'wideSize' => null, ), 'spacing' => array( diff --git a/lib/compat/wordpress-6.1/theme.json b/lib/compat/wordpress-6.1/theme.json index 40432cedf38777..6ace6510111845 100644 --- a/lib/compat/wordpress-6.1/theme.json +++ b/lib/compat/wordpress-6.1/theme.json @@ -323,7 +323,8 @@ } ] } - } + }, + "position": false }, "spacing": { "blockGap": null, diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index a2058cb24677e9..623bd3d5705420 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -29,8 +29,9 @@ import useSetting from '../components/use-setting'; import { LayoutStyle } from '../components/block-list/layout'; import BlockList from '../components/block-list'; import { getLayoutType, getLayoutTypes } from '../layouts'; +import { PositionEdit, getPositionCSS } from './position'; -const layoutBlockSupportKey = '__experimentalLayout'; +export const LAYOUT_SUPPORT_KEY = '__experimentalLayout'; /** * Generates the utility classnames for the given block's layout attributes. @@ -51,7 +52,7 @@ export function useLayoutClasses( block = {} ) { const { layout } = attributes; const { default: defaultBlockLayout } = - getBlockSupport( name, layoutBlockSupportKey ) || {}; + getBlockSupport( name, LAYOUT_SUPPORT_KEY ) || {}; const usedLayout = layout?.inherit || layout?.contentSize || layout?.wideSize ? { ...layout, type: 'constrained' } @@ -128,7 +129,8 @@ export function useLayoutStyles( block = {}, selector ) { return css; } -function LayoutPanel( { setAttributes, attributes, name: blockName } ) { +function LayoutPanel( props ) { + const { setAttributes, attributes, name: blockName } = props; const { layout } = attributes; const defaultThemeLayout = useSetting( 'layout' ); const themeSupportsLayout = useSelect( ( select ) => { @@ -138,13 +140,14 @@ function LayoutPanel( { setAttributes, attributes, name: blockName } ) { const layoutBlockSupport = getBlockSupport( blockName, - layoutBlockSupportKey, + LAYOUT_SUPPORT_KEY, {} ); const { - allowSwitching, allowEditing = true, allowInheriting = true, + allowPosition, + allowSwitching, default: defaultBlockLayout, } = layoutBlockSupport; @@ -252,6 +255,8 @@ function LayoutPanel( { setAttributes, attributes, name: blockName } ) { layoutBlockSupport={ layoutBlockSupport } /> ) } + + { allowPosition && } { ! inherit && layoutType && ( @@ -294,7 +299,7 @@ export function addAttribute( settings ) { if ( 'type' in ( settings.attributes?.layout ?? {} ) ) { return settings; } - if ( hasBlockSupport( settings, layoutBlockSupportKey ) ) { + if ( hasBlockSupport( settings, LAYOUT_SUPPORT_KEY ) ) { settings.attributes = { ...settings.attributes, layout: { @@ -316,10 +321,7 @@ export function addAttribute( settings ) { export const withInspectorControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const { name: blockName } = props; - const supportLayout = hasBlockSupport( - blockName, - layoutBlockSupportKey - ); + const supportLayout = hasBlockSupport( blockName, LAYOUT_SUPPORT_KEY ); return [ supportLayout && , @@ -341,7 +343,7 @@ export const withLayoutStyles = createHigherOrderComponent( const { name, attributes, block } = props; const hasLayoutBlockSupport = hasBlockSupport( name, - layoutBlockSupportKey + LAYOUT_SUPPORT_KEY ); const disableLayoutStyles = useSelect( ( select ) => { const { getSettings } = select( blockEditorStore ); @@ -350,11 +352,12 @@ export const withLayoutStyles = createHigherOrderComponent( const shouldRenderLayoutStyles = hasLayoutBlockSupport && ! disableLayoutStyles; const id = useInstanceId( BlockListBlock ); + const positionId = useInstanceId( BlockListBlock ); const defaultThemeLayout = useSetting( 'layout' ) || {}; const element = useContext( BlockList.__unstableElementContext ); const { layout } = attributes; const { default: defaultBlockLayout } = - getBlockSupport( name, layoutBlockSupportKey ) || {}; + getBlockSupport( name, LAYOUT_SUPPORT_KEY ) || {}; const usedLayout = layout?.inherit || layout?.contentSize || layout?.wideSize ? { ...layout, type: 'constrained' } @@ -362,18 +365,23 @@ export const withLayoutStyles = createHigherOrderComponent( const layoutClasses = hasLayoutBlockSupport ? useLayoutClasses( block ) : null; + // Higher specificity to override defaults from theme.json. const selector = `.wp-container-${ id }.wp-container-${ id }`; + const positionSelector = `.wp-container-${ positionId }.wp-container-${ positionId }`; const blockGapSupport = useSetting( 'spacing.blockGap' ); const hasBlockGapSupport = blockGapSupport !== null; // Get CSS string for the current layout type. // The CSS and `style` element is only output if it is not empty. let css; + let positionCss; if ( shouldRenderLayoutStyles ) { const fullLayoutType = getLayoutType( usedLayout?.type || 'default' ); + + // Add layout CSS. css = fullLayoutType?.getLayoutStyle?.( { blockName: name, selector, @@ -382,6 +390,16 @@ export const withLayoutStyles = createHigherOrderComponent( style: attributes?.style, hasBlockGapSupport, } ); + + // Add position CSS where applicable. + positionCss = + getPositionCSS( { + selector: positionSelector, + style: attributes?.style, + } ) || ''; + + // Concatenate CSS for output. + css += positionCss; } // Attach a `wp-container-` id-based class name as well as a layout class name such as `is-layout-flex`. @@ -392,6 +410,11 @@ export const withLayoutStyles = createHigherOrderComponent( layoutClasses ); + const wrapperClassNames = classnames( props?.className, { + [ `wp-container-${ positionId }` ]: + shouldRenderLayoutStyles && !! positionCss, // Use separate container class for position styles in prep for layout styles moving to inner wrapper in: https://github.com/WordPress/gutenberg/pull/44600 + } ); + return ( <> { shouldRenderLayoutStyles && @@ -409,6 +432,7 @@ export const withLayoutStyles = createHigherOrderComponent( ) } diff --git a/packages/block-editor/src/hooks/position.js b/packages/block-editor/src/hooks/position.js new file mode 100644 index 00000000000000..8439423f80baba --- /dev/null +++ b/packages/block-editor/src/hooks/position.js @@ -0,0 +1,201 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Platform } from '@wordpress/element'; +import { getBlockSupport } from '@wordpress/blocks'; +import { + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import useSetting from '../components/use-setting'; +import { LAYOUT_SUPPORT_KEY } from './layout'; +import { cleanEmptyObject } from './utils'; + +const POSITION_OPTIONS = [ + { + key: 'default', + label: __( 'Default' ), + value: '', + name: __( 'Default' ), + }, + { + key: 'sticky', + label: __( 'Sticky' ), + value: 'sticky', + name: __( 'Sticky' ), + }, +]; + +const POSITION_SIDES = [ 'top', 'right', 'bottom', 'left' ]; +const VALID_POSITION_TYPES = [ 'sticky', 'fixed' ]; + +/** + * Get calculated position CSS. + * + * @param {Object} props Component props. + * @param {string} props.selector Selector to use. + * @param {Object} props.style Style object. + * @return {string} The generated CSS rules. + */ +export function getPositionCSS( { selector, style } ) { + let output = ''; + + const { position } = style?.layout || {}; + + if ( ! VALID_POSITION_TYPES.includes( position ) ) { + return output; + } + + output += `${ selector } {`; + output += `position: ${ position };`; + + POSITION_SIDES.forEach( ( side ) => { + if ( style?.layout?.[ side ] !== undefined ) { + output += `${ side }: ${ style.layout[ side ] };`; + } + } ); + + if ( position === 'sticky' || position === 'fixed' ) { + // TODO: Work out where to put the magic z-index value. + output += `z-index: 250`; + } + output += `}`; + + return output; +} + +/** + * Determines if there is position support. + * + * @param {string|Object} blockType Block name or Block Type object. + * + * @return {boolean} Whether there is support. + */ +export function hasPositionSupport( blockType ) { + const support = getBlockSupport( blockType, LAYOUT_SUPPORT_KEY ); + return !! ( true === support || support?.allowPosition ); +} + +/** + * Checks if there is a current value in the position block support attributes. + * + * @param {Object} props Block props. + * @return {boolean} Whether or not the block has a position value set. + */ +export function hasPositionValue( props ) { + return props.attributes.style?.layout?.position !== undefined; +} + +/** + * Resets the position block support attributes. This can be used when disabling + * the position support controls for a block via a `ToolsPanel`. + * + * @param {Object} props Block props. + * @param {Object} props.attributes Block's attributes. + * @param {Object} props.setAttributes Function to set block's attributes. + */ +export function resetPosition( { attributes = {}, setAttributes } ) { + const { style = {} } = attributes; + + setAttributes( { + style: cleanEmptyObject( { + ...style, + layout: { + ...style?.layout, + position: undefined, + top: undefined, + right: undefined, + bottom: undefined, + left: undefined, + }, + } ), + } ); +} + +/** + * Custom hook that checks if position settings have been disabled. + * + * @param {string} name The name of the block. + * + * @return {boolean} Whether padding setting is disabled. + */ +export function useIsPositionDisabled( { name: blockName } = {} ) { + const isDisabled = ! useSetting( 'layout.position' ); + + return ! hasPositionSupport( blockName ) || isDisabled; +} + +/** + * Inspector control panel containing the padding related configuration + * + * @param {Object} props + * + * @return {WPElement} Padding edit element. + */ +export function PositionEdit( props ) { + const { + attributes: { style = {} }, + setAttributes, + } = props; + + if ( useIsPositionDisabled( props ) ) { + return null; + } + + const onChangeType = ( next ) => { + // For now, use a hard-coded `0px` value for the position. + // `0px` is preferred over `0` as it can be used in `calc()` functions. + // In the future, it could be useful to allow for an offset value. + const placementValue = '0px'; + + const newStyle = { + ...style, + layout: { + ...style?.layout, + position: next, + top: + next === 'sticky' || next === 'fixed' + ? placementValue + : undefined, + }, + }; + + setAttributes( { + style: cleanEmptyObject( newStyle ), + } ); + }; + + return Platform.select( { + web: ( + <> + { + onChangeType( newValue ); + } } + isBlock + > + { POSITION_OPTIONS.map( ( option ) => ( + + ) ) } + + + ), + native: null, + } ); +} diff --git a/packages/block-library/src/group/block.json b/packages/block-library/src/group/block.json index 968f00cb1c5cbf..981439cbc69d00 100644 --- a/packages/block-library/src/group/block.json +++ b/packages/block-library/src/group/block.json @@ -66,7 +66,10 @@ "fontSize": true } }, - "__experimentalLayout": true + "__experimentalLayout": { + "allowSwitching": false, + "allowPosition": true + } }, "editorStyle": "wp-block-group-editor", "style": "wp-block-group"