From 169dc1b3dab28db05ad7629b2a1243ed0f6953c9 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Tue, 10 Jan 2023 17:46:46 +0100 Subject: [PATCH 01/12] WP_HTML_Tag_Processor: Add `get_attribute_names_with_prefix()` method (#46840) Add a `get_attribute_names_with_prefix()` method to `WP_HTML_Tag_Processor`, which returns the lowercase names of all attributes whose names start with a given prefix (matching is case-insensitive). ```php $p = new WP_HTML_Tag_Processor( '
Test
' ); $p->next_tag(); $p->get_attributes_by_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' ); ``` --- .../html/class-wp-html-tag-processor.php | 43 +++++++++ phpunit/html/wp-html-tag-processor-test.php | 94 +++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/lib/experimental/html/class-wp-html-tag-processor.php b/lib/experimental/html/class-wp-html-tag-processor.php index 0dfa57f30f2aab..9539f0d626e9d6 100644 --- a/lib/experimental/html/class-wp-html-tag-processor.php +++ b/lib/experimental/html/class-wp-html-tag-processor.php @@ -1412,6 +1412,49 @@ public function get_attribute( $name ) { return html_entity_decode( $raw_value ); } + /** + * Returns the lowercase names of all attributes matching a given prefix in the currently-opened tag. + * + * Note that matching is case-insensitive. This is in accordance with the spec: + * + * > There must never be two or more attributes on + * > the same start tag whose names are an ASCII + * > case-insensitive match for each other. + * - HTML 5 spec + * + * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive + * + * Example: + * + * $p = new WP_HTML_Tag_Processor( '
Test
' ); + * $p->next_tag( [ 'class_name' => 'test' ] ) === true; + * $p->get_attribute_names_with_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' ); + * + * $p->next_tag( [] ) === false; + * $p->get_attribute_names_with_prefix( 'data-' ) === null; + *
+ * + * @since 6.2.0 + * + * @param string $prefix Prefix of requested attribute names. + * @return array|null List of attribute names, or `null` if not at a tag. + */ + function get_attribute_names_with_prefix( $prefix ) { + if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) { + return null; + } + + $comparable = strtolower( $prefix ); + + $matches = array(); + foreach ( array_keys( $this->attributes ) as $attr_name ) { + if ( str_starts_with( $attr_name, $comparable ) ) { + $matches[] = $attr_name; + } + } + return $matches; + } + /** * Returns the lowercase name of the currently-opened tag. * diff --git a/phpunit/html/wp-html-tag-processor-test.php b/phpunit/html/wp-html-tag-processor-test.php index e6b3d4d8071af5..6750410f26d43c 100644 --- a/phpunit/html/wp-html-tag-processor-test.php +++ b/phpunit/html/wp-html-tag-processor-test.php @@ -74,6 +74,19 @@ public function test_get_attribute_returns_null_when_not_in_open_tag() { $this->assertNull( $p->get_attribute( 'class' ), 'Accessing an attribute of a non-existing tag did not return null' ); } + /** + * @ticket 56299 + * + * @covers next_tag + * @covers get_attribute + */ + public function test_get_attribute_returns_null_when_in_closing_tag() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $this->assertTrue( $p->next_tag( 'div' ), 'Querying an existing tag did not return true' ); + $this->assertTrue( $p->next_tag( array( 'tag_closers' => 'visit' ) ), 'Querying an existing closing tag did not return true' ); + $this->assertNull( $p->get_attribute( 'class' ), 'Accessing an attribute of a closing tag did not return null' ); + } + /** * @ticket 56299 * @@ -195,6 +208,87 @@ public function test_set_attribute_is_case_insensitive() { $this->assertEquals( '
Test
', $p->get_updated_html(), 'A case-insensitive set_attribute call did not update the existing attribute.' ); } + /** + * @ticket 56299 + * + * @covers get_attribute_names_with_prefix + */ + public function test_get_attribute_names_with_prefix_returns_null_before_finding_tags() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $this->assertNull( $p->get_attribute_names_with_prefix( 'data-' ) ); + } + + /** + * @ticket 56299 + * + * @covers get_attribute_names_with_prefix + */ + public function test_get_attribute_names_with_prefix_returns_null_when_not_in_open_tag() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $p->next_tag( 'p' ); + $this->assertNull( $p->get_attribute_names_with_prefix( 'data-' ), 'Accessing attributes of a non-existing tag did not return null' ); + } + + /** + * @ticket 56299 + * + * @covers get_attribute_names_with_prefix + */ + public function test_get_attribute_names_with_prefix_returns_null_when_in_closing_tag() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $p->next_tag( 'div' ); + $p->next_tag( array( 'tag_closers' => 'visit' ) ); + $this->assertNull( $p->get_attribute_names_with_prefix( 'data-' ), 'Accessing attributes of a closing tag did not return null' ); + } + + /** + * @ticket 56299 + * + * @covers get_attribute_names_with_prefix + */ + public function test_get_attribute_names_with_prefix_returns_empty_array_when_no_attributes_present() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $p->next_tag( 'div' ); + $this->assertSame( array(), $p->get_attribute_names_with_prefix( 'data-' ), 'Accessing the attributes on a tag without any did not return an empty array' ); + } + + /** + * @ticket 56299 + * + * @covers get_attribute_names_with_prefix + */ + public function test_get_attribute_names_with_prefix_returns_matching_attribute_names_in_lowercase() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $p->next_tag(); + $this->assertSame( + array( 'data-enabled', 'data-test-id' ), + $p->get_attribute_names_with_prefix( 'data-' ) + ); + } + + /** + * @ticket 56299 + * + * @covers set_attribute + * @covers get_updated_html + * @covers get_attribute_names_with_prefix + */ + public function test_get_attribute_names_with_prefix_returns_attribute_added_by_set_attribute() { + $p = new WP_HTML_Tag_Processor( '
Test
' ); + $p->next_tag(); + $p->set_attribute( 'data-test-id', '14' ); + $this->assertSame( + '
Test
', + $p->get_updated_html(), + "Updated HTML doesn't include attribute added via set_attribute" + ); + $this->assertSame( + array( 'data-test-id', 'data-foo' ), + $p->get_attribute_names_with_prefix( 'data-' ), + "Accessing attribute names doesn't find attribute added via set_attribute" + ); + } + /** * @ticket 56299 * From 727797c9e5c6cc034fcb18011b72612509def708 Mon Sep 17 00:00:00 2001 From: Bogdan Ungureanu Date: Wed, 11 Jan 2023 01:17:39 +0200 Subject: [PATCH 02/12] Fixed Global Styles variables for colors, font family, gradient, fontSize (#46944) --- .../src/components/global-styles/utils.js | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/utils.js b/packages/edit-site/src/components/global-styles/utils.js index 14f3b868294172..e3c3bf47d4a5a3 100644 --- a/packages/edit-site/src/components/global-styles/utils.js +++ b/packages/edit-site/src/components/global-styles/utils.js @@ -90,11 +90,42 @@ export const STYLE_PATH_TO_CSS_VAR_INFIX = { 'color.background': 'color', 'color.text': 'color', 'elements.link.color.text': 'color', + 'elements.link.:hover.color.text': 'color', + 'elements.link.typography.fontFamily': 'font-family', + 'elements.link.typography.fontSize': 'font-size', 'elements.button.color.text': 'color', - 'elements.button.backgroundColor': 'background-color', + 'elements.button.color.background': 'color', + 'elements.button.typography.fontFamily': 'font-family', + 'elements.button.typography.fontSize': 'font-size', 'elements.heading.color': 'color', - 'elements.heading.backgroundColor': 'background-color', + 'elements.heading.color.background': 'color', + 'elements.heading.typography.fontFamily': 'font-family', 'elements.heading.gradient': 'gradient', + 'elements.heading.color.gradient': 'gradient', + 'elements.h1.color': 'color', + 'elements.h1.color.background': 'color', + 'elements.h1.typography.fontFamily': 'font-family', + 'elements.h1.color.gradient': 'gradient', + 'elements.h2.color': 'color', + 'elements.h2.color.background': 'color', + 'elements.h2.typography.fontFamily': 'font-family', + 'elements.h2.color.gradient': 'gradient', + 'elements.h3.color': 'color', + 'elements.h3.color.background': 'color', + 'elements.h3.typography.fontFamily': 'font-family', + 'elements.h3.color.gradient': 'gradient', + 'elements.h4.color': 'color', + 'elements.h4.color.background': 'color', + 'elements.h4.typography.fontFamily': 'font-family', + 'elements.h4.color.gradient': 'gradient', + 'elements.h5.color': 'color', + 'elements.h5.color.background': 'color', + 'elements.h5.typography.fontFamily': 'font-family', + 'elements.h5.color.gradient': 'gradient', + 'elements.h6.color': 'color', + 'elements.h6.color.background': 'color', + 'elements.h6.typography.fontFamily': 'font-family', + 'elements.h6.color.gradient': 'gradient', 'color.gradient': 'gradient', 'typography.fontSize': 'font-size', 'typography.fontFamily': 'font-family', From 38eac5d036ee48d2b7d128ffb9c89e7a9326909c Mon Sep 17 00:00:00 2001 From: Juan Aldasoro Date: Wed, 11 Jan 2023 00:30:01 +0100 Subject: [PATCH 03/12] Remove the border property from the body element on previews. (#46946) * Remove the border property from the body element on previews. * Apply changes from cr. --- packages/block-editor/src/components/block-preview/auto.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-preview/auto.js b/packages/block-editor/src/components/block-preview/auto.js index a71c622a6457ca..fb763ed466629c 100644 --- a/packages/block-editor/src/components/block-preview/auto.js +++ b/packages/block-editor/src/components/block-preview/auto.js @@ -48,7 +48,7 @@ function ScaledBlockPreview( { ...styles, ...__experimentalStyles, { - css: 'body{height:auto;overflow:hidden;}', + css: 'body{height:auto;overflow:hidden;border:none;}', __unstableType: 'presets', }, ]; From 9764ee77d87ec000bf478cba9c5dc60709c97127 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 11 Jan 2023 10:33:08 +1100 Subject: [PATCH 04/12] Design Tools: Add a Position block support (including sticky), decoupled from layout (#46142) * Try: Add Position support, decoupled from Layout * Try a CustomSelectControl for the position input * Try moving the control to a separate Position PanelBody * Add Position group, ensure Position panel always appears just before Advanced * Roll in fix for toolbar position * Remove no longer needed change to block popover * Add scrollContainer to deps array * Remove redundant PHP code * Switch Static to Default, display help text after selection, update text * Remove help text, update logic to gracefully handle illegal values * Try ensuring sticky/fixed blocks always flip the toolbar position * Ensure position UI and style output is only applied on themes that support it, tidy up comments * Clarify TODO comment for z-index values --- docs/reference-guides/core-blocks.md | 2 +- lib/block-supports/position.php | 143 +++++++ lib/class-wp-theme-json-gutenberg.php | 9 +- lib/compat/wordpress-6.2/blocks.php | 26 ++ lib/load.php | 2 + .../src/components/block-inspector/index.js | 2 + .../use-block-toolbar-popover-props.js | 80 +++- .../position-controls-panel.js | 37 ++ .../inspector-controls-tabs/settings-tab.js | 2 + .../use-inspector-controls-tabs.js | 2 + .../components/inspector-controls/groups.js | 2 + packages/block-editor/src/hooks/index.js | 1 + packages/block-editor/src/hooks/position.js | 375 ++++++++++++++++++ packages/block-editor/src/hooks/position.scss | 18 + packages/block-editor/src/style.scss | 1 + packages/block-library/src/group/block.json | 3 + 16 files changed, 691 insertions(+), 14 deletions(-) create mode 100644 lib/block-supports/position.php create mode 100644 lib/compat/wordpress-6.2/blocks.php create mode 100644 packages/block-editor/src/components/inspector-controls-tabs/position-controls-panel.js create mode 100644 packages/block-editor/src/hooks/position.js create mode 100644 packages/block-editor/src/hooks/position.scss 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 46d9e442f3a5b7..147d28173a7e3a 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 12e8608250f383..deee5efdb62f9b 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, From e5cae9d27b4351bc67595e6b286796d811fc9626 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 11 Jan 2023 10:44:04 +1100 Subject: [PATCH 05/12] Update push changes to global styles label text to "apply globally" (#46965) * Global styles: Update label of button that pushes local block styles to global styles * Update label to say Apply globally * Updates tests and snackbar notification text * Update help text and snackbar text * Snackbar label * Update test text for snackbar to match Co-authored-by: James Koster --- .../src/hooks/push-changes-to-global-styles/index.js | 6 +++--- .../specs/site-editor/push-to-global-styles.spec.js | 12 +++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js index 70b9fa1b02f8ea..6d29de4fdc9eed 100644 --- a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js +++ b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js @@ -97,7 +97,7 @@ function PushChangesToGlobalStylesControl( { createSuccessNotice( sprintf( // translators: %s: Title of the block e.g. 'Heading'. - __( 'Pushed styles to all %s blocks.' ), + __( '%s styles applied.' ), getBlockType( name ).title ), { @@ -124,7 +124,7 @@ function PushChangesToGlobalStylesControl( { help={ sprintf( // translators: %s: Title of the block e.g. 'Heading'. __( - 'Move this block’s typography, spacing, dimensions, and color styles to all %s blocks.' + 'Apply this block’s typography, spacing, dimensions, and color styles to all %s blocks.' ), getBlockType( name ).title ) } @@ -137,7 +137,7 @@ function PushChangesToGlobalStylesControl( { disabled={ changes.length === 0 } onClick={ pushChanges } > - { __( 'Push changes to Global Styles' ) } + { __( 'Apply globally' ) } ); diff --git a/test/e2e/specs/site-editor/push-to-global-styles.spec.js b/test/e2e/specs/site-editor/push-to-global-styles.spec.js index ce854f476e4f9d..7e9d3a07807f1c 100644 --- a/test/e2e/specs/site-editor/push-to-global-styles.spec.js +++ b/test/e2e/specs/site-editor/push-to-global-styles.spec.js @@ -59,7 +59,7 @@ test.describe( 'Push to Global Styles button', () => { // Push button should be disabled await expect( page.getByRole( 'button', { - name: 'Push changes to Global Styles', + name: 'Apply globally', } ) ).toBeDisabled(); @@ -69,27 +69,25 @@ test.describe( 'Push to Global Styles button', () => { // Push button should now be enabled await expect( page.getByRole( 'button', { - name: 'Push changes to Global Styles', + name: 'Apply globally', } ) ).toBeEnabled(); // Press the Push button - await page - .getByRole( 'button', { name: 'Push changes to Global Styles' } ) - .click(); + await page.getByRole( 'button', { name: 'Apply globally' } ).click(); // Snackbar notification should appear await expect( page.getByRole( 'button', { name: 'Dismiss this notice', - text: 'Pushed styles to all Heading blocks.', + text: 'Heading styles applied.', } ) ).toBeVisible(); // Push button should be disabled again await expect( page.getByRole( 'button', { - name: 'Push changes to Global Styles', + name: 'Apply globally', } ) ).toBeDisabled(); From 297565c13e84b07d17b9d08b1066927190a9f2bd Mon Sep 17 00:00:00 2001 From: Andy Peatling Date: Tue, 10 Jan 2023 19:54:42 -0800 Subject: [PATCH 06/12] When adding a new template part, check existing template part titles to ensure that there is no duplicate title. If there is, add a suffix. (#46996) --- .../add-new-template/new-template-part.js | 24 ++++++++++++++++++- .../src/components/add-new-template/utils.js | 14 +++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/add-new-template/new-template-part.js b/packages/edit-site/src/components/add-new-template/new-template-part.js index 578cfa515bd2c2..377a5c56f7fe55 100644 --- a/packages/edit-site/src/components/add-new-template/new-template-part.js +++ b/packages/edit-site/src/components/add-new-template/new-template-part.js @@ -20,6 +20,7 @@ import { plus } from '@wordpress/icons'; import { useHistory } from '../routes'; import { store as editSiteStore } from '../../store'; import CreateTemplatePartModal from '../create-template-part-modal'; +import { useExistingTemplateParts } from './utils'; export default function NewTemplatePart( { postType, @@ -31,6 +32,7 @@ export default function NewTemplatePart( { const { createErrorNotice } = useDispatch( noticesStore ); const { saveEntityRecord } = useDispatch( coreStore ); const { __unstableSetCanvasMode } = useDispatch( editSiteStore ); + const existingTemplateParts = useExistingTemplateParts(); async function createTemplatePart( { title, area } ) { if ( ! title ) { @@ -40,6 +42,26 @@ export default function NewTemplatePart( { return; } + const uniqueTitle = () => { + const lowercaseTitle = title.toLowerCase(); + const existingTitles = existingTemplateParts.map( + ( templatePart ) => templatePart.title.rendered.toLowerCase() + ); + + if ( ! existingTitles.includes( lowercaseTitle ) ) { + return title; + } + + let suffix = 2; + while ( + existingTitles.includes( `${ lowercaseTitle } ${ suffix }` ) + ) { + suffix++; + } + + return `${ title } ${ suffix }`; + }; + try { // Currently template parts only allow latin chars. // Fallback slug will receive suffix by default. @@ -52,7 +74,7 @@ export default function NewTemplatePart( { 'wp_template_part', { slug: cleanSlug, - title, + title: uniqueTitle(), content: '', area, }, diff --git a/packages/edit-site/src/components/add-new-template/utils.js b/packages/edit-site/src/components/add-new-template/utils.js index 7309f2ff711ca9..03bd184a8af609 100644 --- a/packages/edit-site/src/components/add-new-template/utils.js +++ b/packages/edit-site/src/components/add-new-template/utils.js @@ -52,6 +52,20 @@ export const useExistingTemplates = () => { ); }; +export const useExistingTemplateParts = () => { + return useSelect( + ( select ) => + select( coreStore ).getEntityRecords( + 'postType', + 'wp_template_part', + { + per_page: -1, + } + ), + [] + ); +}; + export const useDefaultTemplateTypes = () => { return useSelect( ( select ) => From a4879a68914e63b1cb9a51f6d18680463ac2b9f2 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Wed, 11 Jan 2023 16:56:33 +1100 Subject: [PATCH 07/12] Edit block style variations from global styles (#46343) * Edit block style variations from global styles * It's not working yet but we'll get there * Hardcoded a bunch of stuff to see if it works * Add back empty line * update panel paths to use encodeURIComponent * Move updated sanitize function to 6.2 folder * Output CSS for edited style variations * Any [a-z]* style variation name should be possible * Fix test failures * Add handling of feature selectors * Add colors and typography * Re-add changes to class-wp-theme-json * Fix color paths * Add dimensions styles * Bump up front end specificity. * Only core variations should be editable * Basic functionality to create a new variation * Revert adding new variations for now * Add preview panel to variation screen. * ItemGroup UI * Fix color screen bug * Disable editing on default styles. * Fix variation showing on general block preview * Fix previews for design tools screens inside variations. * Address feedback. * Remove default variation from panel. * Default style should always be called Default. --- lib/class-wp-theme-json-gutenberg.php | 93 +++++++++++-- .../src/components/block-styles/index.js | 5 +- .../global-styles/block-preview-panel.js | 16 ++- .../components/global-styles/border-panel.js | 12 +- .../components/global-styles/context-menu.js | 6 + .../global-styles/dimensions-panel.js | 47 ++++--- .../global-styles/screen-background-color.js | 17 ++- .../global-styles/screen-block-list.js | 7 +- .../components/global-styles/screen-border.js | 11 +- .../global-styles/screen-button-color.js | 4 +- .../components/global-styles/screen-colors.js | 103 ++++++++++++--- .../global-styles/screen-heading-color.js | 14 +- .../components/global-styles/screen-layout.js | 14 +- .../global-styles/screen-link-color.js | 26 +++- .../global-styles/screen-text-color.js | 10 +- .../global-styles/screen-typography.js | 15 ++- .../global-styles/screen-variations.js | 47 +++++++ .../src/components/global-styles/style.scss | 9 ++ .../test/use-global-styles-output.js | 2 +- .../global-styles/typography-panel.js | 39 ++++-- .../src/components/global-styles/ui.js | 114 ++++++++++++++-- .../global-styles/use-global-styles-output.js | 125 +++++++++++++++++- .../src/components/global-styles/utils.js | 13 ++ .../global-styles/variations-panel.js | 78 +++++++++++ schemas/json/theme.json | 11 ++ 25 files changed, 726 insertions(+), 112 deletions(-) create mode 100644 packages/edit-site/src/components/global-styles/screen-variations.js create mode 100644 packages/edit-site/src/components/global-styles/variations-panel.js diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 982832f33722c6..9acef6006d4e68 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -720,9 +720,17 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_blocks = array(); $schema_settings_blocks = array(); foreach ( $valid_block_names as $block ) { - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; - $schema_styles_blocks[ $block ] = $styles_non_top_level; - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; + // Build the schema for each block style variation. + $style_variation_names = isset( $input['styles']['blocks'][ $block ]['variations'] ) ? array_keys( $input['styles']['blocks'][ $block ]['variations'] ) : array(); + $schema_styles_variations = array(); + if ( ! empty( $style_variation_names ) ) { + $schema_styles_variations = array_fill_keys( $style_variation_names, $styles_non_top_level ); + } + + $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; + $schema_styles_blocks[ $block ] = $styles_non_top_level; + $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; + $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations; } $schema['styles'] = static::VALID_STYLES; @@ -870,6 +878,16 @@ protected static function get_blocks_metadata() { } static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector ); } + + // If the block has style variations, append their selectors to the block metadata. + if ( ! empty( $block_type->styles ) ) { + $style_selectors = array(); + foreach ( $block_type->styles as $style ) { + // The style variation classname is duplicated in the selector to ensure that it overrides core block styles. + $style_selectors[ $style['name'] ] = static::append_to_selector( '.is-style-' . $style['name'] . '.is-style-' . $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); + } + static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; + } } return static::$blocks_metadata; @@ -2152,12 +2170,23 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { $feature_selectors = $selectors[ $name ]['features']; } + $variation_selectors = array(); + if ( isset( $node['variations'] ) ) { + foreach ( $node['variations'] as $variation => $node ) { + $variation_selectors[] = array( + 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ), + 'selector' => $selectors[ $name ]['styleVariations'][ $variation ], + ); + } + } + $nodes[] = array( - 'name' => $name, - 'path' => array( 'styles', 'blocks', $name ), - 'selector' => $selector, - 'duotone' => $duotone_selector, - 'features' => $feature_selectors, + 'name' => $name, + 'path' => array( 'styles', 'blocks', $name ), + 'selector' => $selector, + 'duotone' => $duotone_selector, + 'features' => $feature_selectors, + 'variations' => $variation_selectors, ); if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { @@ -2237,6 +2266,49 @@ public function get_styles_for_block( $block_metadata ) { } } + // If there are style variations, generate the declarations for them, including any feature selectors the block may have. + $style_variation_declarations = array(); + if ( ! empty( $block_metadata['variations'] ) ) { + foreach ( $block_metadata['variations'] as $style_variation ) { + $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); + $style_variation_selector = $style_variation['selector']; + + // If the block has feature selectors, generate the declarations for them within the current style variation. + if ( ! empty( $block_metadata['features'] ) ) { + foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) { + if ( ! empty( $style_variation_node[ $feature_name ] ) ) { + // Prepend the variation selector to the feature selector. + $split_feature_selectors = explode( ',', $feature_selector ); + $feature_selectors = array_map( + function( $split_feature_selector ) use ( $style_variation_selector ) { + return trim( $style_variation_selector ) . trim( $split_feature_selector ); + }, + $split_feature_selectors + ); + $combined_feature_selectors = implode( ',', $feature_selectors ); + + // Compute declarations for the feature. + $new_feature_declarations = static::compute_style_properties( array( $feature_name => $style_variation_node[ $feature_name ] ), $settings, null, $this->theme_json ); + + // Merge new declarations with any that already exist for + // the feature selector. This may occur when multiple block + // support features use the same custom selector. + if ( isset( $style_variation_declarations[ $combined_feature_selectors ] ) ) { + $style_variation_declarations[ $combined_feature_selectors ] = array_merge( $style_variation_declarations[ $combined_feature_selectors ], $new_feature_declarations ); + } else { + $style_variation_declarations[ $combined_feature_selectors ] = $new_feature_declarations; + } + // Remove the feature from the variation's node now the + // styles will be included under the feature level selector. + unset( $style_variation_node[ $feature_name ] ); + } + } + } + // Compute declarations for remaining styles not covered by feature level selectors. + $style_variation_declarations[ $style_variation_selector ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json ); + } + } + /* * Get a reference to element name from path. * $block_metadata['path'] = array( 'styles','elements','link' ); @@ -2327,6 +2399,11 @@ function( $pseudo_selector ) use ( $selector ) { $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations ); } + // 6. Generate and append the style variation rulesets. + foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) { + $block_rules .= static::to_ruleset( $style_variation_selector, $individual_style_variation_declarations ); + } + return $block_rules; } diff --git a/packages/block-editor/src/components/block-styles/index.js b/packages/block-editor/src/components/block-styles/index.js index a4c45dea8c14e1..7b9cbc72691985 100644 --- a/packages/block-editor/src/components/block-styles/index.js +++ b/packages/block-editor/src/components/block-styles/index.js @@ -14,6 +14,7 @@ import { Popover, } from '@wordpress/components'; import deprecated from '@wordpress/deprecated'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -64,7 +65,9 @@ function BlockStyles( { clientId, onSwitch = noop, onHoverClassName = noop } ) {
{ stylesToRender.map( ( style ) => { - const buttonText = style.label || style.name; + const buttonText = style.isDefault + ? __( 'Default' ) + : style.label || style.name; return (