diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 25fe5746a04122..71c0c1c8b957ff 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -275,7 +275,7 @@ Gather blocks in a layout container. ([Source](https://github.com/WordPress/gute - **Name:** core/group - **Category:** design -- **Supports:** align (full, wide), anchor, ariaLabel, color (background, gradients, link, text), dimensions (minHeight), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), anchor, ariaLabel, color (background, gradients, link, text), dimensions (minHeight), position (sticky), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** tagName, templateLock ## Heading diff --git a/lib/block-supports/position.php b/lib/block-supports/position.php new file mode 100644 index 00000000000000..8d1436049a7e42 --- /dev/null +++ b/lib/block-supports/position.php @@ -0,0 +1,143 @@ +attributes ) { + $block_type->attributes = array(); + } + + if ( $has_position_support && ! array_key_exists( 'style', $block_type->attributes ) ) { + $block_type->attributes['style'] = array( + 'type' => 'object', + ); + } +} + +/** + * Renders position styles to the block wrapper. + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function gutenberg_render_position_support( $block_content, $block ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $has_position_support = block_has_support( $block_type, array( 'position' ), false ); + + if ( + ! $has_position_support || + empty( $block['attrs']['style']['position'] ) + ) { + return $block_content; + } + + $global_settings = gutenberg_get_global_settings(); + $theme_has_sticky_support = _wp_array_get( $global_settings, array( 'position', 'sticky' ), false ); + $theme_has_fixed_support = _wp_array_get( $global_settings, array( 'position', 'fixed' ), false ); + + // Only allow output for position types that the theme supports. + $allowed_position_types = array(); + if ( true === $theme_has_sticky_support ) { + $allowed_position_types[] = 'sticky'; + } + if ( true === $theme_has_fixed_support ) { + $allowed_position_types[] = 'fixed'; + } + + $style_attribute = _wp_array_get( $block, array( 'attrs', 'style' ), null ); + $class_name = wp_unique_id( 'wp-container-' ); + $selector = ".$class_name"; + $position_styles = array(); + $position_type = _wp_array_get( $style_attribute, array( 'position', 'type' ), '' ); + + if ( + in_array( $position_type, $allowed_position_types, true ) + ) { + $sides = array( 'top', 'right', 'bottom', 'left' ); + + foreach ( $sides as $side ) { + $side_value = _wp_array_get( $style_attribute, array( 'position', $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' => '10', // TODO: Replace hard-coded z-index value with a z-index preset approach in theme.json. + ), + ); + } + + if ( ! empty( $position_styles ) ) { + /* + * Add to the style engine store to enqueue and render position styles. + */ + gutenberg_style_engine_get_stylesheet_from_css_rules( + $position_styles, + array( + 'context' => 'block-supports', + 'prettify' => false, + ) + ); + + // Inject class name to block container markup. + $content = new WP_HTML_Tag_Processor( $block_content ); + $content->next_tag(); + $content->add_class( $class_name ); + return (string) $content; + } + + return $block_content; +} + +// Register the block support. (overrides core one). +WP_Block_Supports::get_instance()->register( + 'position', + array( + 'register_attribute' => 'gutenberg_register_position_support', + ) +); + +if ( function_exists( 'wp_render_position_support' ) ) { + remove_filter( 'render_block', 'wp_render_position_support' ); +} +add_filter( 'render_block', 'gutenberg_render_position_support', 10, 2 ); diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index ddad36e15fc02d..982832f33722c6 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -330,7 +330,7 @@ class WP_Theme_JSON_Gutenberg { * and `typography`, and renamed others according to the new schema. * @since 6.0.0 Added `color.defaultDuotone`. * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`. - * @since 6.2.0 Added `dimensions.minHeight`. + * @since 6.2.0 Added `dimensions.minHeight`, `position.fixed` and `position.sticky`. * @var array */ const VALID_SETTINGS = array( @@ -369,6 +369,10 @@ class WP_Theme_JSON_Gutenberg { 'definitions' => null, 'wideSize' => null, ), + 'position' => array( + 'fixed' => null, + 'sticky' => null, + ), 'spacing' => array( 'customSpacingSize' => null, 'spacingSizes' => null, @@ -536,6 +540,7 @@ public static function get_element_class_name( $element ) { * Options that settings.appearanceTools enables. * * @since 6.0.0 + * @since 6.2.0 Added `position.fixed` and `position.sticky`. * @var array */ const APPEARANCE_TOOLS_OPT_INS = array( @@ -545,6 +550,8 @@ public static function get_element_class_name( $element ) { array( 'border', 'width' ), array( 'color', 'link' ), array( 'dimensions', 'minHeight' ), + array( 'position', 'fixed' ), + array( 'position', 'sticky' ), array( 'spacing', 'blockGap' ), array( 'spacing', 'margin' ), array( 'spacing', 'padding' ), diff --git a/lib/compat/wordpress-6.2/blocks.php b/lib/compat/wordpress-6.2/blocks.php new file mode 100644 index 00000000000000..250ed70ff9b98a --- /dev/null +++ b/lib/compat/wordpress-6.2/blocks.php @@ -0,0 +1,26 @@ += 6.2. + * + * @param string[] $attrs Array of allowed CSS attributes. + * @return string[] CSS attributes. + */ +function gutenberg_safe_style_attrs_6_2( $attrs ) { + $attrs[] = 'position'; + $attrs[] = 'top'; + $attrs[] = 'right'; + $attrs[] = 'bottom'; + $attrs[] = 'left'; + $attrs[] = 'z-index'; + + return $attrs; +} +add_filter( 'safe_style_css', 'gutenberg_safe_style_attrs_6_2' ); diff --git a/lib/load.php b/lib/load.php index 1b296053965319..83af73adc53d11 100644 --- a/lib/load.php +++ b/lib/load.php @@ -75,6 +75,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.1/theme.php'; // WordPress 6.2 compat. +require __DIR__ . '/compat/wordpress-6.2/blocks.php'; require __DIR__ . '/compat/wordpress-6.2/script-loader.php'; require __DIR__ . '/compat/wordpress-6.2/block-template-utils.php'; require __DIR__ . '/compat/wordpress-6.2/get-global-styles-and-settings.php'; @@ -132,6 +133,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/block-supports/typography.php'; require __DIR__ . '/block-supports/border.php'; require __DIR__ . '/block-supports/layout.php'; +require __DIR__ . '/block-supports/position.php'; require __DIR__ . '/block-supports/spacing.php'; require __DIR__ . '/block-supports/dimensions.php'; require __DIR__ . '/block-supports/duotone.php'; diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index 3e2aebb5d500a5..eb19c84e3ef47b 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -35,6 +35,7 @@ import { default as InspectorControls } from '../inspector-controls'; import { default as InspectorControlsTabs } from '../inspector-controls-tabs'; import useInspectorControlsTabs from '../inspector-controls-tabs/use-inspector-controls-tabs'; import AdvancedControls from '../inspector-controls-tabs/advanced-controls-panel'; +import PositionControls from '../inspector-controls-tabs/position-controls-panel'; function useContentBlocks( blockTypes, block ) { const contentBlocksObjectAux = useMemo( () => { @@ -377,6 +378,7 @@ const BlockInspectorSingleBlock = ( { clientId, blockName } ) => { __experimentalGroup="border" label={ __( 'Border' ) } /> +
diff --git a/packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js b/packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js index d218b1104139cf..f99323dd5c80a7 100644 --- a/packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js +++ b/packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js @@ -3,13 +3,20 @@ */ import { useRefEffect } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; -import { useCallback, useLayoutEffect, useState } from '@wordpress/element'; +import { getScrollContainer } from '@wordpress/dom'; +import { + useCallback, + useLayoutEffect, + useMemo, + useState, +} from '@wordpress/element'; /** * Internal dependencies */ import { store as blockEditorStore } from '../../store'; import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; +import { hasStickyOrFixedPositionValue } from '../../hooks/position'; const COMMON_PROPS = { placement: 'top-start', @@ -40,28 +47,50 @@ const RESTRICTED_HEIGHT_PROPS = { * * @param {Element} contentElement The DOM element that represents the editor content or canvas. * @param {Element} selectedBlockElement The outer DOM element of the first selected block. + * @param {Element} scrollContainer The scrollable container for the contentElement. * @param {number} toolbarHeight The height of the toolbar in pixels. + * @param {boolean} isSticky Whether or not the selected block is sticky or fixed. * * @return {Object} The popover props used to determine the position of the toolbar. */ -function getProps( contentElement, selectedBlockElement, toolbarHeight ) { +function getProps( + contentElement, + selectedBlockElement, + scrollContainer, + toolbarHeight, + isSticky +) { if ( ! contentElement || ! selectedBlockElement ) { return DEFAULT_PROPS; } + // Get how far the content area has been scrolled. + const scrollTop = scrollContainer?.scrollTop || 0; + const blockRect = selectedBlockElement.getBoundingClientRect(); const contentRect = contentElement.getBoundingClientRect(); + // Get the vertical position of top of the visible content area. + const topOfContentElementInViewport = scrollTop + contentRect.top; + // The document element's clientHeight represents the viewport height. const viewportHeight = contentElement.ownerDocument.documentElement.clientHeight; - const hasSpaceForToolbarAbove = - blockRect.top - contentRect.top > toolbarHeight; + // The restricted height area is calculated as the sum of the + // vertical position of the visible content area, plus the height + // of the block toolbar. + const restrictedTopArea = topOfContentElementInViewport + toolbarHeight; + const hasSpaceForToolbarAbove = blockRect.top > restrictedTopArea; + const isBlockTallerThanViewport = blockRect.height > viewportHeight - toolbarHeight; - if ( hasSpaceForToolbarAbove || isBlockTallerThanViewport ) { + // Sticky blocks are treated as if they will never have enough space for the toolbar above. + if ( + ! isSticky && + ( hasSpaceForToolbarAbove || isBlockTallerThanViewport ) + ) { return DEFAULT_PROPS; } @@ -83,13 +112,34 @@ export default function useBlockToolbarPopoverProps( { } ) { const selectedBlockElement = useBlockElement( clientId ); const [ toolbarHeight, setToolbarHeight ] = useState( 0 ); - const [ props, setProps ] = useState( () => - getProps( contentElement, selectedBlockElement, toolbarHeight ) - ); - const blockIndex = useSelect( - ( select ) => select( blockEditorStore ).getBlockIndex( clientId ), + const { blockIndex, isSticky } = useSelect( + ( select ) => { + const { getBlockIndex, getBlockAttributes } = + select( blockEditorStore ); + return { + blockIndex: getBlockIndex( clientId ), + isSticky: hasStickyOrFixedPositionValue( + getBlockAttributes( clientId ) + ), + }; + }, [ clientId ] ); + const scrollContainer = useMemo( () => { + if ( ! contentElement ) { + return; + } + return getScrollContainer( contentElement ); + }, [ contentElement ] ); + const [ props, setProps ] = useState( () => + getProps( + contentElement, + selectedBlockElement, + scrollContainer, + toolbarHeight, + isSticky + ) + ); const popoverRef = useRefEffect( ( popoverNode ) => { setToolbarHeight( popoverNode.offsetHeight ); @@ -98,9 +148,15 @@ export default function useBlockToolbarPopoverProps( { const updateProps = useCallback( () => setProps( - getProps( contentElement, selectedBlockElement, toolbarHeight ) + getProps( + contentElement, + selectedBlockElement, + scrollContainer, + toolbarHeight, + isSticky + ) ), - [ contentElement, selectedBlockElement, toolbarHeight ] + [ contentElement, selectedBlockElement, scrollContainer, toolbarHeight ] ); // Update props when the block is moved. This also ensures the props are diff --git a/packages/block-editor/src/components/inspector-controls-tabs/position-controls-panel.js b/packages/block-editor/src/components/inspector-controls-tabs/position-controls-panel.js new file mode 100644 index 00000000000000..5fc71fab5960f4 --- /dev/null +++ b/packages/block-editor/src/components/inspector-controls-tabs/position-controls-panel.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { + PanelBody, + __experimentalUseSlotFills as useSlotFills, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import InspectorControlsGroups from '../inspector-controls/groups'; +import { default as InspectorControls } from '../inspector-controls'; + +const PositionControls = () => { + const fills = useSlotFills( + InspectorControlsGroups.position.Slot.__unstableName + ); + const hasFills = Boolean( fills && fills.length ); + + if ( ! hasFills ) { + return null; + } + + return ( + + + + ); +}; + +export default PositionControls; diff --git a/packages/block-editor/src/components/inspector-controls-tabs/settings-tab.js b/packages/block-editor/src/components/inspector-controls-tabs/settings-tab.js index beac0f10a178a5..ec34035b754a91 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/settings-tab.js +++ b/packages/block-editor/src/components/inspector-controls-tabs/settings-tab.js @@ -2,11 +2,13 @@ * Internal dependencies */ import AdvancedControls from './advanced-controls-panel'; +import PositionControls from './position-controls-panel'; import { default as InspectorControls } from '../inspector-controls'; const SettingsTab = ( { showAdvancedControls = false } ) => ( <> + { showAdvancedControls && (
diff --git a/packages/block-editor/src/components/inspector-controls-tabs/use-inspector-controls-tabs.js b/packages/block-editor/src/components/inspector-controls-tabs/use-inspector-controls-tabs.js index fbe072fab11c58..bf7f61de4c8cb7 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/use-inspector-controls-tabs.js +++ b/packages/block-editor/src/components/inspector-controls-tabs/use-inspector-controls-tabs.js @@ -43,6 +43,7 @@ export default function useInspectorControlsTabs( blockName ) { default: defaultGroup, dimensions: dimensionsGroup, list: listGroup, + position: positionGroup, typography: typographyGroup, } = InspectorControlsGroups; @@ -71,6 +72,7 @@ export default function useInspectorControlsTabs( blockName ) { // or Advanced Controls slot, then add this tab. const settingsFills = [ ...( useSlotFills( defaultGroup.Slot.__unstableName ) || [] ), + ...( useSlotFills( positionGroup.Slot.__unstableName ) || [] ), ...( useSlotFills( InspectorAdvancedControls.slotName ) || [] ), ]; diff --git a/packages/block-editor/src/components/inspector-controls/groups.js b/packages/block-editor/src/components/inspector-controls/groups.js index cb03c1ff13fa57..46fca564925aa6 100644 --- a/packages/block-editor/src/components/inspector-controls/groups.js +++ b/packages/block-editor/src/components/inspector-controls/groups.js @@ -10,6 +10,7 @@ const InspectorControlsColor = createSlotFill( 'InspectorControlsColor' ); const InspectorControlsDimensions = createSlotFill( 'InspectorControlsDimensions' ); +const InspectorControlsPosition = createSlotFill( 'InspectorControlsPosition' ); const InspectorControlsTypography = createSlotFill( 'InspectorControlsTypography' ); @@ -23,6 +24,7 @@ const groups = { dimensions: InspectorControlsDimensions, list: InspectorControlsListView, typography: InspectorControlsTypography, + position: InspectorControlsPosition, }; export default groups; diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index f6fa73a053ac58..84b3ba3a95a33b 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -14,6 +14,7 @@ import './color'; import './duotone'; import './font-size'; import './border'; +import './position'; import './layout'; import './content-lock-ui'; import './metadata'; diff --git a/packages/block-editor/src/hooks/position.js b/packages/block-editor/src/hooks/position.js new file mode 100644 index 00000000000000..1e50c35bfd0e6f --- /dev/null +++ b/packages/block-editor/src/hooks/position.js @@ -0,0 +1,375 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; +import { BaseControl, CustomSelectControl } from '@wordpress/components'; +import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { + useContext, + useMemo, + createPortal, + Platform, +} from '@wordpress/element'; +import { addFilter } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import BlockList from '../components/block-list'; +import useSetting from '../components/use-setting'; +import InspectorControls from '../components/inspector-controls'; +import { cleanEmptyObject } from './utils'; + +const POSITION_SUPPORT_KEY = 'position'; + +const OPTION_CLASSNAME = + 'block-editor-hooks__position-selection__select-control__option'; + +const DEFAULT_OPTION = { + key: 'default', + value: '', + name: __( 'Default' ), + className: OPTION_CLASSNAME, +}; + +const STICKY_OPTION = { + key: 'sticky', + value: 'sticky', + name: __( 'Sticky' ), + className: OPTION_CLASSNAME, + __experimentalHint: __( + 'The block will stick to the top of the window instead of scrolling.' + ), +}; + +const FIXED_OPTION = { + key: 'fixed', + value: 'fixed', + name: __( 'Fixed' ), + className: OPTION_CLASSNAME, + __experimentalHint: __( + 'The block will not move when the page is scrolled.' + ), +}; + +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 { type: positionType } = style?.position || {}; + + if ( ! VALID_POSITION_TYPES.includes( positionType ) ) { + return output; + } + + output += `${ selector } {`; + output += `position: ${ positionType };`; + + POSITION_SIDES.forEach( ( side ) => { + if ( style?.position?.[ side ] !== undefined ) { + output += `${ side }: ${ style.position[ side ] };`; + } + } ); + + if ( positionType === 'sticky' || positionType === 'fixed' ) { + // TODO: Replace hard-coded z-index value with a z-index preset approach in theme.json. + output += `z-index: 10`; + } + output += `}`; + + return output; +} + +/** + * Determines if there is sticky position support. + * + * @param {string|Object} blockType Block name or Block Type object. + * + * @return {boolean} Whether there is support. + */ +export function hasStickyPositionSupport( blockType ) { + const support = getBlockSupport( blockType, POSITION_SUPPORT_KEY ); + return !! ( true === support || support?.sticky ); +} + +/** + * Determines if there is fixed position support. + * + * @param {string|Object} blockType Block name or Block Type object. + * + * @return {boolean} Whether there is support. + */ +export function hasFixedPositionSupport( blockType ) { + const support = getBlockSupport( blockType, POSITION_SUPPORT_KEY ); + return !! ( true === support || support?.fixed ); +} + +/** + * 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, POSITION_SUPPORT_KEY ); + return !! support; +} + +/** + * 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?.position?.type !== undefined; +} + +/** + * Checks if the block is currently set to a sticky or fixed position. + * This check is helpful for determining how to position block toolbars or other elements. + * + * @param {Object} attributes Block attributes. + * @return {boolean} Whether or not the block is set to a sticky or fixed position. + */ +export function hasStickyOrFixedPositionValue( attributes ) { + const positionType = attributes.style?.position?.type; + return positionType === 'sticky' || positionType === 'fixed'; +} + +/** + * 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, + position: { + ...style?.position, + type: 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 allowFixed = useSetting( 'position.fixed' ); + const allowSticky = useSetting( 'position.sticky' ); + const isDisabled = ! allowFixed && ! allowSticky; + + return ! hasPositionSupport( blockName ) || isDisabled; +} + +/* + * Position controls to be rendered in an inspector control panel. + * + * @param {Object} props + * + * @return {WPElement} Padding edit element. + */ +export function PositionEdit( props ) { + const { + attributes: { style = {} }, + name: blockName, + setAttributes, + } = props; + + const allowFixed = hasFixedPositionSupport( blockName ); + const allowSticky = hasStickyPositionSupport( blockName ); + const value = style?.position?.type; + + const options = useMemo( () => { + const availableOptions = [ DEFAULT_OPTION ]; + if ( allowSticky || value === STICKY_OPTION.value ) { + availableOptions.push( STICKY_OPTION ); + } + if ( allowFixed || value === FIXED_OPTION.value ) { + availableOptions.push( FIXED_OPTION ); + } + return availableOptions; + }, [ allowFixed, allowSticky, value ] ); + + 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, + position: { + ...style?.position, + type: next, + top: + next === 'sticky' || next === 'fixed' + ? placementValue + : undefined, + }, + }; + + setAttributes( { + style: cleanEmptyObject( newStyle ), + } ); + }; + + const selectedOption = value + ? options.find( ( option ) => option.value === value ) || DEFAULT_OPTION + : DEFAULT_OPTION; + + return Platform.select( { + web: ( + <> + + { + onChangeType( selectedItem.value ); + } } + size={ '__unstable-large' } + /> + + + ), + native: null, + } ); +} + +/** + * Override the default edit UI to include position controls. + * + * @param {Function} BlockEdit Original component. + * + * @return {Function} Wrapped component. + */ +export const withInspectorControls = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const { name: blockName } = props; + const positionSupport = hasBlockSupport( + blockName, + POSITION_SUPPORT_KEY + ); + const showPositionControls = + positionSupport && ! useIsPositionDisabled( props ); + + return [ + showPositionControls && ( + + + + ), + , + ]; + }, + 'withInspectorControls' +); + +/** + * Override the default block element to add the position styles. + * + * @param {Function} BlockListBlock Original component. + * + * @return {Function} Wrapped component. + */ +export const withPositionStyles = createHigherOrderComponent( + ( BlockListBlock ) => ( props ) => { + const { name, attributes } = props; + const hasPositionBlockSupport = hasBlockSupport( + name, + POSITION_SUPPORT_KEY + ); + const allowPositionStyles = + hasPositionBlockSupport && ! useIsPositionDisabled( props ); + + const id = useInstanceId( BlockListBlock ); + const element = useContext( BlockList.__unstableElementContext ); + + // Higher specificity to override defaults in editor UI. + const positionSelector = `.wp-container-${ id }.wp-container-${ id }`; + + // Get CSS string for the current position values. + let css; + if ( allowPositionStyles ) { + css = + getPositionCSS( { + selector: positionSelector, + style: attributes?.style, + } ) || ''; + } + + // Attach a `wp-container-` id-based class name. + const className = classnames( props?.className, { + [ `wp-container-${ id }` ]: allowPositionStyles && !! css, // Only attach a container class if there is generated CSS to be attached. + } ); + + return ( + <> + { allowPositionStyles && + element && + !! css && + createPortal( , element ) } + + + ); + } +); + +addFilter( + 'editor.BlockListBlock', + 'core/editor/position/with-position-styles', + withPositionStyles +); +addFilter( + 'editor.BlockEdit', + 'core/editor/position/with-inspector-controls', + withInspectorControls +); diff --git a/packages/block-editor/src/hooks/position.scss b/packages/block-editor/src/hooks/position.scss new file mode 100644 index 00000000000000..b3bd6b1b9ef041 --- /dev/null +++ b/packages/block-editor/src/hooks/position.scss @@ -0,0 +1,18 @@ +.block-editor-hooks__position-selection__select-control { + .components-custom-select-control__hint { + display: none; + } +} + +.block-editor-hooks__position-selection__select-control__option { + &.has-hint { + grid-template-columns: auto 30px; + line-height: $default-line-height; + margin-bottom: 0; + } + + .components-custom-select-control__item-hint { + grid-row: 2; + text-align: left; + } +} diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 76da3c16325db9..3c2f60388e0df0 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -50,6 +50,7 @@ @import "./hooks/layout.scss"; @import "./hooks/border.scss"; @import "./hooks/dimensions.scss"; +@import "./hooks/position.scss"; @import "./hooks/typography.scss"; @import "./hooks/color.scss"; @import "./hooks/padding.scss"; diff --git a/packages/block-library/src/group/block.json b/packages/block-library/src/group/block.json index a3997db0621c54..2b227a15847a27 100644 --- a/packages/block-library/src/group/block.json +++ b/packages/block-library/src/group/block.json @@ -56,6 +56,9 @@ "width": true } }, + "position": { + "sticky": true + }, "typography": { "fontSize": true, "lineHeight": true,