diff --git a/backport-changelog/6.7/7552.md b/backport-changelog/6.7/7552.md new file mode 100644 index 0000000000000..d304162b3cae6 --- /dev/null +++ b/backport-changelog/6.7/7552.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7552 + +* https://github.com/WordPress/gutenberg/pull/66058 diff --git a/backport-changelog/6.7/7561.md b/backport-changelog/6.7/7561.md new file mode 100644 index 0000000000000..43fcffa41c02a --- /dev/null +++ b/backport-changelog/6.7/7561.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7561 + +* https://github.com/WordPress/gutenberg/pull/66084 diff --git a/backport-changelog/6.8/7575.md b/backport-changelog/6.8/7575.md new file mode 100644 index 0000000000000..f1c6a84da1368 --- /dev/null +++ b/backport-changelog/6.8/7575.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7575 + +* https://github.com/WordPress/gutenberg/pull/66154 diff --git a/docs/how-to-guides/data-basics/3-building-an-edit-form.md b/docs/how-to-guides/data-basics/3-building-an-edit-form.md index 65c4d0a5486a0..5f4e30357798d 100644 --- a/docs/how-to-guides/data-basics/3-building-an-edit-form.md +++ b/docs/how-to-guides/data-basics/3-building-an-edit-form.md @@ -539,5 +539,5 @@ function EditPageForm( { pageId, onCancel, onSaveFinished } ) { ## What's next? * **Previous part:** [Building a list of pages](/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md) -* **Next part:** Building a *New Page* form (coming soon) +* **Next part:** [Building a Create Page form](/docs/how-to-guides/data-basics/4-building-a-create-page-form.md) * (optional) Review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository diff --git a/docs/how-to-guides/data-basics/4-building-a-create-page-form.md b/docs/how-to-guides/data-basics/4-building-a-create-page-form.md index 33c6e9a5ccff5..9011596e0fedd 100644 --- a/docs/how-to-guides/data-basics/4-building-a-create-page-form.md +++ b/docs/how-to-guides/data-basics/4-building-a-create-page-form.md @@ -1,4 +1,4 @@ -# Part 4: Building a Create page form +# Building a Create page form In the [previous part](/docs/how-to-guides/data-basics/3-building-an-edit-form.md) we created an *Edit page* feature, and in this part we will add a *Create page* feature. Here's a glimpse of what we're going to build: diff --git a/docs/manifest.json b/docs/manifest.json index 8387b9079694c..5e94c6b83b70d 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -198,7 +198,7 @@ "parent": "data-basics" }, { - "title": "Part 4: Building a Create page form", + "title": "Building a Create page form", "slug": "4-building-a-create-page-form", "markdown_source": "../docs/how-to-guides/data-basics/4-building-a-create-page-form.md", "parent": "data-basics" diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 8cd83e007f37c..98d0719cd24a9 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -2732,17 +2732,13 @@ private static function update_separator_declarations( $declarations ) { * @return array The block nodes in theme.json. */ private static function get_block_nodes( $theme_json, $selectors = array(), $options = array() ) { - $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors; - $nodes = array(); - if ( ! isset( $theme_json['styles'] ) ) { - return $nodes; - } + $nodes = array(); - // Blocks. if ( ! isset( $theme_json['styles']['blocks'] ) ) { return $nodes; } + $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors; $include_variations = $options['include_block_style_variations'] ?? false; $include_node_paths_only = $options['include_node_paths_only'] ?? false; diff --git a/lib/client-assets.php b/lib/client-assets.php index 2343530e5595a..3e4f79cd3c1f7 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -630,7 +630,7 @@ function gutenberg_default_script_modules() { switch ( $script_module_id ) { /* * Interactivity exposes two entrypoints, "/index" and "/debug". - * "/debug" should replalce "/index" in devlopment. + * "/debug" should replace "/index" in development. */ case '@wordpress/interactivity/debug': if ( ! SCRIPT_DEBUG ) { diff --git a/lib/compat/wordpress-6.7/block-bindings.php b/lib/compat/wordpress-6.7/block-bindings.php index a8f68c0f0f04e..70ba523ac966e 100644 --- a/lib/compat/wordpress-6.7/block-bindings.php +++ b/lib/compat/wordpress-6.7/block-bindings.php @@ -6,38 +6,28 @@ */ /** - * Adds the block bindings sources registered in the server to the editor settings. - * - * This allows them to be bootstrapped in the editor. - * - * @param array $settings The block editor settings from the `block_editor_settings_all` filter. - * @return array The editor settings including the block bindings sources. + * Bootstrap the block bindings sources registered in the server. */ -function gutenberg_add_server_block_bindings_sources_to_editor_settings( $editor_settings ) { - // Check if the sources are already exposed in the editor settings. - if ( isset( $editor_settings['blockBindingsSources'] ) ) { - return $editor_settings; - } - - $registered_block_bindings_sources = get_all_registered_block_bindings_sources(); - if ( ! empty( $registered_block_bindings_sources ) ) { - // Initialize array. - $editor_settings['blockBindingsSources'] = array(); - foreach ( $registered_block_bindings_sources as $source_name => $source_properties ) { - // Add source with the label to editor settings. - $editor_settings['blockBindingsSources'][ $source_name ] = array( - 'label' => $source_properties->label, +function gutenberg_bootstrap_server_block_bindings_sources() { + $registered_sources = get_all_registered_block_bindings_sources(); + if ( ! empty( $registered_sources ) ) { + $filtered_sources = array(); + foreach ( $registered_sources as $source ) { + $filtered_sources[] = array( + 'name' => $source->name, + 'label' => $source->label, + 'usesContext' => $source->uses_context, ); - // Add `usesContext` property if exists. - if ( ! empty( $source_properties->uses_context ) ) { - $editor_settings['blockBindingsSources'][ $source_name ]['usesContext'] = $source_properties->uses_context; - } } + $script = sprintf( 'for ( const source of %s ) { ! wp.blocks.getBlockBindingsSource( source.name ) && wp.blocks.registerBlockBindingsSource( source ); }', wp_json_encode( $filtered_sources ) ); + wp_add_inline_script( + 'wp-blocks', + $script + ); } - return $editor_settings; } -add_filter( 'block_editor_settings_all', 'gutenberg_add_server_block_bindings_sources_to_editor_settings', 10 ); +add_action( 'enqueue_block_editor_assets', 'gutenberg_bootstrap_server_block_bindings_sources', 5 ); /** * Initialize `canUpdateBlockBindings` editor setting if it doesn't exist. By default, it is `true` only for admin users. diff --git a/lib/init.php b/lib/init.php index 88dcba4525f6e..13ca26d4b9e83 100644 --- a/lib/init.php +++ b/lib/init.php @@ -51,7 +51,7 @@ function gutenberg_menu() { 'gutenberg', __( 'Experiments Settings', 'gutenberg' ), __( 'Experiments', 'gutenberg' ), - 'edit_posts', + 'manage_options', 'gutenberg-experiments', 'the_gutenberg_experiments' ); diff --git a/packages/base-styles/_animations.scss b/packages/base-styles/_animations.scss index 87e5f035f46a6..e5bbf86375735 100644 --- a/packages/base-styles/_animations.scss +++ b/packages/base-styles/_animations.scss @@ -36,11 +36,6 @@ @include reduce-motion("animation"); } -@mixin editor-canvas-resize-animation() { - transition: all 0.4s cubic-bezier(0.46, 0.03, 0.52, 0.96); - @include reduce-motion("transition"); -} - // Deprecated @mixin edit-post__fade-in-animation($speed: 0.08s, $delay: 0s) { @warn "The `edit-post__fade-in-animation` mixin is deprecated. Use `animation__fade-in` instead."; diff --git a/packages/block-editor/src/components/block-canvas/style.scss b/packages/block-editor/src/components/block-canvas/style.scss index 1395b5c0a437d..9e924cb79bace 100644 --- a/packages/block-editor/src/components/block-canvas/style.scss +++ b/packages/block-editor/src/components/block-canvas/style.scss @@ -4,5 +4,4 @@ iframe[name="editor-canvas"] { height: 100%; display: block; background-color: transparent; - @include editor-canvas-resize-animation; } diff --git a/packages/block-editor/src/components/block-list/content.scss b/packages/block-editor/src/components/block-list/content.scss index 6a88813b0c604..b42ebe349d599 100644 --- a/packages/block-editor/src/components/block-list/content.scss +++ b/packages/block-editor/src/components/block-list/content.scss @@ -404,14 +404,15 @@ _::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-b display: flex; align-items: center; justify-content: center; + overflow: hidden; font-size: $default-font-size; font-family: $default-font; color: $black; font-weight: normal; .is-zoomed-out & { - // Scale the font size based on the zoom level. - font-size: calc(#{$default-font-size} * ( 2 - var(--wp-block-editor-iframe-zoom-out-scale) )); + // Maintains an absolute font-size by counter-scaling based on the zoom level. + font-size: calc(#{$default-font-size} / var(--wp-block-editor-iframe-zoom-out-scale)); } &.is-dragged-over { diff --git a/packages/block-editor/src/components/block-list/zoom-out-separator.js b/packages/block-editor/src/components/block-list/zoom-out-separator.js index 9e0d087c2267c..f2e6d050141fb 100644 --- a/packages/block-editor/src/components/block-list/zoom-out-separator.js +++ b/packages/block-editor/src/components/block-list/zoom-out-separator.js @@ -101,14 +101,15 @@ export function ZoomOutSeparator( { { isVisible && ( { __( 'Drop pattern.' ) } diff --git a/packages/block-editor/src/components/block-popover/index.js b/packages/block-editor/src/components/block-popover/index.js index 47022e336e486..637ab1cb8a53e 100644 --- a/packages/block-editor/src/components/block-popover/index.js +++ b/packages/block-editor/src/components/block-popover/index.js @@ -8,6 +8,7 @@ import clsx from 'clsx'; */ import { useMergeRefs } from '@wordpress/compose'; import { Popover } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; import { forwardRef, useMemo, @@ -21,6 +22,8 @@ import { import { useBlockElement } from '../block-list/use-block-props/use-block-refs'; import usePopoverScroll from './use-popover-scroll'; import { rectUnion, getVisibleElementBounds } from '../../utils/dom'; +import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; const MAX_POPOVER_RECOMPUTE_COUNTER = Number.MAX_SAFE_INTEGER; @@ -74,12 +77,38 @@ function BlockPopover( }; }, [ selectedElement ] ); + const { isZoomOut, parentSectionBlock, isSectionSelected } = useSelect( + ( select ) => { + const { + isZoomOut: isZoomOutSelector, + getSectionRootClientId, + getParentSectionBlock, + getBlockOrder, + } = unlock( select( blockEditorStore ) ); + + return { + isZoomOut: isZoomOutSelector(), + parentSectionBlock: + getParentSectionBlock( clientId ) ?? clientId, + isSectionSelected: getBlockOrder( + getSectionRootClientId() + ).includes( clientId ), + }; + }, + [ clientId ] + ); + + // This element is used to position the zoom out view vertical toolbar + // correctly, relative to the selected section. + const parentSectionElement = useBlockElement( parentSectionBlock ); + const popoverAnchor = useMemo( () => { if ( // popoverDimensionsRecomputeCounter is by definition always equal or greater // than 0. This check is only there to satisfy the correctness of the // exhaustive-deps rule for the `useMemo` hook. popoverDimensionsRecomputeCounter < 0 || + ( isZoomOut && ! parentSectionElement ) || ! selectedElement || ( bottomClientId && ! lastSelectedElement ) ) { @@ -88,6 +117,35 @@ function BlockPopover( return { getBoundingClientRect() { + // The zoom out view has a vertical block toolbar that should always + // be on the edge of the canvas, aligned to the top of the currently + // selected section. This condition changes the anchor of the toolbar + // to the section instead of the block to handle blocks that are + // not full width and nested blocks to keep section height. + if ( isZoomOut && isSectionSelected ) { + // Compute the height based on the parent section of the + // selected block, because the selected block may be + // shorter than the section. + const canvasElementRect = getVisibleElementBounds( + __unstableContentRef.current + ); + const parentSectionElementRect = + getVisibleElementBounds( parentSectionElement ); + const anchorHeight = + parentSectionElementRect.bottom - + parentSectionElementRect.top; + + // Always use the width of the section root element to make sure + // the toolbar is always on the edge of the canvas. + const anchorWidth = canvasElementRect.width; + return new window.DOMRectReadOnly( + canvasElementRect.left, + parentSectionElementRect.top, + anchorWidth, + anchorHeight + ); + } + return lastSelectedElement ? rectUnion( getVisibleElementBounds( selectedElement ), @@ -98,10 +156,14 @@ function BlockPopover( contextElement: selectedElement, }; }, [ + popoverDimensionsRecomputeCounter, + isZoomOut, + parentSectionElement, + selectedElement, bottomClientId, lastSelectedElement, - selectedElement, - popoverDimensionsRecomputeCounter, + isSectionSelected, + __unstableContentRef, ] ); if ( ! selectedElement || ( bottomClientId && ! lastSelectedElement ) ) { diff --git a/packages/block-editor/src/components/block-tools/use-show-block-tools.js b/packages/block-editor/src/components/block-tools/use-show-block-tools.js index 02a8f0583bcdd..97737b4ec2f5b 100644 --- a/packages/block-editor/src/components/block-tools/use-show-block-tools.js +++ b/packages/block-editor/src/components/block-tools/use-show-block-tools.js @@ -8,6 +8,7 @@ import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; * Internal dependencies */ import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; /** * Source of truth for which block tools are showing in the block editor. @@ -24,7 +25,9 @@ export function useShowBlockTools() { getSettings, __unstableGetEditorMode, isTyping, - } = select( blockEditorStore ); + getBlockOrder, + getSectionRootClientId, + } = unlock( select( blockEditorStore ) ); const clientId = getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); @@ -42,10 +45,14 @@ export function useShowBlockTools() { editorMode === 'edit' && isEmptyDefaultBlock; const isZoomOut = editorMode === 'zoom-out'; + const isSectionSelected = getBlockOrder( + getSectionRootClientId() + ).includes( clientId ); const _showZoomOutToolbar = + clientId && isZoomOut && - block?.attributes?.align === 'full' && - ! _showEmptyBlockSideInserter; + ! _showEmptyBlockSideInserter && + isSectionSelected; const _showBlockToolbarPopover = ! _showZoomOutToolbar && ! getSettings().hasFixedToolbar && diff --git a/packages/block-editor/src/components/block-tools/zoom-out-popover.js b/packages/block-editor/src/components/block-tools/zoom-out-popover.js index a1f2990a5cc1e..7a5c2243cf054 100644 --- a/packages/block-editor/src/components/block-tools/zoom-out-popover.js +++ b/packages/block-editor/src/components/block-tools/zoom-out-popover.js @@ -5,7 +5,7 @@ import clsx from 'clsx'; /** * Internal dependencies */ -import BlockPopover from '../block-popover'; +import { PrivateBlockPopover as BlockPopover } from '../block-popover'; import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; import useSelectedBlockToolProps from './use-selected-block-tool-props'; import ZoomOutToolbar from './zoom-out-toolbar'; @@ -29,6 +29,7 @@ export default function ZoomOutPopover( { clientId, __unstableContentRef } ) { return ( { if ( ! isZoomedOut ) { - prevContainerWidthRef.current = containerWidth; + initialContainerWidth.current = containerWidth; } }, [ containerWidth, isZoomedOut ] ); @@ -298,14 +298,49 @@ function Iframe( { useEffect( () => cleanup, [ cleanup ] ); + const zoomOutAnimationClassnameRef = useRef( null ); + const handleZoomOutAnimationClassname = () => { + clearTimeout( zoomOutAnimationClassnameRef.current ); + + iframeDocument.documentElement.classList.add( 'zoom-out-animation' ); + + zoomOutAnimationClassnameRef.current = setTimeout( () => { + iframeDocument.documentElement.classList.remove( + 'zoom-out-animation' + ); + }, 400 ); // 400ms should match the animation speed used in components/iframe/content.scss + }; + + // Toggle zoom out CSS Classes only when zoom out mode changes. We could add these into the useEffect + // that controls settings the CSS variables, but then we would need to do more work to ensure we're + // only toggling these when the zoom out mode changes, as that useEffect is also triggered by a large + // number of dependencies. useEffect( () => { if ( ! iframeDocument || ! isZoomedOut ) { return; } + handleZoomOutAnimationClassname(); iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); + return () => { + handleZoomOutAnimationClassname(); + iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); + }; + }, [ iframeDocument, isZoomedOut ] ); + + // Calculate the scaling and CSS variables for the zoom out canvas + useEffect( () => { + if ( ! iframeDocument || ! isZoomedOut ) { + return; + } + const maxWidth = 750; + // Note: When we initialize the zoom out when the canvas is smaller (sidebars open), + // initialContainerWidth will be smaller than the full page, and reflow will happen + // when the canvas area becomes larger due to sidebars closing. This is a known but + // minor divergence for now. + // This scaling calculation has to happen within the JS because CSS calc() can // only divide and multiply by a unitless value. I.e. calc( 100px / 2 ) is valid // but calc( 100px / 2px ) is not. @@ -314,7 +349,10 @@ function Iframe( { scale === 'default' ? ( Math.min( containerWidth, maxWidth ) - parseInt( frameSize ) * 2 ) / - prevContainerWidthRef.current + Math.max( + initialContainerWidth.current, + containerWidth + ) : scale ); @@ -336,13 +374,16 @@ function Iframe( { `${ containerWidth }px` ); iframeDocument.documentElement.style.setProperty( - '--wp-block-editor-iframe-zoom-out-prev-container-width', - `${ prevContainerWidthRef.current }px` + '--wp-block-editor-iframe-zoom-out-outer-container-width', + `${ Math.max( initialContainerWidth.current, containerWidth ) }px` ); - return () => { - iframeDocument.documentElement.classList.remove( 'is-zoomed-out' ); + // iframeDocument.documentElement.style.setProperty( + // '--wp-block-editor-iframe-zoom-out-outer-container-width', + // `${ Math.max( initialContainerWidth.current, containerWidth ) }px` + // ); + return () => { iframeDocument.documentElement.style.removeProperty( '--wp-block-editor-iframe-zoom-out-scale' ); @@ -359,7 +400,7 @@ function Iframe( { '--wp-block-editor-iframe-zoom-out-container-width' ); iframeDocument.documentElement.style.removeProperty( - '--wp-block-editor-iframe-zoom-out-prev-container-width' + '--wp-block-editor-iframe-zoom-out-outer-container-width' ); }; }, [ @@ -460,10 +501,12 @@ function Iframe( { isZoomedOut && 'is-zoomed-out' ) } style={ { - '--wp-block-editor-iframe-zoom-out-container-width': - isZoomedOut && `${ containerWidth }px`, - '--wp-block-editor-iframe-zoom-out-prev-container-width': - isZoomedOut && `${ prevContainerWidthRef.current }px`, + '--wp-block-editor-iframe-zoom-out-outer-container-width': + isZoomedOut && + `${ Math.max( + initialContainerWidth.current, + containerWidth + ) }px`, } } > { iframe } diff --git a/packages/block-editor/src/components/iframe/style.scss b/packages/block-editor/src/components/iframe/style.scss index dcddcdf0950a4..d05be2f3977b9 100644 --- a/packages/block-editor/src/components/iframe/style.scss +++ b/packages/block-editor/src/components/iframe/style.scss @@ -9,9 +9,10 @@ } .block-editor-iframe__scale-container.is-zoomed-out { - $container-width: var(--wp-block-editor-iframe-zoom-out-container-width, 100vw); - $prev-container-width: var(--wp-block-editor-iframe-zoom-out-prev-container-width, 100vw); - width: $prev-container-width; - // This is to offset the movement of the iframe when we open sidebars - margin-left: calc(-1 * (#{$prev-container-width} - #{$container-width}) / 2); + $outer-container-width: var(--wp-block-editor-iframe-zoom-out-outer-container-width, 100vw); + width: $outer-container-width; + // Position the iframe so that it is always aligned with the right side so that + // the scrollbar is always visible on the right side + position: absolute; + right: 0; } diff --git a/packages/block-editor/src/components/responsive-block-control/style.scss b/packages/block-editor/src/components/responsive-block-control/style.scss index 7f90e12dd87c6..e840a3bfcf436 100644 --- a/packages/block-editor/src/components/responsive-block-control/style.scss +++ b/packages/block-editor/src/components/responsive-block-control/style.scss @@ -1,6 +1,5 @@ @mixin screen-reader-text() { border: 0; - clip: rect(1px, 1px, 1px, 1px); clip-path: inset(50%); height: 1px; margin: -1px; diff --git a/packages/block-editor/src/components/rich-text/style.scss b/packages/block-editor/src/components/rich-text/style.scss index ca274d378d408..ba109648df142 100644 --- a/packages/block-editor/src/components/rich-text/style.scss +++ b/packages/block-editor/src/components/rich-text/style.scss @@ -9,12 +9,17 @@ margin-bottom: $grid-unit-10; box-shadow: none; outline: none; + border-radius: $radius-small; } .components-toolbar { border-radius: $radius-small; } + .components-toolbar-group { + background: none; + } + .components-toolbar__control, .components-dropdown-menu__toggle { min-width: $block-toolbar-height; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index a81d88de33466..2bfc52118d7ac 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2385,6 +2385,21 @@ const getAllowedPatternsDependants = ( select ) => ( state, rootClientId ) => [ ...getInsertBlockTypeDependants( state, rootClientId ), ]; +const patternsWithParsedBlocks = new WeakMap(); +function enhancePatternWithParsedBlocks( pattern ) { + let enhancedPattern = patternsWithParsedBlocks.get( pattern ); + if ( ! enhancedPattern ) { + enhancedPattern = { + ...pattern, + get blocks() { + return getParsedPattern( pattern ).blocks; + }, + }; + patternsWithParsedBlocks.set( pattern, enhancedPattern ); + } + return enhancedPattern; +} + /** * Returns the list of allowed patterns for inner blocks children. * @@ -2406,14 +2421,7 @@ export const __experimentalGetAllowedPatterns = createRegistrySelector( const { allowedBlockTypes } = getSettings( state ); const parsedPatterns = patterns .filter( ( { inserter = true } ) => !! inserter ) - .map( ( pattern ) => { - return { - ...pattern, - get blocks() { - return getParsedPattern( pattern ).blocks; - }, - }; - } ); + .map( enhancePatternWithParsedBlocks ); const availableParsedPatterns = parsedPatterns.filter( ( pattern ) => diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js index fd83ba716ddd4..2ed9b055d49f3 100644 --- a/packages/block-library/src/cover/edit/inspector-controls.js +++ b/packages/block-library/src/cover/edit/inspector-controls.js @@ -5,12 +5,12 @@ import { useMemo } from '@wordpress/element'; import { ExternalLink, FocalPointPicker, - PanelBody, RangeControl, TextareaControl, ToggleControl, SelectControl, __experimentalUseCustomUnits as useCustomUnits, + __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, __experimentalUnitControl as UnitControl, __experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue, @@ -31,6 +31,7 @@ import { __ } from '@wordpress/i18n'; */ import { COVER_MIN_HEIGHT, mediaPosition } from '../shared'; import { unlock } from '../../lock-unlock'; +import { useToolsPanelDropdownMenuProps } from '../../utils/hooks'; const { cleanEmptyObject } = unlock( blockEditorPrivateApis ); @@ -160,72 +161,130 @@ export default function CoverInspectorControls( { ), }; + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + return ( <> { !! url && ( - + { + setAttributes( { + hasParallax: false, + focalPoint: undefined, + isRepeated: false, + alt: '', + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > { isImageBackground && ( <> - + isShownByDefault + hasValue={ () => hasParallax } + onDeselect={ () => + setAttributes( { + hasParallax: false, + focalPoint: undefined, + } ) + } + > + + - + isShownByDefault + hasValue={ () => isRepeated } + onDeselect={ () => + setAttributes( { + isRepeated: false, + } ) + } + > + + ) } { showFocalPointPicker && ( - + isShownByDefault + hasValue={ () => !! focalPoint } + onDeselect={ () => setAttributes( { - focalPoint: newFocalPoint, + focalPoint: undefined, } ) } - /> + > + + setAttributes( { + focalPoint: newFocalPoint, + } ) + } + /> + ) } { ! useFeaturedImage && url && ! isVideoBackground && ( - - setAttributes( { alt: newAlt } ) + isShownByDefault + hasValue={ () => !! alt } + onDeselect={ () => + setAttributes( { alt: '' } ) } - help={ - <> - + > + + setAttributes( { alt: newAlt } ) + } + help={ + <> + + { __( + 'Describe the purpose of the image.' + ) } + +
{ __( - 'Describe the purpose of the image.' + 'Leave empty if decorative.' ) } -
-
- { __( 'Leave empty if decorative.' ) } - - } - /> + + } + /> + ) } -
+ ) }
{ colorGradientSettings.hasColorsOrGradients && ( diff --git a/packages/block-library/src/cover/index.php b/packages/block-library/src/cover/index.php index 2fca0b0374dd2..1ffe7ab3f4dbc 100644 --- a/packages/block-library/src/cover/index.php +++ b/packages/block-library/src/cover/index.php @@ -20,29 +20,22 @@ function render_block_core_cover( $attributes, $content ) { return $content; } + $object_position = isset( $attributes['focalPoint'] ) + ? round( $attributes['focalPoint']['x'] * 100 ) . '% ' . round( $attributes['focalPoint']['y'] * 100 ) . '%' + : null; + if ( ! ( $attributes['hasParallax'] || $attributes['isRepeated'] ) ) { $attr = array( 'class' => 'wp-block-cover__image-background', 'data-object-fit' => 'cover', ); - if ( isset( $attributes['focalPoint'] ) ) { - $object_position = round( $attributes['focalPoint']['x'] * 100 ) . '% ' . round( $attributes['focalPoint']['y'] * 100 ) . '%'; + if ( $object_position ) { $attr['data-object-position'] = $object_position; - $attr['style'] = 'object-position: ' . $object_position; + $attr['style'] = 'object-position:' . $object_position . ';'; } $image = get_the_post_thumbnail( null, 'post-thumbnail', $attr ); - - /* - * Inserts the featured image between the (1st) cover 'background' `span` and 'inner_container' `div`, - * and removes eventual whitespace characters between the two (typically introduced at template level) - */ - $inner_container_start = '/]+wp-block-cover__inner-container[\s|"][^>]*>/U'; - if ( 1 === preg_match( $inner_container_start, $content, $matches, PREG_OFFSET_CAPTURE ) ) { - $offset = $matches[0][1]; - $content = substr( $content, 0, $offset ) . $image . substr( $content, $offset ); - } } else { if ( in_the_loop() ) { update_post_thumbnail_cache(); @@ -52,15 +45,41 @@ function render_block_core_cover( $attributes, $content ) { return $content; } - $processor = new WP_HTML_Tag_Processor( $content ); + $current_thumbnail_id = get_post_thumbnail_id(); + + $processor = new WP_HTML_Tag_Processor( '
' ); $processor->next_tag(); - $styles = $processor->get_attribute( 'style' ); - $merged_styles = ! empty( $styles ) ? $styles . ';' : ''; - $merged_styles .= 'background-image:url(' . esc_url( $current_featured_image ) . ');'; + $current_alt = trim( strip_tags( get_post_meta( $current_thumbnail_id, '_wp_attachment_image_alt', true ) ) ); + if ( $current_alt ) { + $processor->set_attribute( 'role', 'img' ); + $processor->set_attribute( 'aria-label', $current_alt ); + } + + $processor->add_class( 'wp-block-cover__image-background' ); + $processor->add_class( 'wp-image-' . $current_thumbnail_id ); + if ( $attributes['hasParallax'] ) { + $processor->add_class( 'has-parallax' ); + } + if ( $attributes['isRepeated'] ) { + $processor->add_class( 'is-repeated' ); + } + + $styles = 'background-position:' . ( $object_position ?? '50% 50%' ) . ';'; + $styles .= 'background-image:url(' . esc_url( $current_featured_image ) . ');'; + $processor->set_attribute( 'style', $styles ); + + $image = $processor->get_updated_html(); + } - $processor->set_attribute( 'style', $merged_styles ); - $content = $processor->get_updated_html(); + /* + * Inserts the featured image between the (1st) cover 'background' `span` and 'inner_container' `div`, + * and removes eventual whitespace characters between the two (typically introduced at template level) + */ + $inner_container_start = '/]+wp-block-cover__inner-container[\s|"][^>]*>/U'; + if ( 1 === preg_match( $inner_container_start, $content, $matches, PREG_OFFSET_CAPTURE ) ) { + $offset = $matches[0][1]; + $content = substr( $content, 0, $offset ) . $image . substr( $content, $offset ); } return $content; diff --git a/packages/block-library/src/cover/test/edit.js b/packages/block-library/src/cover/test/edit.js index 97c52a47ec4d7..348cd88edfd96 100644 --- a/packages/block-library/src/cover/test/edit.js +++ b/packages/block-library/src/cover/test/edit.js @@ -190,7 +190,7 @@ describe( 'Cover block', () => { test( 'does not display media settings panel if url is not set', async () => { await setup(); expect( - screen.queryByRole( 'button', { + screen.queryByRole( 'heading', { name: 'Settings', } ) ).not.toBeInTheDocument(); @@ -202,7 +202,7 @@ describe( 'Cover block', () => { await selectBlock( 'Block: Cover' ); expect( - screen.getByRole( 'button', { + screen.getByRole( 'heading', { name: 'Settings', } ) ).toBeInTheDocument(); diff --git a/packages/block-library/src/query/edit/inspector-controls/index.js b/packages/block-library/src/query/edit/inspector-controls/index.js index b373e0933fe71..06105bef4e026 100644 --- a/packages/block-library/src/query/edit/inspector-controls/index.js +++ b/packages/block-library/src/query/edit/inspector-controls/index.js @@ -62,7 +62,7 @@ export default function QueryInspectorControls( props ) { format, } = query; const allowedControls = useAllowedControls( attributes ); - const [ showSticky, setShowSticky ] = useState( postType === 'post' ); + const showSticky = postType === 'post'; const { postTypesTaxonomiesMap, postTypesSelectOptions, @@ -70,9 +70,6 @@ export default function QueryInspectorControls( props ) { } = usePostTypes(); const taxonomies = useTaxonomies( postType ); const isPostTypeHierarchical = useIsPostTypeHierarchical( postType ); - useEffect( () => { - setShowSticky( postType === 'post' ); - }, [ postType ] ); const onPostTypeChange = ( newValue ) => { const updateQuery = { postType: newValue }; // We need to dynamically update the `taxQuery` property, diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 31be38b861c28..ae12b0cfbcf49 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -809,13 +809,16 @@ export const registerBlockBindingsSource = ( source ) => { /* * Check if the source has been already registered on the client. - * If the `getValues` property is defined, it could be assumed the source is already registered. + * If any property expected to be "client-only" is defined, return a warning. */ - if ( existingSource?.getValues ) { - warning( - 'Block bindings source "' + name + '" is already registered.' - ); - return; + const serverProps = [ 'label', 'usesContext' ]; + for ( const prop in existingSource ) { + if ( ! serverProps.includes( prop ) && existingSource[ prop ] ) { + warning( + 'Block bindings source "' + name + '" is already registered.' + ); + return; + } } // Check the `name` property is correct. diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 483949af4fe5f..99ca6390d2d3b 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -1513,10 +1513,8 @@ describe( 'blocks', () => { } ); it( 'should not override label from the server', () => { - // Bootstrap source from the server. - unlock( - dispatch( blocksStore ) - ).addBootstrappedBlockBindingsSource( { + // Simulate bootstrap source from the server. + registerBlockBindingsSource( { name: 'core/server', label: 'Server label', } ); @@ -1543,10 +1541,8 @@ describe( 'blocks', () => { } ); it( 'should add usesContext when only defined in the server', () => { - // Bootstrap source from the server. - unlock( - dispatch( blocksStore ) - ).addBootstrappedBlockBindingsSource( { + // Simulate bootstrap source from the server. + registerBlockBindingsSource( { name: 'core/testing', label: 'testing', usesContext: [ 'postId', 'postType' ], @@ -1562,10 +1558,8 @@ describe( 'blocks', () => { } ); it( 'should add usesContext when only defined in the client', () => { - // Bootstrap source from the server. - unlock( - dispatch( blocksStore ) - ).addBootstrappedBlockBindingsSource( { + // Simulate bootstrap source from the server. + registerBlockBindingsSource( { name: 'core/testing', label: 'testing', } ); @@ -1581,10 +1575,8 @@ describe( 'blocks', () => { } ); it( 'should merge usesContext from server and client without duplicates', () => { - // Bootstrap source from the server. - unlock( - dispatch( blocksStore ) - ).addBootstrappedBlockBindingsSource( { + // Simulate bootstrap source from the server. + registerBlockBindingsSource( { name: 'core/testing', label: 'testing', usesContext: [ 'postId', 'postType' ], @@ -1705,42 +1697,6 @@ describe( 'blocks', () => { 'Block bindings source "core/test-source" is already registered.' ); } ); - - it( 'should correctly merge properties when bootstrap happens after registration', () => { - // Register source in the client. - const clientOnlyProperties = { - getValues: () => 'values', - setValues: () => 'new values', - canUserEditValue: () => true, - }; - registerBlockBindingsSource( { - name: 'core/custom-source', - label: 'Client Label', - usesContext: [ 'postId', 'postType' ], - ...clientOnlyProperties, - } ); - - // Bootstrap source from the server. - unlock( - dispatch( blocksStore ) - ).addBootstrappedBlockBindingsSource( { - name: 'core/custom-source', - label: 'Server Label', - usesContext: [ 'postId', 'serverContext' ], - } ); - - // Check that the bootstrap values prevail and the client properties are still there. - expect( getBlockBindingsSource( 'core/custom-source' ) ).toEqual( { - // Should use the server label. - label: 'Server Label', - // Should merge usesContext from server and client. - usesContext: [ 'postId', 'postType', 'serverContext' ], - // Should keep client properties. - ...clientOnlyProperties, - } ); - - unregisterBlockBindingsSource( 'core/custom-source' ); - } ); } ); describe( 'unregisterBlockBindingsSource', () => { diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js index 02f8506b3c3bf..bfefe56773d77 100644 --- a/packages/blocks/src/store/private-actions.js +++ b/packages/blocks/src/store/private-actions.js @@ -70,17 +70,3 @@ export function removeBlockBindingsSource( name ) { name, }; } - -/** - * Add bootstrapped block bindings sources, usually initialized from the server. - * - * @param {string} source Name of the source to bootstrap. - */ -export function addBootstrappedBlockBindingsSource( source ) { - return { - type: 'ADD_BOOTSTRAPPED_BLOCK_BINDINGS_SOURCE', - name: source.name, - label: source.label, - usesContext: source.usesContext, - }; -} diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 7c7fb4763a1cb..16594a79271e6 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -417,22 +417,6 @@ export function blockBindingsSources( state = {}, action ) { getFieldsList, }, }; - case 'ADD_BOOTSTRAPPED_BLOCK_BINDINGS_SOURCE': - return { - ...state, - [ action.name ]: { - /* - * Keep the exisitng properties in case the source has been registered - * in the client before bootstrapping. - */ - ...state[ action.name ], - label: action.label, - usesContext: getMergedUsesContext( - state[ action.name ]?.usesContext, - action.usesContext - ), - }, - }; case 'REMOVE_BLOCK_BINDINGS_SOURCE': return omit( state, action.name ); } diff --git a/packages/customize-widgets/src/index.js b/packages/customize-widgets/src/index.js index 35be3c2ee4b14..df96d645ee700 100644 --- a/packages/customize-widgets/src/index.js +++ b/packages/customize-widgets/src/index.js @@ -39,10 +39,7 @@ const DISABLED_BLOCKS = [ ]; const ENABLE_EXPERIMENTAL_FSE_BLOCKS = false; -const { - registerCoreBlockBindingsSources, - bootstrapBlockBindingsSourcesFromServer, -} = unlock( editorPrivateApis ); +const { registerCoreBlockBindingsSources } = unlock( editorPrivateApis ); /** * Initializes the widgets block editor in the customizer. @@ -67,9 +64,6 @@ export function initialize( editorName, blockEditorSettings ) { ); } ); registerCoreBlocks( coreBlocks ); - bootstrapBlockBindingsSourcesFromServer( - blockEditorSettings?.blockBindingsSources - ); registerCoreBlockBindingsSources(); registerLegacyWidgetBlock(); if ( globalThis.IS_GUTENBERG_PLUGIN ) { diff --git a/packages/e2e-tests/plugins/block-bindings/index.js b/packages/e2e-tests/plugins/block-bindings/index.js index c31502631307d..5c364257caed1 100644 --- a/packages/e2e-tests/plugins/block-bindings/index.js +++ b/packages/e2e-tests/plugins/block-bindings/index.js @@ -19,7 +19,6 @@ const setValues = ( { dispatch, bindings } ) => { registerBlockBindingsSource( { name: 'testing/complete-source', - label: 'Complete Source', getValues, setValues, canUserEditValue: () => true, diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index daf789cb0a2ec..685ffc56f63a8 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -28,7 +28,6 @@ import { unlock } from './lock-unlock'; const { BackButton: __experimentalMainDashboardButton, registerCoreBlockBindingsSources, - bootstrapBlockBindingsSourcesFromServer, } = unlock( editorPrivateApis ); /** @@ -95,7 +94,6 @@ export function initializeEditor( } registerCoreBlocks(); - bootstrapBlockBindingsSourcesFromServer( settings?.blockBindingsSources ); registerCoreBlockBindingsSources(); registerLegacyWidgetBlock( { inserter: false } ); registerWidgetGroupBlock( { inserter: false } ); diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index 1dc0401baf21c..7ab0a965379be 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -481,7 +481,7 @@ export const initializeMetaBoxes = addAction( 'editor.savePost', 'core/edit-post/save-metaboxes', - async ( options ) => { + async ( post, options ) => { if ( ! options.isAutosave && select.hasMetaBoxes() ) { await dispatch.requestMetaBoxUpdates(); } diff --git a/packages/edit-site/src/components/global-styles/font-families.js b/packages/edit-site/src/components/global-styles/font-families.js index cd1697b7b79bd..f3e81efbe597b 100644 --- a/packages/edit-site/src/components/global-styles/font-families.js +++ b/packages/edit-site/src/components/global-styles/font-families.js @@ -6,9 +6,11 @@ import { __experimentalText as Text, __experimentalItemGroup as ItemGroup, __experimentalVStack as VStack, + __experimentalHStack as HStack, Button, } from '@wordpress/components'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { settings } from '@wordpress/icons'; import { useContext } from '@wordpress/element'; /** @@ -67,10 +69,18 @@ function FontFamilies() { /> ) } - + + + { __( 'Fonts' ) } + + ) } - ); diff --git a/packages/edit-site/src/components/global-styles/style-variations-container.js b/packages/edit-site/src/components/global-styles/style-variations-container.js index 18fc6f6ad9056..759303bcf5fa7 100644 --- a/packages/edit-site/src/components/global-styles/style-variations-container.js +++ b/packages/edit-site/src/components/global-styles/style-variations-container.js @@ -3,7 +3,7 @@ */ import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; -import { useContext, useEffect, useMemo, useState } from '@wordpress/element'; +import { useContext, useMemo } from '@wordpress/element'; import { __experimentalGrid as Grid } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; @@ -20,12 +20,7 @@ const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); export default function StyleVariationsContainer( { gap = 2 } ) { const { user } = useContext( GlobalStylesContext ); - const [ currentUserStyles, setCurrentUserStyles ] = useState( user ); - const userStyles = currentUserStyles?.styles; - - useEffect( () => { - setCurrentUserStyles( user ); - }, [ user ] ); + const userStyles = user?.styles; const variations = useSelect( ( select ) => { return select( diff --git a/packages/edit-site/src/components/sidebar-button/index.js b/packages/edit-site/src/components/sidebar-button/index.js index 64c6efb891f46..f4cea37f01078 100644 --- a/packages/edit-site/src/components/sidebar-button/index.js +++ b/packages/edit-site/src/components/sidebar-button/index.js @@ -11,7 +11,7 @@ import { Button } from '@wordpress/components'; export default function SidebarButton( props ) { return (