From 95a6b3b6dcb5092663e2d750523b9e694de3e04b Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Tue, 5 Mar 2024 17:00:37 +0800 Subject: [PATCH 1/7] Refine renaming flow for blocks with overrides --- .../src/components/block-rename/modal.js | 65 +++++++--- .../components/block-rename/rename-control.js | 47 +++++-- .../src/hooks/use-bindings-attributes.js | 31 ++++- packages/block-editor/src/private-apis.js | 3 + .../editor/src/hooks/pattern-overrides.js | 39 ++++-- .../src/components/allow-overrides-modal.js | 92 +++++++++++++ .../components/pattern-overrides-controls.js | 122 ++++++++++++++++++ packages/patterns/src/components/style.scss | 5 + .../components/use-set-pattern-bindings.js | 120 ----------------- packages/patterns/src/constants.js | 2 + packages/patterns/src/private-apis.js | 4 +- test/e2e/specs/editor/blocks/heading.spec.js | 1 - 12 files changed, 367 insertions(+), 164 deletions(-) create mode 100644 packages/patterns/src/components/allow-overrides-modal.js create mode 100644 packages/patterns/src/components/pattern-overrides-controls.js delete mode 100644 packages/patterns/src/components/use-set-pattern-bindings.js diff --git a/packages/block-editor/src/components/block-rename/modal.js b/packages/block-editor/src/components/block-rename/modal.js index 8e9ffc1d81b63a..98c074f5aba35c 100644 --- a/packages/block-editor/src/components/block-rename/modal.js +++ b/packages/block-editor/src/components/block-rename/modal.js @@ -7,38 +7,49 @@ import { Button, TextControl, Modal, + ToggleControl, } from '@wordpress/components'; -import { useInstanceId } from '@wordpress/compose'; import { __, sprintf } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; import { speak } from '@wordpress/a11y'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ import isEmptyString from './is-empty-string'; +import { store as blockEditorStore } from '../../store'; export default function BlockRenameModal( { blockName, originalBlockName, onClose, onSave, + initialAllowOverrides, } ) { const [ editedBlockName, setEditedBlockName ] = useState( blockName ); + const [ allowOverrides, setAllowOverrides ] = useState( + initialAllowOverrides + ); + const { isEditingPattern } = useSelect( + ( select ) => ( { + isEditingPattern: + select( blockEditorStore ).getSettings()?.postType === + 'wp_block', + } ), + [] + ); const nameHasChanged = editedBlockName !== blockName; const nameIsOriginal = editedBlockName === originalBlockName; const nameIsEmpty = isEmptyString( editedBlockName ); const isNameValid = nameHasChanged || nameIsOriginal; + const isSaveDisabled = + ! isNameValid && allowOverrides === initialAllowOverrides; const autoSelectInputText = ( event ) => event.target.select(); - const dialogDescription = useInstanceId( - BlockRenameModal, - `block-editor-rename-modal__description` - ); - const handleSubmit = () => { const message = nameIsOriginal || nameIsEmpty @@ -55,7 +66,10 @@ export default function BlockRenameModal( { // Must be assertive to immediately announce change. speak( message, 'assertive' ); - onSave( editedBlockName ); + onSave( + editedBlockName, + isEditingPattern ? allowOverrides : undefined + ); // Immediate close avoids ability to hit save multiple times. onClose(); @@ -66,19 +80,14 @@ export default function BlockRenameModal( { title={ __( 'Rename' ) } onRequestClose={ onClose } overlayClassName="block-editor-block-rename-modal" - aria={ { - describedby: dialogDescription, - } } focusOnMount="firstContentElement" + size="small" > -

- { __( 'Enter a custom name for this block.' ) } -

{ e.preventDefault(); - if ( ! isNameValid ) { + if ( isSaveDisabled ) { return; } @@ -91,11 +100,33 @@ export default function BlockRenameModal( { __next40pxDefaultSize value={ editedBlockName } label={ __( 'Block name' ) } - hideLabelFromVision + help={ __( + 'Naming your block will help people understand its purpose.' + ) } placeholder={ originalBlockName } - onChange={ setEditedBlockName } + onChange={ ( newName ) => { + setEditedBlockName( newName ); + if ( newName === '' ) { + setAllowOverrides( false ); + } + } } onFocus={ autoSelectInputText } /> + { isEditingPattern ? ( + { + if ( ! nameIsEmpty ) { + setAllowOverrides( checked ); + } + } } + disabled={ nameIsEmpty } + /> + ) : null } + + + + + + + ); +} diff --git a/packages/patterns/src/components/pattern-overrides-controls.js b/packages/patterns/src/components/pattern-overrides-controls.js new file mode 100644 index 00000000000000..65eae1e8d01694 --- /dev/null +++ b/packages/patterns/src/components/pattern-overrides-controls.js @@ -0,0 +1,122 @@ +/** + * WordPress dependencies + */ +import { useState, useId } from '@wordpress/element'; +import { + InspectorControls, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { ToggleControl, BaseControl, Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { unlock } from '../lock-unlock'; +import { + PARTIAL_SYNCING_SUPPORTED_BLOCKS, + PATTERN_OVERRIDES_BINDING_SOURCE, +} from '../constants'; +import AllowOverridesModal from './allow-overrides-modal'; + +const { removeBindings, addBindings } = unlock( blockEditorPrivateApis ); + +function PatternOverridesControls( { attributes, name, setAttributes } ) { + const controlId = useId(); + const [ showAllowOverridesModal, setShowAllowOverridesModal ] = + useState( false ); + + const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ]; + const attributeSources = syncedAttributes.map( + ( attributeName ) => + attributes.metadata?.bindings?.[ attributeName ]?.source + ); + const isConnectedToOtherSources = attributeSources.every( + ( source ) => source && source !== 'core/pattern-overrides' + ); + + function updateBindings( isChecked, customName ) { + if ( isChecked && ! attributes.metadata?.name && ! customName ) { + setShowAllowOverridesModal( true ); + return; + } + + const prevBindings = attributes?.metadata?.bindings; + const updatedBindings = isChecked + ? addBindings( + prevBindings, + syncedAttributes, + PATTERN_OVERRIDES_BINDING_SOURCE + ) + : removeBindings( + prevBindings, + syncedAttributes, + PATTERN_OVERRIDES_BINDING_SOURCE + ); + + const updatedMetadata = { + ...attributes.metadata, + bindings: updatedBindings, + }; + + if ( customName ) { + updatedMetadata.name = customName; + } + + setAttributes( { + metadata: updatedMetadata, + } ); + } + + // Avoid overwriting other (e.g. meta) bindings. + if ( isConnectedToOtherSources ) return null; + + const hasName = attributes.metadata?.name; + + return ( + <> + + + { hasName ? ( + + source === 'core/pattern-overrides' + ) } + onChange={ ( isChecked ) => { + updateBindings( isChecked ); + } } + /> + ) : ( + + ) } + + + + { showAllowOverridesModal && ( + setShowAllowOverridesModal( false ) } + onSave={ ( newName ) => { + updateBindings( true, newName ); + } } + /> + ) } + + ); +} + +export default PatternOverridesControls; diff --git a/packages/patterns/src/components/style.scss b/packages/patterns/src/components/style.scss index e913c924a2149c..36c441ed20659f 100644 --- a/packages/patterns/src/components/style.scss +++ b/packages/patterns/src/components/style.scss @@ -38,3 +38,8 @@ width: $grid-unit * 40; } } + +.pattern-overrides-control__allow-overrides-button { + width: 100%; + justify-content: center; +} diff --git a/packages/patterns/src/components/use-set-pattern-bindings.js b/packages/patterns/src/components/use-set-pattern-bindings.js deleted file mode 100644 index 261187a91088c1..00000000000000 --- a/packages/patterns/src/components/use-set-pattern-bindings.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * WordPress dependencies - */ -import { usePrevious } from '@wordpress/compose'; -import { store as blocksStore } from '@wordpress/blocks'; -import { useEffect } from '@wordpress/element'; -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { PARTIAL_SYNCING_SUPPORTED_BLOCKS } from '../constants'; - -import { unlock } from '../lock-unlock'; - -function removeBindings( bindings, syncedAttributes ) { - let updatedBindings = {}; - for ( const attributeName of syncedAttributes ) { - // Omit any pattern override bindings from the `updatedBindings` object. - if ( - bindings?.[ attributeName ]?.source !== 'core/pattern-overrides' && - bindings?.[ attributeName ]?.source !== undefined - ) { - updatedBindings[ attributeName ] = bindings[ attributeName ]; - } - } - if ( ! Object.keys( updatedBindings ).length ) { - updatedBindings = undefined; - } - return updatedBindings; -} - -function addBindings( bindings, syncedAttributes ) { - const updatedBindings = { ...bindings }; - for ( const attributeName of syncedAttributes ) { - if ( ! bindings?.[ attributeName ] ) { - updatedBindings[ attributeName ] = { - source: 'core/pattern-overrides', - }; - } - } - return updatedBindings; -} - -export default function useSetPatternBindings( - { name, attributes, setAttributes }, - currentPostType -) { - const hasPatternOverridesSource = useSelect( ( select ) => { - const { getBlockBindingsSource } = unlock( select( blocksStore ) ); - - // For editing link to the site editor if the theme and user permissions support it. - return !! getBlockBindingsSource( 'core/pattern-overrides' ); - }, [] ); - - const metadataName = attributes?.metadata?.name ?? ''; - const prevMetadataName = usePrevious( metadataName ) ?? ''; - const bindings = attributes?.metadata?.bindings; - - useEffect( () => { - // Bindings should only be created when editing a wp_block post type, - // and also when there's a change to the user-given name for the block. - // Also check that the pattern overrides source is registered. - if ( - ! hasPatternOverridesSource || - currentPostType !== 'wp_block' || - metadataName === prevMetadataName - ) { - return; - } - - const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ]; - const attributeSources = syncedAttributes.map( - ( attributeName ) => - attributes.metadata?.bindings?.[ attributeName ]?.source - ); - const isConnectedToOtherSources = attributeSources.every( - ( source ) => source && source !== 'core/pattern-overrides' - ); - - // Avoid overwriting other (e.g. meta) bindings. - if ( isConnectedToOtherSources ) { - return; - } - - // The user-given name for the block was deleted, remove the bindings. - if ( ! metadataName?.length && prevMetadataName?.length ) { - const updatedBindings = removeBindings( - bindings, - syncedAttributes - ); - setAttributes( { - metadata: { - ...attributes.metadata, - bindings: updatedBindings, - }, - } ); - } - - // The user-given name for the block was set, set the bindings. - if ( ! prevMetadataName?.length && metadataName.length ) { - const updatedBindings = addBindings( bindings, syncedAttributes ); - setAttributes( { - metadata: { - ...attributes.metadata, - bindings: updatedBindings, - }, - } ); - } - }, [ - hasPatternOverridesSource, - bindings, - prevMetadataName, - metadataName, - currentPostType, - name, - attributes.metadata, - setAttributes, - ] ); -} diff --git a/packages/patterns/src/constants.js b/packages/patterns/src/constants.js index 99d6a0fa975a8d..99563a1a16787f 100644 --- a/packages/patterns/src/constants.js +++ b/packages/patterns/src/constants.js @@ -22,3 +22,5 @@ export const PARTIAL_SYNCING_SUPPORTED_BLOCKS = { 'core/button': [ 'text', 'url', 'linkTarget', 'rel' ], 'core/image': [ 'id', 'url', 'title', 'alt' ], }; + +export const PATTERN_OVERRIDES_BINDING_SOURCE = 'core/pattern-overrides'; diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js index 15ff161305f4a6..05417de2b2c669 100644 --- a/packages/patterns/src/private-apis.js +++ b/packages/patterns/src/private-apis.js @@ -15,7 +15,7 @@ import { isOverridableBlock } from './api'; import RenamePatternModal from './components/rename-pattern-modal'; import PatternsMenuItems from './components'; import RenamePatternCategoryModal from './components/rename-pattern-category-modal'; -import useSetPatternBindings from './components/use-set-pattern-bindings'; +import PatternOverridesControls from './components/pattern-overrides-controls'; import ResetOverridesControl from './components/reset-overrides-control'; import { useAddPatternCategory } from './private-hooks'; import { @@ -38,7 +38,7 @@ lock( privateApis, { RenamePatternModal, PatternsMenuItems, RenamePatternCategoryModal, - useSetPatternBindings, + PatternOverridesControls, ResetOverridesControl, useAddPatternCategory, PATTERN_TYPES, diff --git a/test/e2e/specs/editor/blocks/heading.spec.js b/test/e2e/specs/editor/blocks/heading.spec.js index 5f05ea82079564..caf7378084efe9 100644 --- a/test/e2e/specs/editor/blocks/heading.spec.js +++ b/test/e2e/specs/editor/blocks/heading.spec.js @@ -379,7 +379,6 @@ test.describe( 'Heading', () => { .getByRole( 'dialog', { name: 'Rename' } ) .getByRole( 'textbox', { name: 'Block name' } ) .fill( 'My new name' ); - await page .getByRole( 'dialog', { name: 'Rename' } ) .getByRole( 'button', { name: 'Save' } ) From 28d79272d2cb5cfe60de4148b39841c2503324e5 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Wed, 3 Apr 2024 21:34:39 +0800 Subject: [PATCH 2/7] Design feedback --- .../src/components/block-rename/modal.js | 68 ++++++------------- .../components/block-rename/rename-control.js | 48 ++++--------- .../src/hooks/use-bindings-attributes.js | 31 +-------- packages/block-editor/src/private-apis.js | 3 - .../src/components/allow-overrides-modal.js | 12 +++- .../components/pattern-overrides-controls.js | 50 +++++++++----- .../editor/various/pattern-overrides.spec.js | 11 +++ 7 files changed, 87 insertions(+), 136 deletions(-) diff --git a/packages/block-editor/src/components/block-rename/modal.js b/packages/block-editor/src/components/block-rename/modal.js index 98c074f5aba35c..dd929573647b42 100644 --- a/packages/block-editor/src/components/block-rename/modal.js +++ b/packages/block-editor/src/components/block-rename/modal.js @@ -7,46 +7,31 @@ import { Button, TextControl, Modal, - ToggleControl, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; +import { useState, useId } from '@wordpress/element'; import { speak } from '@wordpress/a11y'; -import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ import isEmptyString from './is-empty-string'; -import { store as blockEditorStore } from '../../store'; export default function BlockRenameModal( { blockName, originalBlockName, onClose, onSave, - initialAllowOverrides, + hasOverridesWarning, } ) { const [ editedBlockName, setEditedBlockName ] = useState( blockName ); - const [ allowOverrides, setAllowOverrides ] = useState( - initialAllowOverrides - ); - const { isEditingPattern } = useSelect( - ( select ) => ( { - isEditingPattern: - select( blockEditorStore ).getSettings()?.postType === - 'wp_block', - } ), - [] - ); + const descriptionId = useId(); const nameHasChanged = editedBlockName !== blockName; const nameIsOriginal = editedBlockName === originalBlockName; const nameIsEmpty = isEmptyString( editedBlockName ); const isNameValid = nameHasChanged || nameIsOriginal; - const isSaveDisabled = - ! isNameValid && allowOverrides === initialAllowOverrides; const autoSelectInputText = ( event ) => event.target.select(); @@ -66,10 +51,7 @@ export default function BlockRenameModal( { // Must be assertive to immediately announce change. speak( message, 'assertive' ); - onSave( - editedBlockName, - isEditingPattern ? allowOverrides : undefined - ); + onSave( editedBlockName ); // Immediate close avoids ability to hit save multiple times. onClose(); @@ -81,52 +63,41 @@ export default function BlockRenameModal( { onRequestClose={ onClose } overlayClassName="block-editor-block-rename-modal" focusOnMount="firstContentElement" + aria={ { describedby: descriptionId } } size="small" >
{ e.preventDefault(); - if ( isSaveDisabled ) { + if ( ! isNameValid ) { return; } handleSubmit(); } } > +

+ { __( 'Enter a custom name for this block.' ) } +

{ - setEditedBlockName( newName ); - if ( newName === '' ) { - setAllowOverrides( false ); - } - } } + onChange={ setEditedBlockName } onFocus={ autoSelectInputText } /> - { isEditingPattern ? ( - { - if ( ! nameIsEmpty ) { - setAllowOverrides( checked ); - } - } } - disabled={ nameIsEmpty } - /> - ) : null } @@ -127,7 +124,10 @@ function PatternOverridesControls( { attributes, name, setAttributes } ) { setShowAllowOverridesModal( false ) } onSave={ ( newName ) => { - updateBindings( true, newName ); + flushSync( () => { + updateBindings( true, newName ); + } ); + toggleRef.current?.focus(); } } /> ) } diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js index 3aa5f8e5e766af..e5a07e79f6c2fc 100644 --- a/test/e2e/specs/editor/various/pattern-overrides.spec.js +++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js @@ -223,10 +223,10 @@ test.describe( 'Pattern Overrides', () => { requestUtils, editor, } ) => { - const paragraphId = 'paragraph-id'; + const paragraphName = 'paragraph-name'; const { id } = await requestUtils.createBlock( { title: 'Pattern', - content: ` + content: `

Editable

`, status: 'publish', From e18cf7f9f377bb1a73b840fae1b44e31fcd9c5ac Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Thu, 11 Apr 2024 13:39:10 +0800 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Daniel Richards --- packages/block-editor/src/components/block-rename/modal.js | 2 +- packages/patterns/src/components/allow-overrides-modal.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/block-rename/modal.js b/packages/block-editor/src/components/block-rename/modal.js index dcda00a92410cf..f3db0d6c36252e 100644 --- a/packages/block-editor/src/components/block-rename/modal.js +++ b/packages/block-editor/src/components/block-rename/modal.js @@ -92,7 +92,7 @@ export default function BlockRenameModal( { help={ hasOverridesWarning ? __( - 'This block allows overrides. Note that renaming could potentially break existing connections.' + 'This block allows overrides. Changing the name can cause problems with content entered into instances of this pattern.' ) : undefined } diff --git a/packages/patterns/src/components/allow-overrides-modal.js b/packages/patterns/src/components/allow-overrides-modal.js index 5200c861299a58..21d306022fd582 100644 --- a/packages/patterns/src/components/allow-overrides-modal.js +++ b/packages/patterns/src/components/allow-overrides-modal.js @@ -68,7 +68,7 @@ export default function AllowOverridesModal( { label={ __( 'Block name' ) } hideLabelFromVision help={ __( - 'This name will be used to denote the override wherever the synced pattern is used. The name here will help people understand its purpose. E.g. if you\'re creating a recipe pattern, it can be "recipe", "ingredients", etc.' + 'This name will be used to denote the override wherever the synced pattern is used. The name here will help people understand its purpose. E.g. if you\'re creating a recipe pattern, it can be "Recipe Title", "Recipe Description", etc.' ) } placeholder={ placeholder } onChange={ setEditedBlockName } From 37b75a3bc1216c5f984f062f00946f447452f3a6 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Thu, 11 Apr 2024 14:18:58 +0800 Subject: [PATCH 7/7] Fix tests --- test/e2e/specs/editor/various/pattern-overrides.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js index e5a07e79f6c2fc..0e6b8e8172f19e 100644 --- a/test/e2e/specs/editor/various/pattern-overrides.spec.js +++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js @@ -258,7 +258,7 @@ test.describe( 'Pattern Overrides', () => { name: 'core/paragraph', attributes: { content: 'edited Editable', - metadata: undefined, + metadata: { name: paragraphName }, }, }, ] );