diff --git a/packages/block-editor/src/components/selection-scroll-into-view/index.js b/packages/block-editor/src/components/selection-scroll-into-view/index.js index 5e9ff07f1ca6d8..bbfb6f7370119b 100644 --- a/packages/block-editor/src/components/selection-scroll-into-view/index.js +++ b/packages/block-editor/src/components/selection-scroll-into-view/index.js @@ -17,10 +17,21 @@ import { getBlockDOMNode } from '../../utils/dom'; import { store as blockEditorStore } from '../../store'; export function useScrollSelectionIntoView( ref ) { - const selectionEnd = useSelect( - ( select ) => select( blockEditorStore ).getBlockSelectionEnd(), - [] - ); + // Although selectionRootClientId isn't used directly in calculating + // whether scrolling should occur, it is used as a dependency of the + // effect to take into account situations where a block might be moved + // to a different parent. In this situation, the selectionEnd clientId + // remains the same, so the rootClientId is used to trigger the effect. + const { selectionRootClientId, selectionEnd } = useSelect( ( select ) => { + const selectors = select( blockEditorStore ); + const selectionEndClientId = selectors.getBlockSelectionEnd(); + return { + selectionEnd: selectionEndClientId, + selectionRootClientId: selectors.getBlockRootClientId( + selectionEndClientId + ), + }; + }, [] ); useEffect( () => { if ( ! selectionEnd ) { @@ -45,7 +56,7 @@ export function useScrollSelectionIntoView( ref ) { scrollIntoView( extentNode, scrollContainer, { onlyScrollIfNeeded: true, } ); - }, [ selectionEnd ] ); + }, [ selectionRootClientId, selectionEnd ] ); } /** diff --git a/packages/e2e-tests/specs/widgets/adding-widgets.test.js b/packages/e2e-tests/specs/widgets/adding-widgets.test.js index 3bfa04945c2a3a..7f935c12a09b47 100644 --- a/packages/e2e-tests/specs/widgets/adding-widgets.test.js +++ b/packages/e2e-tests/specs/widgets/adding-widgets.test.js @@ -2,11 +2,13 @@ * WordPress dependencies */ import { - visitAdminPage, - deactivatePlugin, activatePlugin, activateTheme, + clickBlockToolbarButton, + deactivatePlugin, pressKeyWithModifier, + showBlockToolbar, + visitAdminPage, } from '@wordpress/e2e-test-utils'; /** @@ -521,6 +523,54 @@ describe( 'Widgets screen', () => { } ` ); } ); + + it( 'allows widgets to be moved between widget areas using the dropdown in the block toolbar', async () => { + const widgetAreas = await page.$$( + '[aria-label="Block: Widget Area"][role="group"]' + ); + const [ firstWidgetArea ] = widgetAreas; + + // Insert a paragraph it should be in the first widget area. + const inserterParagraphBlock = await getBlockInGlobalInserter( + 'Paragraph' + ); + await inserterParagraphBlock.hover(); + await inserterParagraphBlock.click(); + const addedParagraphBlockInFirstWidgetArea = await firstWidgetArea.$( + '[data-block][data-type="core/paragraph"][aria-label^="Empty block"]' + ); + await addedParagraphBlockInFirstWidgetArea.focus(); + await page.keyboard.type( 'First Paragraph' ); + + // Check that the block exists in the first widget area. + await page.waitForXPath( + '//*[@aria-label="Block: Widget Area"][@role="group"][1]//p[@data-type="core/paragraph"][.="First Paragraph"]' + ); + + // Move the block to the second widget area. + await showBlockToolbar(); + await clickBlockToolbarButton( 'Move to widget area' ); + const widgetAreaButton = await page.waitForXPath( + '//button[@role="menuitemradio"][contains(.,"Footer #2")]' + ); + await widgetAreaButton.click(); + + // Check that the block exists in the second widget area. + await page.waitForXPath( + '//*[@aria-label="Block: Widget Area"][@role="group"][2]//p[@data-type="core/paragraph"][.="First Paragraph"]' + ); + + // Assert that the serialized widget areas shows the block as in the second widget area. + await saveWidgets(); + const serializedWidgetAreas2 = await getSerializedWidgetAreas(); + expect( serializedWidgetAreas2 ).toMatchInlineSnapshot( ` + Object { + "sidebar-2": "
+

First Paragraph

+
", + } + ` ); + } ); } ); async function saveWidgets() { diff --git a/packages/edit-widgets/src/components/move-to-widget-area/index.js b/packages/edit-widgets/src/components/move-to-widget-area/index.js new file mode 100644 index 00000000000000..b2d44624b4d16c --- /dev/null +++ b/packages/edit-widgets/src/components/move-to-widget-area/index.js @@ -0,0 +1,51 @@ +/** + * WordPress dependencies + */ +import { + DropdownMenu, + MenuGroup, + MenuItemsChoice, + ToolbarGroup, + ToolbarItem, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { moveTo } from '@wordpress/icons'; + +export default function MoveToWidgetArea( { + currentWidgetArea, + widgetAreas, + onSelect, +} ) { + return ( + + + { ( toggleProps ) => ( + + { ( { onClose } ) => ( + + ( { + value: widgetArea.id, + label: widgetArea.name, + info: widgetArea.description, + } ) + ) } + value={ currentWidgetArea?.id } + onSelect={ ( value ) => { + onSelect( value ); + onClose(); + } } + /> + + ) } + + ) } + + + ); +} diff --git a/packages/edit-widgets/src/filters/index.js b/packages/edit-widgets/src/filters/index.js new file mode 100644 index 00000000000000..3cd2ae58dc5bf3 --- /dev/null +++ b/packages/edit-widgets/src/filters/index.js @@ -0,0 +1,5 @@ +/** + * Internal dependencies + */ +import './move-to-widget-area'; +import './replace-media-upload'; diff --git a/packages/edit-widgets/src/filters/move-to-widget-area.js b/packages/edit-widgets/src/filters/move-to-widget-area.js new file mode 100644 index 00000000000000..5a825c16e0b93f --- /dev/null +++ b/packages/edit-widgets/src/filters/move-to-widget-area.js @@ -0,0 +1,63 @@ +/** + * WordPress dependencies + */ + +import { BlockControls } from '@wordpress/block-editor'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { addFilter } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import MoveToWidgetArea from '../components/move-to-widget-area'; +import { store as editWidgetsStore } from '../store'; + +const withMoveToWidgetAreaToolbarItem = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const { __internalWidgetId } = props.attributes; + const { widgetAreas, currentWidgetArea } = useSelect( + ( select ) => { + const selectors = select( editWidgetsStore ); + return { + widgetAreas: selectors.getWidgetAreas(), + currentWidgetArea: __internalWidgetId + ? selectors.getWidgetAreaForWidgetId( + __internalWidgetId + ) + : undefined, + }; + }, + [ __internalWidgetId ] + ); + + const { moveBlockToWidgetArea } = useDispatch( editWidgetsStore ); + + return ( + <> + + { props.name !== 'core/widget-area' && ( + + { + moveBlockToWidgetArea( + props.clientId, + widgetAreaId + ); + } } + /> + + ) } + + ); + }, + 'withMoveToWidgetAreaToolbarItem' +); + +addFilter( + 'editor.BlockEdit', + 'core/edit-widgets/block-edit', + withMoveToWidgetAreaToolbarItem +); diff --git a/packages/edit-widgets/src/hooks/components/index.js b/packages/edit-widgets/src/filters/replace-media-upload.js similarity index 100% rename from packages/edit-widgets/src/hooks/components/index.js rename to packages/edit-widgets/src/filters/replace-media-upload.js diff --git a/packages/edit-widgets/src/hooks/index.js b/packages/edit-widgets/src/hooks/index.js deleted file mode 100644 index c6cbc1d173e861..00000000000000 --- a/packages/edit-widgets/src/hooks/index.js +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Internal dependencies - */ -import './components'; diff --git a/packages/edit-widgets/src/index.js b/packages/edit-widgets/src/index.js index 521c997ec791b3..78ea17b68c8a3b 100644 --- a/packages/edit-widgets/src/index.js +++ b/packages/edit-widgets/src/index.js @@ -17,7 +17,7 @@ import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions } from '@wor * Internal dependencies */ import './store'; -import './hooks'; +import './filters'; import * as widgetArea from './blocks/widget-area'; import Layout from './components/layout'; import registerLegacyWidgetVariations from './register-legacy-widget-variations'; diff --git a/packages/edit-widgets/src/store/actions.js b/packages/edit-widgets/src/store/actions.js index c8934021c18a5a..9c5b86924e2908 100644 --- a/packages/edit-widgets/src/store/actions.js +++ b/packages/edit-widgets/src/store/actions.js @@ -321,3 +321,60 @@ export function* closeGeneralSidebar() { editWidgetsStoreName ); } + +/** + * Action that handles moving a block between widget areas + * + * @param {string} clientId The clientId of the block to move. + * @param {string} widgetAreaId The id of the widget area to move the block to. + */ +export function* moveBlockToWidgetArea( clientId, widgetAreaId ) { + const sourceRootClientId = yield select( + 'core/block-editor', + 'getBlockRootClientId', + [ clientId ] + ); + + // Search the top level blocks (widget areas) for the one with the matching + // id attribute. Makes the assumption that all top-level blocks are widget + // areas. + const widgetAreas = yield select( 'core/block-editor', 'getBlocks' ); + const destinationWidgetAreaBlock = widgetAreas.find( + ( { attributes } ) => attributes.id === widgetAreaId + ); + const destinationRootClientId = destinationWidgetAreaBlock.clientId; + + // Get the index for moving to the end of the the destination widget area. + const destinationInnerBlocksClientIds = yield select( + 'core/block-editor', + 'getBlockOrder', + destinationRootClientId + ); + const destinationIndex = destinationInnerBlocksClientIds.length; + + // Reveal the widget area, if it's not open. + const isDestinationWidgetAreaOpen = yield select( + editWidgetsStoreName, + 'getIsWidgetAreaOpen', + destinationRootClientId + ); + + if ( ! isDestinationWidgetAreaOpen ) { + yield dispatch( + editWidgetsStoreName, + 'setIsWidgetAreaOpen', + destinationRootClientId, + true + ); + } + + // Move the block. + yield dispatch( + 'core/block-editor', + 'moveBlocksToPosition', + [ clientId ], + sourceRootClientId, + destinationRootClientId, + destinationIndex + ); +} diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index 22e0b3aa7a7073..3fa3853b61568f 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -122,6 +122,7 @@ export { default as more } from './library/more'; export { default as moreHorizontal } from './library/more-horizontal'; export { default as moreHorizontalMobile } from './library/more-horizontal-mobile'; export { default as moreVertical } from './library/more-vertical'; +export { default as moveTo } from './library/move-to'; export { default as navigation } from './library/navigation'; export { default as overlayText } from './library/overlay-text'; export { default as pageBreak } from './library/page-break'; diff --git a/packages/icons/src/library/move-to.js b/packages/icons/src/library/move-to.js new file mode 100644 index 00000000000000..b4caef9ce499e5 --- /dev/null +++ b/packages/icons/src/library/move-to.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { Path, SVG } from '@wordpress/primitives'; + +const moveTo = ( + + + +); + +export default moveTo;