diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index deef2d75386fd5..3a041bba1f3652 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -617,6 +617,9 @@ function BlockListBlockProvider( props ) { getBlockRootClientId( clientId ) ), isEditingDisabled: blockEditingMode === 'disabled', + hasEditableOutline: + blockEditingMode !== 'disabled' && + getBlockEditingMode( rootClientId ) === 'disabled', className: hasLightBlockWrapper ? attributes.className : undefined, @@ -661,6 +664,7 @@ function BlockListBlockProvider( props ) { isBlockMovingMode, canInsertMovingBlock, isEditingDisabled, + hasEditableOutline, className, defaultClassName, } = selectedProps; @@ -696,6 +700,7 @@ function BlockListBlockProvider( props ) { isBlockMovingMode, canInsertMovingBlock, isEditingDisabled, + hasEditableOutline, isTemporarilyEditingAsBlocks, defaultClassName, mayDisplayControls, diff --git a/packages/block-editor/src/components/block-list/content.scss b/packages/block-editor/src/components/block-list/content.scss index ba1714063fa8fc..04bd4b6ab39025 100644 --- a/packages/block-editor/src/components/block-list/content.scss +++ b/packages/block-editor/src/components/block-list/content.scss @@ -300,35 +300,30 @@ _::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-b } } -// Indicate which blocks are editable within a locked context. -// 1. User must be hovering an editor with renderingMode = 'template-lock'; or... -.is-template-locked:hover, -// ...a container block. -.block-editor-block-list__block:hover { - // 2. Look for locked blocks; or... - .block-editor-block-list__block.is-editing-disabled, - // ...container blocks that have locked children. - &:has(> .block-editor-block-list__block.is-editing-disabled) { - // 3. Highlight any unlocked children of that locked block. - & > .block-editor-block-list__block:not(.is-editing-disabled):not(.is-selected):not(.has-child-selected) { - &::after { - content: ""; - border-style: dotted; - position: absolute; - pointer-events: none; - top: $border-width; - left: $border-width; - right: $border-width; - bottom: $border-width; - border: 1px dotted var(--wp-admin-theme-color); - border-radius: $radius-block-ui - $border-width; - } +@keyframes block-editor-has-editable-outline__fade-out-animation { + from { + border-color: rgba(var(--wp-admin-theme-color--rgb), 1); + } + to { + border-color: rgba(var(--wp-admin-theme-color--rgb), 0); + } +} - &.is-hovered::after { - background: rgba(var(--wp-admin-theme-color--rgb), 0.1); - border: none; - } - } +.block-editor-block-list__block.has-editable-outline { + &::after { + content: ""; + position: absolute; + pointer-events: none; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 1px dotted rgba(var(--wp-admin-theme-color--rgb), 1); + border-radius: $radius-block-ui; + animation: block-editor-has-editable-outline__fade-out-animation 0.3s ease-out; + animation-delay: 3s; + animation-fill-mode: forwards; + @include reduce-motion("animation"); } } diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 9c7ae30b5997a5..7f3b9f1bb05e24 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -95,6 +95,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { isBlockMovingMode, canInsertMovingBlock, isEditingDisabled, + hasEditableOutline, isTemporarilyEditingAsBlocks, defaultClassName, } = useContext( PrivateBlockContext ); @@ -152,6 +153,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { 'is-block-moving-mode': isBlockMovingMode, 'can-insert-moving-block': canInsertMovingBlock, 'is-editing-disabled': isEditingDisabled, + 'has-editable-outline': hasEditableOutline, 'is-content-locked-temporarily-editing-as-blocks': isTemporarilyEditingAsBlocks, }, diff --git a/packages/block-editor/src/components/use-flash-editable-blocks/index.js b/packages/block-editor/src/components/use-flash-editable-blocks/index.js new file mode 100644 index 00000000000000..25e07e40f3dedb --- /dev/null +++ b/packages/block-editor/src/components/use-flash-editable-blocks/index.js @@ -0,0 +1,48 @@ +/** + * WordPress dependencies + */ +import { useRefEffect } from '@wordpress/compose'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; + +export default function useFlashEditableBlocks( rootClientId = '' ) { + const { getEnabledClientIdsTree } = unlock( useSelect( blockEditorStore ) ); + + return useRefEffect( ( element ) => { + const flashEditableBlocks = () => { + getEnabledClientIdsTree( rootClientId ).forEach( + ( { clientId } ) => { + const blockElement = element.querySelector( + `[data-block="${ clientId }"]` + ); + if ( ! blockElement ) { + return; + } + blockElement.classList.remove( 'has-editable-outline' ); + // Force reflow to trigger the animation. + // eslint-disable-next-line no-unused-expressions + blockElement.offsetWidth; + blockElement.classList.add( 'has-editable-outline' ); + } + ); + }; + + const handleClick = ( event ) => { + if ( event.defaultPrevented ) { + return; + } + event.preventDefault(); + flashEditableBlocks(); + }; + + element.addEventListener( 'click', handleClick ); + return () => { + element.removeEventListener( 'click', handleClick ); + }; + } ); +} diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 74bf4af421dfbb..555d0dd574fef7 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -24,6 +24,7 @@ import { import { usesContextKey } from './components/rich-text/format-edit'; import { ExperimentalBlockCanvas } from './components/block-canvas'; import { getDuotoneFilter } from './components/duotone/utils'; +import useFlashEditableBlocks from './components/use-flash-editable-blocks'; /** * Private @wordpress/block-editor APIs. @@ -52,4 +53,5 @@ lock( privateApis, { ReusableBlocksRenameHint, useReusableBlocksRenameHint, usesContextKey, + useFlashEditableBlocks, } ); diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index c940f36e1a8b86..8be6cf0197d08e 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -35,7 +35,9 @@ import { parse, cloneBlock } from '@wordpress/blocks'; */ import { unlock } from '../lock-unlock'; -const { useLayoutClasses } = unlock( blockEditorPrivateApis ); +const { useLayoutClasses, useFlashEditableBlocks } = unlock( + blockEditorPrivateApis +); const { PARTIAL_SYNCING_SUPPORTED_BLOCKS } = unlock( patternsPrivateApis ); function isPartiallySynced( block ) { @@ -311,7 +313,10 @@ export default function ReusableBlockEdit( { ); const layoutClasses = useLayoutClasses( { layout }, name ); + const flashEditableBlocksRef = useFlashEditableBlocks( patternClientId ); + const blockProps = useBlockProps( { + ref: flashEditableBlocksRef, className: classnames( 'block-library-block__reusable-block-container', layout && layoutClasses, @@ -320,6 +325,7 @@ export default function ReusableBlockEdit( { } ); const innerBlocksProps = useInnerBlocksProps( blockProps, { + ref: flashEditableBlocksRef, templateLock: 'all', layout, renderAppender: innerBlocks?.length diff --git a/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js b/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js index 01947e453aa112..acf13c9025bce3 100644 --- a/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js +++ b/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js @@ -27,27 +27,18 @@ import { store as editorStore } from '../../store'; * editor iframe canvas. */ export default function EditTemplateBlocksNotification( { contentRef } ) { - const { renderingMode, getPostLinkProps, templateId } = useSelect( - ( select ) => { - const { - getRenderingMode, - getEditorSettings, - getCurrentTemplateId, - } = select( editorStore ); - return { - renderingMode: getRenderingMode(), - getPostLinkProps: getEditorSettings().getPostLinkProps, - templateId: getCurrentTemplateId(), - }; - }, - [] - ); - const editTemplate = getPostLinkProps - ? getPostLinkProps( { - postId: templateId, - postType: 'wp_template', - } ) - : {}; + const editTemplate = useSelect( ( select ) => { + const { getEditorSettings, getCurrentTemplateId } = + select( editorStore ); + const { getPostLinkProps } = getEditorSettings(); + return getPostLinkProps + ? getPostLinkProps( { + postId: getCurrentTemplateId(), + postType: 'wp_template', + } ) + : {}; + }, [] ); + const { getNotices } = useSelect( noticesStore ); const { createInfoNotice, removeNotice } = useDispatch( noticesStore ); @@ -58,18 +49,17 @@ export default function EditTemplateBlocksNotification( { contentRef } ) { useEffect( () => { const handleClick = async ( event ) => { - if ( renderingMode !== 'template-locked' ) { - return; - } if ( ! event.target.classList.contains( 'is-root-container' ) ) { return; } + const isNoticeAlreadyShowing = getNotices().some( ( notice ) => notice.id === lastNoticeId.current ); if ( isNoticeAlreadyShowing ) { return; } + const { notice } = await createInfoNotice( __( 'Edit your template to edit this block.' ), { @@ -87,9 +77,6 @@ export default function EditTemplateBlocksNotification( { contentRef } ) { }; const handleDblClick = ( event ) => { - if ( renderingMode !== 'template-locked' ) { - return; - } if ( ! event.target.classList.contains( 'is-root-container' ) ) { return; } @@ -106,7 +93,7 @@ export default function EditTemplateBlocksNotification( { contentRef } ) { canvas?.removeEventListener( 'click', handleClick ); canvas?.removeEventListener( 'dblclick', handleDblClick ); }; - }, [ lastNoticeId, renderingMode, contentRef.current ] ); + }, [ lastNoticeId, contentRef.current ] ); return ( {}; @@ -287,9 +288,11 @@ function EditorCanvas( { const localRef = useRef(); const typewriterRef = useTypewriter(); + const flashEditableBlocksRef = useFlashEditableBlocks(); const contentRef = useMergeRefs( [ localRef, renderingMode === 'post-only' ? typewriterRef : noop, + renderingMode === 'template-locked' ? flashEditableBlocksRef : noop, ] ); return ( @@ -364,8 +367,7 @@ function EditorCanvas( { 'is-' + deviceType.toLowerCase() + '-preview', renderingMode !== 'post-only' ? 'wp-site-blocks' - : `${ blockListLayoutClass } wp-block-post-content`, // Ensure root level blocks receive default/flow blockGap styling rules. - renderingMode !== 'all' && 'is-' + renderingMode + : `${ blockListLayoutClass } wp-block-post-content` // Ensure root level blocks receive default/flow blockGap styling rules. ) } layout={ blockListLayout } dropZoneElement={ @@ -377,7 +379,9 @@ function EditorCanvas( { } renderAppender={ renderAppender } /> - + { renderingMode === 'template-locked' && ( + + ) } { children }