diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/use-sidebar-block-editor.js b/packages/customize-widgets/src/components/sidebar-block-editor/use-sidebar-block-editor.js index 340aca24cb61d7..d91ec2ac092ca0 100644 --- a/packages/customize-widgets/src/components/sidebar-block-editor/use-sidebar-block-editor.js +++ b/packages/customize-widgets/src/components/sidebar-block-editor/use-sidebar-block-editor.js @@ -1,100 +1,19 @@ /** * External dependencies */ -import { omit, isEqual } from 'lodash'; +import { isEqual } from 'lodash'; /** * WordPress dependencies */ -import { serialize, parse, createBlock } from '@wordpress/blocks'; import { useState, useEffect, useCallback } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; import { getWidgetIdFromBlock, addWidgetIdToBlock } from '@wordpress/widgets'; -function blockToWidget( block, existingWidget = null ) { - let widget; - - const isValidLegacyWidgetBlock = - block.name === 'core/legacy-widget' && - ( block.attributes.id || block.attributes.instance ); - - if ( isValidLegacyWidgetBlock ) { - if ( block.attributes.id ) { - // Widget that does not extend WP_Widget. - widget = { - id: block.attributes.id, - }; - } else { - const { encoded, hash, raw, ...rest } = block.attributes.instance; - - // Widget that extends WP_Widget. - widget = { - idBase: block.attributes.idBase, - instance: { - ...existingWidget?.instance, - // Required only for the customizer. - is_widget_customizer_js_value: true, - encoded_serialized_instance: encoded, - instance_hash_key: hash, - raw_instance: raw, - ...rest, - }, - }; - } - } else { - const instance = { - content: serialize( block ), - }; - widget = { - idBase: 'block', - widgetClass: 'WP_Widget_Block', - instance: { - raw_instance: instance, - }, - }; - } - - return { - ...omit( existingWidget, [ 'form', 'rendered' ] ), - ...widget, - }; -} - -function widgetToBlock( { id, idBase, number, instance } ) { - let block; - - const { - encoded_serialized_instance: encoded, - instance_hash_key: hash, - raw_instance: raw, - ...rest - } = instance; - - if ( idBase === 'block' ) { - const parsedBlocks = parse( raw.content ); - block = parsedBlocks.length - ? parsedBlocks[ 0 ] - : createBlock( 'core/paragraph', {} ); - } else if ( number ) { - // Widget that extends WP_Widget. - block = createBlock( 'core/legacy-widget', { - idBase, - instance: { - encoded, - hash, - raw, - ...rest, - }, - } ); - } else { - // Widget that does not extend WP_Widget. - block = createBlock( 'core/legacy-widget', { - id, - } ); - } - - return addWidgetIdToBlock( block, id ); -} +/** + * Internal dependencies + */ +import { blockToWidget, widgetToBlock } from '../../utils'; function widgetsToBlocks( widgets ) { return widgets.map( ( widget ) => widgetToBlock( widget ) ); diff --git a/packages/customize-widgets/src/filters/move-to-sidebar.js b/packages/customize-widgets/src/filters/move-to-sidebar.js index e91dafe09aa0f5..9e7910ce785a6a 100644 --- a/packages/customize-widgets/src/filters/move-to-sidebar.js +++ b/packages/customize-widgets/src/filters/move-to-sidebar.js @@ -11,7 +11,7 @@ import { store as blockEditorStore, } from '@wordpress/block-editor'; import { createHigherOrderComponent } from '@wordpress/compose'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { addFilter } from '@wordpress/hooks'; import { MoveToWidgetArea, getWidgetIdFromBlock } from '@wordpress/widgets'; @@ -22,14 +22,17 @@ import { useSidebarControls, useActiveSidebarControl, } from '../components/sidebar-controls'; +import { useFocusControl } from '../components/focus-control'; +import { blockToWidget } from '../utils'; const withMoveToSidebarToolbarItem = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { - const widgetId = getWidgetIdFromBlock( props ); + let widgetId = getWidgetIdFromBlock( props ); const sidebarControls = useSidebarControls(); const activeSidebarControl = useActiveSidebarControl(); const hasMultipleSidebars = sidebarControls?.length > 1; const blockName = props.name; + const clientId = props.clientId; const canInsertBlockInSidebar = useSelect( ( select ) => { // Use an empty string to represent the root block list, which @@ -41,19 +44,46 @@ const withMoveToSidebarToolbarItem = createHigherOrderComponent( }, [ blockName ] ); + const block = useSelect( + ( select ) => select( blockEditorStore ).getBlock( clientId ), + [ clientId ] + ); + const { removeBlock } = useDispatch( blockEditorStore ); + const [ , focusWidget ] = useFocusControl(); function moveToSidebar( sidebarControlId ) { const newSidebarControl = sidebarControls.find( ( sidebarControl ) => sidebarControl.id === sidebarControlId ); - const oldSetting = activeSidebarControl.setting; - const newSetting = newSidebarControl.setting; + if ( widgetId ) { + /** + * If there's a widgetId, move it to the other sidebar. + */ + const oldSetting = activeSidebarControl.setting; + const newSetting = newSidebarControl.setting; + + oldSetting( without( oldSetting(), widgetId ) ); + newSetting( [ ...newSetting(), widgetId ] ); + } else { + /** + * If there isn't a widgetId, it's most likely a inner block. + * First, remove the block in the original sidebar, + * then, create a new widget in the new sidebar and get back its widgetId. + */ + const sidebarAdapter = newSidebarControl.sidebarAdapter; - oldSetting( without( oldSetting(), widgetId ) ); - newSetting( [ ...newSetting(), widgetId ] ); + removeBlock( clientId ); + const addedWidgetIds = sidebarAdapter.setWidgets( [ + ...sidebarAdapter.getWidgets(), + blockToWidget( block ), + ] ); + // The last non-null id is the added widget's id. + widgetId = addedWidgetIds.reverse().find( ( id ) => !! id ); + } - newSidebarControl.expand(); + // Move focus to the moved widget and expand the sidebar. + focusWidget( widgetId ); } return ( diff --git a/packages/customize-widgets/src/utils.js b/packages/customize-widgets/src/utils.js index baa8c70b01ac2b..68fc6b2fe2d71a 100644 --- a/packages/customize-widgets/src/utils.js +++ b/packages/customize-widgets/src/utils.js @@ -1,4 +1,14 @@ // @ts-check +/** + * WordPress dependencies + */ +import { serialize, parse, createBlock } from '@wordpress/blocks'; +import { addWidgetIdToBlock } from '@wordpress/widgets'; + +/** + * External dependencies + */ +import { omit } from 'lodash'; /** * Convert settingId to widgetId. @@ -18,3 +28,105 @@ export function settingIdToWidgetId( settingId ) { return settingId; } + +/** + * Transform a block to a customizable widget. + * + * @param {WPBlock} block The block to be transformed from. + * @param {Object} existingWidget The widget to be extended from. + * @return {Object} The transformed widget. + */ +export function blockToWidget( block, existingWidget = null ) { + let widget; + + const isValidLegacyWidgetBlock = + block.name === 'core/legacy-widget' && + ( block.attributes.id || block.attributes.instance ); + + if ( isValidLegacyWidgetBlock ) { + if ( block.attributes.id ) { + // Widget that does not extend WP_Widget. + widget = { + id: block.attributes.id, + }; + } else { + const { encoded, hash, raw, ...rest } = block.attributes.instance; + + // Widget that extends WP_Widget. + widget = { + idBase: block.attributes.idBase, + instance: { + ...existingWidget?.instance, + // Required only for the customizer. + is_widget_customizer_js_value: true, + encoded_serialized_instance: encoded, + instance_hash_key: hash, + raw_instance: raw, + ...rest, + }, + }; + } + } else { + const instance = { + content: serialize( block ), + }; + widget = { + idBase: 'block', + widgetClass: 'WP_Widget_Block', + instance: { + raw_instance: instance, + }, + }; + } + + return { + ...omit( existingWidget, [ 'form', 'rendered' ] ), + ...widget, + }; +} + +/** + * Transform a widget to a block. + * + * @param {Object} widget The widget to be transformed from. + * @param {string} widget.id The widget id. + * @param {string} widget.idBase The id base of the widget. + * @param {number} widget.number The number/index of the widget. + * @param {Object} widget.instance The instance of the widget. + * @return {WPBlock} The transformed block. + */ +export function widgetToBlock( { id, idBase, number, instance } ) { + let block; + + const { + encoded_serialized_instance: encoded, + instance_hash_key: hash, + raw_instance: raw, + ...rest + } = instance; + + if ( idBase === 'block' ) { + const parsedBlocks = parse( raw.content ); + block = parsedBlocks.length + ? parsedBlocks[ 0 ] + : createBlock( 'core/paragraph', {} ); + } else if ( number ) { + // Widget that extends WP_Widget. + block = createBlock( 'core/legacy-widget', { + idBase, + instance: { + encoded, + hash, + raw, + ...rest, + }, + } ); + } else { + // Widget that does not extend WP_Widget. + block = createBlock( 'core/legacy-widget', { + id, + } ); + } + + return addWidgetIdToBlock( block, id ); +} diff --git a/packages/e2e-tests/specs/widgets/customizing-widgets.test.js b/packages/e2e-tests/specs/widgets/customizing-widgets.test.js index 701ece8f38220b..960d93ce1b2992 100644 --- a/packages/e2e-tests/specs/widgets/customizing-widgets.test.js +++ b/packages/e2e-tests/specs/widgets/customizing-widgets.test.js @@ -700,6 +700,69 @@ describe( 'Widgets Customizer', () => { "The page delivered both an 'X-Frame-Options' header and a 'Content-Security-Policy' header with a 'frame-ancestors' directive. Although the 'X-Frame-Options' header alone would have blocked embedding, it has been ignored." ); } ); + + it( 'should move (inner) blocks to another sidebar', async () => { + const widgetsPanel = await find( { + role: 'heading', + name: /Widgets/, + level: 3, + } ); + await widgetsPanel.click(); + + const footer1Section = await find( { + role: 'heading', + name: /Footer #1/, + level: 3, + } ); + await footer1Section.click(); + + await addBlock( 'Paragraph' ); + await page.keyboard.type( 'First Paragraph' ); + + await showBlockToolbar(); + await clickBlockToolbarButton( 'Options' ); + const groupButton = await find( { + role: 'menuitem', + name: 'Group', + } ); + await groupButton.click(); + + // Refocus the paragraph block. + const paragraphBlock = await find( { + role: 'group', + name: 'Paragraph block', + value: 'First Paragraph', + } ); + await paragraphBlock.focus(); + await showBlockToolbar(); + await clickBlockToolbarButton( 'Move to widget area' ); + + const footer2Option = await find( { + role: 'menuitemradio', + name: 'Footer #2', + } ); + await footer2Option.click(); + + // Should switch to and expand Footer #2. + await expect( { + role: 'heading', + name: 'Customizing ▸ Widgets Footer #2', + } ).toBeFound(); + + // The paragraph block should be moved to the new sidebar and have focus. + const movedParagraphBlockQuery = { + role: 'group', + name: 'Paragraph block', + value: 'First Paragraph', + }; + await expect( movedParagraphBlockQuery ).toBeFound(); + const movedParagraphBlock = await find( movedParagraphBlockQuery ); + await expect( movedParagraphBlock ).toHaveFocus(); + + expect( console ).toHaveWarned( + "The page delivered both an 'X-Frame-Options' header and a 'Content-Security-Policy' header with a 'frame-ancestors' directive. Although the 'X-Frame-Options' header alone would have blocked embedding, it has been ignored." + ); + } ); } ); /**