Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Design Tools: Add a Position block support (including sticky), decoupled from layout #46142

Merged
merged 13 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from 11 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
2 changes: 1 addition & 1 deletion docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
130 changes: 130 additions & 0 deletions lib/block-supports/position.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php
/**
* Position block support flag.
*
* @package gutenberg
*/

/**
* Registers the style block attribute for block types that support it.
*
* @param WP_Block_Type $block_type Block Type.
*/
function gutenberg_register_position_support( $block_type ) {
$has_position_support = block_has_support( $block_type, array( 'position' ), false );

// Set up attributes and styles within that if needed.
if ( ! $block_type->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;
}

$style_attribute = _wp_array_get( $block, array( 'attrs', 'style' ), null );
$class_name = wp_unique_id( 'wp-container-' );
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we anticipate having to target children of positioned blocks with CSS, or are there others reason to use the layout support logic here as opposed to the mix of preset classnames and inline styles used for other block supports?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, good point! The approach I went with here predates the WP_HTML_Tag_Processor, without which it would have been tricky to append the style rules, but you're right, each of the generated rules should be able to happily exist as an inline value.

It's definitely worth a try, and would be a neater approach if it works cleanly 👍

$selector = ".$class_name";
$position_styles = array();
$position_type = _wp_array_get( $style_attribute, array( 'position', 'type' ), '' );

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_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: This hard-coded value should live somewhere else.
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we associate this with a static classname so it could be attached to the existing z-index scale?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, I like the idea of a classname approach. z-index could potentially work nicely with presets now that you mention it, as we could have a preset for the sticky level. I was planning to look further into how to manage z-indexes in a follow-up PR rather than this one, as it could be a bit fiddly to get right. Maybe as a first step for this PR, if we inject z-index: 10 in the inline style along with the position / top value... then in a follow-up we can look at creating the preset?

One thing to note: because the z-index value is output to the site frontend, it needs to exist separately from the existing z-index scale, which is only present in the editor.

But I do like the idea of presets for z-index values, a bit like we have for font sizes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I think it's fine to leave the z-index refinements for a follow-up 👍

),
);
}

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 );
9 changes: 8 additions & 1 deletion lib/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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' ),
Expand Down
26 changes: 26 additions & 0 deletions lib/compat/wordpress-6.2/blocks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
/**
* Temporary compatibility shims for block APIs present in Gutenberg.
*
* @package gutenberg
*/

/**
* Update allowed inline style attributes list.
*
* Note: This should be removed when the minimum required WP version is >= 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' );
2 changes: 2 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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( () => {
Expand Down Expand Up @@ -377,6 +378,7 @@ const BlockInspectorSingleBlock = ( { clientId, blockName } ) => {
__experimentalGroup="border"
label={ __( 'Border' ) }
/>
<PositionControls />
<div>
<AdvancedControls />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;
}

Expand All @@ -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 );
Expand All @@ -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
Expand Down
Loading