diff --git a/packages/base-styles/_functions.scss b/packages/base-styles/_functions.scss new file mode 100644 index 00000000000000..734506d456d974 --- /dev/null +++ b/packages/base-styles/_functions.scss @@ -0,0 +1,9 @@ +/** +* Converts a hex value into the rgb equivalent. +* +* @param {string} hex - the hexadecimal value to convert +* @return {string} comma separated rgb values +*/ +@function hex-to-rgb($hex) { + @return red($hex), green($hex), blue($hex); +} diff --git a/packages/base-styles/_mixins.scss b/packages/base-styles/_mixins.scss index c1c4215c4f48f4..d01d312014b879 100644 --- a/packages/base-styles/_mixins.scss +++ b/packages/base-styles/_mixins.scss @@ -1,3 +1,5 @@ +@import "./functions"; + /** * Breakpoint mixins */ @@ -440,11 +442,15 @@ } @mixin admin-scheme($color-primary) { + // Define RGB equivalents for use in rgba function. + // Hexadecimal css vars do not work in the rgba function. --wp-admin-theme-color: #{$color-primary}; - + --wp-admin-theme-color--rgb: #{hex-to-rgb($color-primary)}; // Darker shades. --wp-admin-theme-color-darker-10: #{darken($color-primary, 5%)}; + --wp-admin-theme-color-darker-10--rgb: #{hex-to-rgb(darken($color-primary, 5%))}; --wp-admin-theme-color-darker-20: #{darken($color-primary, 10%)}; + --wp-admin-theme-color-darker-20--rgb: #{hex-to-rgb(darken($color-primary, 10%))}; // Focus style width. // Avoid rounding issues by showing a whole 2px for 1x screens, and 1.5px on high resolution screens. diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index dda6e438edf4af..c7bc54004e1d49 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -74,6 +74,9 @@ $z-layers: ( // The toolbar, when contextual, should be above any adjacent nested block click overlays. ".block-editor-block-contextual-toolbar": 61, + // Ensures content overlay appears higher than resize containers used for image/video/etc. + ".block-editor-block-content-overlay__overlay": 10, + // The block mover, particularly in nested contexts, // should overlap most block content. ".block-editor-block-list__block.is-{selected,hovered} .block-editor-block-mover": 61, diff --git a/packages/block-editor/src/components/block-content-overlay/index.js b/packages/block-editor/src/components/block-content-overlay/index.js new file mode 100644 index 00000000000000..d75d2716c9eb5c --- /dev/null +++ b/packages/block-editor/src/components/block-content-overlay/index.js @@ -0,0 +1,101 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useState, useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +/** + * External dependencies + */ +import classnames from 'classnames'; + +export default function BlockContentOverlay( { + clientId, + tagName: TagName = 'div', + wrapperProps, + className, +} ) { + const baseClassName = 'block-editor-block-content-overlay'; + const [ isOverlayActive, setIsOverlayActive ] = useState( true ); + const [ isHovered, setIsHovered ] = useState( false ); + + const { + isParentSelected, + hasChildSelected, + isDraggingBlocks, + isParentHighlighted, + } = useSelect( + ( select ) => { + const { + isBlockSelected, + hasSelectedInnerBlock, + isDraggingBlocks: _isDraggingBlocks, + isBlockHighlighted, + } = select( blockEditorStore ); + return { + isParentSelected: isBlockSelected( clientId ), + hasChildSelected: hasSelectedInnerBlock( clientId, true ), + isDraggingBlocks: _isDraggingBlocks(), + isParentHighlighted: isBlockHighlighted( clientId ), + }; + }, + [ clientId ] + ); + + const classes = classnames( + baseClassName, + wrapperProps?.className, + className, + { + 'overlay-active': isOverlayActive, + 'parent-highlighted': isParentHighlighted, + 'is-dragging-blocks': isDraggingBlocks, + } + ); + + useEffect( () => { + // Reenable when blocks are not in use. + if ( ! isParentSelected && ! hasChildSelected && ! isOverlayActive ) { + setIsOverlayActive( true ); + } + // Disable if parent selected by another means (such as list view). + // We check hover to ensure the overlay click interaction is not taking place. + // Trying to click the overlay will select the parent block via its 'focusin' + // listener on the wrapper, so if the block is selected while hovered we will + // let the mouseup disable the overlay instead. + if ( isParentSelected && ! isHovered && isOverlayActive ) { + setIsOverlayActive( false ); + } + // Ensure overlay is disabled if a child block is selected. + if ( hasChildSelected && isOverlayActive ) { + setIsOverlayActive( false ); + } + }, [ isParentSelected, hasChildSelected, isOverlayActive, isHovered ] ); + + // Disabled because the overlay div doesn't actually have a role or functionality + // as far as the a11y is concerned. We're just catching the first click so that + // the block can be selected without interacting with its contents. + /* eslint-disable jsx-a11y/no-static-element-interactions */ + return ( + setIsHovered( true ) } + onMouseLeave={ () => setIsHovered( false ) } + > + { isOverlayActive && ( +
setIsOverlayActive( false ) } + /> + ) } + { wrapperProps?.children } + + ); +} +/* eslint-enable jsx-a11y/no-static-element-interactions */ diff --git a/packages/block-editor/src/components/block-content-overlay/style.scss b/packages/block-editor/src/components/block-content-overlay/style.scss new file mode 100644 index 00000000000000..35cd3afcd8a897 --- /dev/null +++ b/packages/block-editor/src/components/block-content-overlay/style.scss @@ -0,0 +1,41 @@ +// Specificity required to ensure overlay width is not restricted to that +// of standard block content. The overlay's width should be as wide as +// its children require. +.editor-styles-wrapper .wp-block .block-editor-block-content-overlay__overlay { + max-width: none; +} + +.block-editor-block-content-overlay { + .block-editor-block-content-overlay__overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: transparent; + border: none; + border-radius: $radius-block-ui; + z-index: z-index(".block-editor-block-content-overlay__overlay"); + } + + &:hover:not(.is-dragging-blocks), + &.parent-highlighted { + > .block-editor-block-content-overlay__overlay { + background: rgba(var(--wp-admin-theme-color--rgb), 0.1); + box-shadow: 0 0 0 $border-width var(--wp-admin-theme-color) inset; + } + } + + &.overlay-active:not(.is-dragging-blocks) { + *:not(.block-editor-block-content-overlay__overlay) { + pointer-events: none; + } + } + + &.is-dragging-blocks { + box-shadow: 0 0 0 $border-width var(--wp-admin-theme-color); + .block-editor-block-content-overlay__overlay { + pointer-events: none; + } + } +} diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index f8752a39f8fde7..ef4095a17a1d3a 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -14,6 +14,7 @@ export { export { default as __experimentalBlockFullHeightAligmentControl } from './block-full-height-alignment-control'; export { default as __experimentalBlockAlignmentMatrixControl } from './block-alignment-matrix-control'; export { default as BlockBreadcrumb } from './block-breadcrumb'; +export { default as __experimentalBlockContentOverlay } from './block-content-overlay'; export { BlockContextProvider } from './block-context'; export { default as BlockControls, diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 92fc673e65ccf5..a3c9a9f055b342 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -15,6 +15,7 @@ @import "./components/block-breadcrumb/style.scss"; @import "./components/block-card/style.scss"; @import "./components/block-compare/style.scss"; +@import "./components/block-content-overlay/style.scss"; @import "./components/block-draggable/style.scss"; @import "./components/block-mobile-toolbar/style.scss"; @import "./components/block-mover/style.scss"; diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 966654c87a3dc8..bf4f933773d6bf 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -19,6 +19,7 @@ import { __ } from '@wordpress/i18n'; import { __experimentalUseInnerBlocksProps as useInnerBlocksProps, __experimentalUseNoRecursiveRenders as useNoRecursiveRenders, + __experimentalBlockContentOverlay as BlockContentOverlay, InnerBlocks, BlockControls, InspectorControls, @@ -70,6 +71,8 @@ export default function ReusableBlockEdit( { attributes: { ref }, clientId } ) { ref ); + const blockProps = useBlockProps(); + const innerBlocksProps = useInnerBlocksProps( {}, { @@ -82,8 +85,6 @@ export default function ReusableBlockEdit( { attributes: { ref }, clientId } ) { } ); - const blockProps = useBlockProps(); - if ( hasAlreadyRendered ) { return (
@@ -136,9 +137,11 @@ export default function ReusableBlockEdit( { attributes: { ref }, clientId } ) { /> -
- {
} -
+
); diff --git a/packages/block-library/src/template-part/edit/index.js b/packages/block-library/src/template-part/edit/index.js index e8ed05baa9b1f4..a6f6078bd09251 100644 --- a/packages/block-library/src/template-part/edit/index.js +++ b/packages/block-library/src/template-part/edit/index.js @@ -194,6 +194,7 @@ export default function TemplatePartEdit( { ) } { isEntityAvailable && ( { const { getSettings } = select( blockEditorStore ); @@ -45,6 +47,7 @@ export default function TemplatePartInnerBlocks( { 'wp_template_part', { id } ); + const innerBlocksProps = useInnerBlocksProps( blockProps, { value: blocks, onInput, @@ -54,5 +57,12 @@ export default function TemplatePartInnerBlocks( { : InnerBlocks.ButtonBlockAppender, __experimentalLayout: _layout, } ); - return ; + + return ( + + ); } diff --git a/packages/e2e-test-utils/src/inserter.js b/packages/e2e-test-utils/src/inserter.js index b5041992f2bbc4..48a4985bfe62fe 100644 --- a/packages/e2e-test-utils/src/inserter.js +++ b/packages/e2e-test-utils/src/inserter.js @@ -196,7 +196,7 @@ export async function insertReusableBlock( searchTerm ) { await waitForInserterCloseAndContentFocus(); // We should wait until the block is loaded await page.waitForXPath( - '//*[@class="block-library-block__reusable-block-container"]' + '//*[contains(@class,"block-library-block__reusable-block-container")]' ); } diff --git a/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js b/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js index 0538e5e4b54a5e..e23c5180828d27 100644 --- a/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js +++ b/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js @@ -10,6 +10,7 @@ import { canvas, openDocumentSettingsSidebar, pressKeyWithModifier, + selectBlockByClientId, } from '@wordpress/e2e-test-utils'; /** @@ -218,7 +219,7 @@ describe( 'Multi-entity editor states', () => { removeErrorMocks(); } ); - afterEach( async () => { + const saveAndWaitResponse = async () => { await Promise.all( [ saveAllEntities(), @@ -241,7 +242,7 @@ describe( 'Multi-entity editor states', () => { } ), ] ); removeErrorMocks(); - } ); + }; it( 'should only dirty the parent entity when editing the parent', async () => { // Clear selection so that the block is not added to the template part. @@ -253,9 +254,12 @@ describe( 'Multi-entity editor states', () => { expect( await isEntityDirty( templateName ) ).toBe( true ); expect( await isEntityDirty( templatePartName ) ).toBe( false ); expect( await isEntityDirty( nestedTPName ) ).toBe( false ); + await saveAndWaitResponse(); } ); it( 'should only dirty the child when editing the child', async () => { + // Select parent TP to unlock selecting content. + await canvas().click( '.wp-block-template-part' ); await canvas().click( '.wp-block-template-part .wp-block[data-type="core/paragraph"]' ); @@ -264,9 +268,16 @@ describe( 'Multi-entity editor states', () => { expect( await isEntityDirty( templateName ) ).toBe( false ); expect( await isEntityDirty( templatePartName ) ).toBe( true ); expect( await isEntityDirty( nestedTPName ) ).toBe( false ); + await saveAndWaitResponse(); } ); it( 'should only dirty the nested entity when editing the nested entity', async () => { + // Select parent TP to unlock selecting child. + await canvas().click( '.wp-block-template-part' ); + // Select child TP to unlock selecting content. + await canvas().click( + '.wp-block-template-part .wp-block-template-part' + ); await canvas().click( '.wp-block-template-part .wp-block-template-part .wp-block[data-type="core/paragraph"]' ); @@ -275,6 +286,21 @@ describe( 'Multi-entity editor states', () => { expect( await isEntityDirty( templateName ) ).toBe( false ); expect( await isEntityDirty( templatePartName ) ).toBe( false ); expect( await isEntityDirty( nestedTPName ) ).toBe( true ); + await saveAndWaitResponse(); + } ); + + it( 'should not allow selecting template part content without parent selected', async () => { + // Unselect blocks. + await selectBlockByClientId(); + // Try to select a child block of a template part. + await canvas().click( + '.wp-block-template-part .wp-block-template-part .wp-block[data-type="core/paragraph"]' + ); + + const selectedBlock = await page.evaluate( () => { + return wp.data.select( 'core/block-editor' ).getSelectedBlock(); + } ); + expect( selectedBlock?.name ).toBe( 'core/template-part' ); } ); } ); } ); diff --git a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js index 357b0967197c96..dc640faf32cf26 100644 --- a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js +++ b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js @@ -105,8 +105,6 @@ describe( 'Multi-entity save flow', () => { ); await createNewButton.click(); await page.waitForSelector( activatedTemplatePartSelector ); - await page.keyboard.press( 'Tab' ); - await page.keyboard.type( 'test-template-part' ); await page.click( '.block-editor-button-block-appender' ); await page.click( '.editor-block-list-item-paragraph' ); await page.keyboard.type( 'some words...' ); @@ -163,6 +161,9 @@ describe( 'Multi-entity save flow', () => { // Update template part. await page.click( templatePartSelector ); + await page.click( + `${ templatePartSelector } .wp-block[data-type="core/paragraph"]` + ); await page.keyboard.type( '...some more words...' ); await page.keyboard.press( 'Enter' ); diff --git a/packages/e2e-tests/specs/experiments/template-part.test.js b/packages/e2e-tests/specs/experiments/template-part.test.js index 4f86248fd79b5e..4e16c07c4e88c1 100644 --- a/packages/e2e-tests/specs/experiments/template-part.test.js +++ b/packages/e2e-tests/specs/experiments/template-part.test.js @@ -94,6 +94,15 @@ describe( 'Template Part', () => { expect( paragraphInTemplatePart ).not.toBeNull(); } + async function awaitHeaderAndFooterLoad() { + await canvas().waitForSelector( + '.wp-block-template-part.site-header.block-editor-block-list__layout' + ); + await canvas().waitForSelector( + '.wp-block-template-part.site-footer.block-editor-block-list__layout' + ); + } + it( 'Should load customizations when in a template even if only the slug and theme attributes are set.', async () => { await updateHeader( 'Header Template Part 123' ); @@ -164,9 +173,7 @@ describe( 'Template Part', () => { } ); it( 'Should convert selected block to template part', async () => { - await canvas().waitForSelector( - '.wp-block-template-part.block-editor-block-list__layout' - ); + await awaitHeaderAndFooterLoad(); const initialTemplateParts = await canvas().$$( '.wp-block-template-part' ); @@ -204,9 +211,7 @@ describe( 'Template Part', () => { } ); it( 'Should convert multiple selected blocks to template part', async () => { - await canvas().waitForSelector( - '.wp-block-template-part.block-editor-block-list__layout' - ); + await awaitHeaderAndFooterLoad(); const initialTemplateParts = await canvas().$$( '.wp-block-template-part' );