Skip to content

Commit

Permalink
Widget editor: Add toolbar button to move between widget areas (#30826)
Browse files Browse the repository at this point in the history
* Add moveTo icon

* Add first draft of move to widget area dropdown menu

* Reorganise editor filters

* Use callback for child of DropdownMenu

* Add filter for showing move to widget area menu

* Fix __internalWidgetId prop being part of the attributes not block props

* Use toolbar item for dropdown menu toggle

* Move button to end of toolbar and place in own toolbar group

* Add moveBlockToWidgetArea action and implement for toolbar button

* Open widget area for moving before moving

* Handle scrolling when block moves into new parent

* Use store variable instead of string

* Improve naming conventions

* Add e2e test
  • Loading branch information
talldan authored Apr 15, 2021
1 parent 6e513e1 commit 3b1360e
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand All @@ -45,7 +56,7 @@ export function useScrollSelectionIntoView( ref ) {
scrollIntoView( extentNode, scrollContainer, {
onlyScrollIfNeeded: true,
} );
}, [ selectionEnd ] );
}, [ selectionRootClientId, selectionEnd ] );
}

/**
Expand Down
54 changes: 52 additions & 2 deletions packages/e2e-tests/specs/widgets/adding-widgets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
* WordPress dependencies
*/
import {
visitAdminPage,
deactivatePlugin,
activatePlugin,
activateTheme,
clickBlockToolbarButton,
deactivatePlugin,
pressKeyWithModifier,
showBlockToolbar,
visitAdminPage,
} from '@wordpress/e2e-test-utils';

/**
Expand Down Expand Up @@ -531,6 +533,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": "<div class=\\"widget widget_block widget_text\\"><div class=\\"widget-content\\">
<p>First Paragraph</p>
</div></div>",
}
` );
} );
} );

async function saveWidgets() {
Expand Down
51 changes: 51 additions & 0 deletions packages/edit-widgets/src/components/move-to-widget-area/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<ToolbarGroup>
<ToolbarItem>
{ ( toggleProps ) => (
<DropdownMenu
icon={ moveTo }
label={ __( 'Move to widget area' ) }
toggleProps={ toggleProps }
>
{ ( { onClose } ) => (
<MenuGroup label={ __( 'Move to' ) }>
<MenuItemsChoice
choices={ widgetAreas.map(
( widgetArea ) => ( {
value: widgetArea.id,
label: widgetArea.name,
info: widgetArea.description,
} )
) }
value={ currentWidgetArea?.id }
onSelect={ ( value ) => {
onSelect( value );
onClose();
} }
/>
</MenuGroup>
) }
</DropdownMenu>
) }
</ToolbarItem>
</ToolbarGroup>
);
}
5 changes: 5 additions & 0 deletions packages/edit-widgets/src/filters/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Internal dependencies
*/
import './move-to-widget-area';
import './replace-media-upload';
63 changes: 63 additions & 0 deletions packages/edit-widgets/src/filters/move-to-widget-area.js
Original file line number Diff line number Diff line change
@@ -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 (
<>
<BlockEdit { ...props } />
{ props.name !== 'core/widget-area' && (
<BlockControls>
<MoveToWidgetArea
widgetAreas={ widgetAreas }
currentWidgetArea={ currentWidgetArea }
onSelect={ ( widgetAreaId ) => {
moveBlockToWidgetArea(
props.clientId,
widgetAreaId
);
} }
/>
</BlockControls>
) }
</>
);
},
'withMoveToWidgetAreaToolbarItem'
);

addFilter(
'editor.BlockEdit',
'core/edit-widgets/block-edit',
withMoveToWidgetAreaToolbarItem
);
4 changes: 0 additions & 4 deletions packages/edit-widgets/src/hooks/index.js

This file was deleted.

2 changes: 1 addition & 1 deletion packages/edit-widgets/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
57 changes: 57 additions & 0 deletions packages/edit-widgets/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
1 change: 1 addition & 0 deletions packages/icons/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
12 changes: 12 additions & 0 deletions packages/icons/src/library/move-to.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* WordPress dependencies
*/
import { Path, SVG } from '@wordpress/primitives';

const moveTo = (
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<Path d="M19.75 9c0-1.257-.565-2.197-1.39-2.858-.797-.64-1.827-1.017-2.815-1.247-1.802-.42-3.703-.403-4.383-.396L11 4.5V6l.177-.001c.696-.006 2.416-.02 4.028.356.887.207 1.67.518 2.216.957.52.416.829.945.829 1.688 0 .592-.167.966-.407 1.23-.255.281-.656.508-1.236.674-1.19.34-2.82.346-4.607.346h-.077c-1.692 0-3.527 0-4.942.404-.732.209-1.424.545-1.935 1.108-.526.579-.796 1.33-.796 2.238 0 1.257.565 2.197 1.39 2.858.797.64 1.827 1.017 2.815 1.247 1.802.42 3.703.403 4.383.396L13 19.5h.714V22L18 18.5 13.714 15v3H13l-.177.001c-.696.006-2.416.02-4.028-.356-.887-.207-1.67-.518-2.216-.957-.52-.416-.829-.945-.829-1.688 0-.592.167-.966.407-1.23.255-.281.656-.508 1.237-.674 1.189-.34 2.819-.346 4.606-.346h.077c1.692 0 3.527 0 4.941-.404.732-.209 1.425-.545 1.936-1.108.526-.579.796-1.33.796-2.238z" />
</SVG>
);

export default moveTo;

0 comments on commit 3b1360e

Please sign in to comment.