Skip to content

Commit 007d897

Browse files
kevin940726youknowriad
authored andcommitted
Fix moving inner blocks in the Widgets Customizer (#33243)
1 parent 19d19bc commit 007d897

File tree

4 files changed

+217
-93
lines changed

4 files changed

+217
-93
lines changed

packages/customize-widgets/src/components/sidebar-block-editor/use-sidebar-block-editor.js

Lines changed: 5 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,19 @@
11
/**
22
* External dependencies
33
*/
4-
import { omit, isEqual } from 'lodash';
4+
import { isEqual } from 'lodash';
55

66
/**
77
* WordPress dependencies
88
*/
9-
import { serialize, parse, createBlock } from '@wordpress/blocks';
109
import { useState, useEffect, useCallback } from '@wordpress/element';
1110
import isShallowEqual from '@wordpress/is-shallow-equal';
1211
import { getWidgetIdFromBlock, addWidgetIdToBlock } from '@wordpress/widgets';
1312

14-
function blockToWidget( block, existingWidget = null ) {
15-
let widget;
16-
17-
const isValidLegacyWidgetBlock =
18-
block.name === 'core/legacy-widget' &&
19-
( block.attributes.id || block.attributes.instance );
20-
21-
if ( isValidLegacyWidgetBlock ) {
22-
if ( block.attributes.id ) {
23-
// Widget that does not extend WP_Widget.
24-
widget = {
25-
id: block.attributes.id,
26-
};
27-
} else {
28-
const { encoded, hash, raw, ...rest } = block.attributes.instance;
29-
30-
// Widget that extends WP_Widget.
31-
widget = {
32-
idBase: block.attributes.idBase,
33-
instance: {
34-
...existingWidget?.instance,
35-
// Required only for the customizer.
36-
is_widget_customizer_js_value: true,
37-
encoded_serialized_instance: encoded,
38-
instance_hash_key: hash,
39-
raw_instance: raw,
40-
...rest,
41-
},
42-
};
43-
}
44-
} else {
45-
const instance = {
46-
content: serialize( block ),
47-
};
48-
widget = {
49-
idBase: 'block',
50-
widgetClass: 'WP_Widget_Block',
51-
instance: {
52-
raw_instance: instance,
53-
},
54-
};
55-
}
56-
57-
return {
58-
...omit( existingWidget, [ 'form', 'rendered' ] ),
59-
...widget,
60-
};
61-
}
62-
63-
function widgetToBlock( { id, idBase, number, instance } ) {
64-
let block;
65-
66-
const {
67-
encoded_serialized_instance: encoded,
68-
instance_hash_key: hash,
69-
raw_instance: raw,
70-
...rest
71-
} = instance;
72-
73-
if ( idBase === 'block' ) {
74-
const parsedBlocks = parse( raw.content );
75-
block = parsedBlocks.length
76-
? parsedBlocks[ 0 ]
77-
: createBlock( 'core/paragraph', {} );
78-
} else if ( number ) {
79-
// Widget that extends WP_Widget.
80-
block = createBlock( 'core/legacy-widget', {
81-
idBase,
82-
instance: {
83-
encoded,
84-
hash,
85-
raw,
86-
...rest,
87-
},
88-
} );
89-
} else {
90-
// Widget that does not extend WP_Widget.
91-
block = createBlock( 'core/legacy-widget', {
92-
id,
93-
} );
94-
}
95-
96-
return addWidgetIdToBlock( block, id );
97-
}
13+
/**
14+
* Internal dependencies
15+
*/
16+
import { blockToWidget, widgetToBlock } from '../../utils';
9817

9918
function widgetsToBlocks( widgets ) {
10019
return widgets.map( ( widget ) => widgetToBlock( widget ) );

packages/customize-widgets/src/filters/move-to-sidebar.js

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
store as blockEditorStore,
1212
} from '@wordpress/block-editor';
1313
import { createHigherOrderComponent } from '@wordpress/compose';
14-
import { useSelect } from '@wordpress/data';
14+
import { useSelect, useDispatch } from '@wordpress/data';
1515
import { addFilter } from '@wordpress/hooks';
1616
import { MoveToWidgetArea, getWidgetIdFromBlock } from '@wordpress/widgets';
1717

@@ -22,14 +22,17 @@ import {
2222
useSidebarControls,
2323
useActiveSidebarControl,
2424
} from '../components/sidebar-controls';
25+
import { useFocusControl } from '../components/focus-control';
26+
import { blockToWidget } from '../utils';
2527

2628
const withMoveToSidebarToolbarItem = createHigherOrderComponent(
2729
( BlockEdit ) => ( props ) => {
28-
const widgetId = getWidgetIdFromBlock( props );
30+
let widgetId = getWidgetIdFromBlock( props );
2931
const sidebarControls = useSidebarControls();
3032
const activeSidebarControl = useActiveSidebarControl();
3133
const hasMultipleSidebars = sidebarControls?.length > 1;
3234
const blockName = props.name;
35+
const clientId = props.clientId;
3336
const canInsertBlockInSidebar = useSelect(
3437
( select ) => {
3538
// Use an empty string to represent the root block list, which
@@ -41,19 +44,46 @@ const withMoveToSidebarToolbarItem = createHigherOrderComponent(
4144
},
4245
[ blockName ]
4346
);
47+
const block = useSelect(
48+
( select ) => select( blockEditorStore ).getBlock( clientId ),
49+
[ clientId ]
50+
);
51+
const { removeBlock } = useDispatch( blockEditorStore );
52+
const [ , focusWidget ] = useFocusControl();
4453

4554
function moveToSidebar( sidebarControlId ) {
4655
const newSidebarControl = sidebarControls.find(
4756
( sidebarControl ) => sidebarControl.id === sidebarControlId
4857
);
4958

50-
const oldSetting = activeSidebarControl.setting;
51-
const newSetting = newSidebarControl.setting;
59+
if ( widgetId ) {
60+
/**
61+
* If there's a widgetId, move it to the other sidebar.
62+
*/
63+
const oldSetting = activeSidebarControl.setting;
64+
const newSetting = newSidebarControl.setting;
65+
66+
oldSetting( without( oldSetting(), widgetId ) );
67+
newSetting( [ ...newSetting(), widgetId ] );
68+
} else {
69+
/**
70+
* If there isn't a widgetId, it's most likely a inner block.
71+
* First, remove the block in the original sidebar,
72+
* then, create a new widget in the new sidebar and get back its widgetId.
73+
*/
74+
const sidebarAdapter = newSidebarControl.sidebarAdapter;
5275

53-
oldSetting( without( oldSetting(), widgetId ) );
54-
newSetting( [ ...newSetting(), widgetId ] );
76+
removeBlock( clientId );
77+
const addedWidgetIds = sidebarAdapter.setWidgets( [
78+
...sidebarAdapter.getWidgets(),
79+
blockToWidget( block ),
80+
] );
81+
// The last non-null id is the added widget's id.
82+
widgetId = addedWidgetIds.reverse().find( ( id ) => !! id );
83+
}
5584

56-
newSidebarControl.expand();
85+
// Move focus to the moved widget and expand the sidebar.
86+
focusWidget( widgetId );
5787
}
5888

5989
return (

packages/customize-widgets/src/utils.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
// @ts-check
2+
/**
3+
* WordPress dependencies
4+
*/
5+
import { serialize, parse, createBlock } from '@wordpress/blocks';
6+
import { addWidgetIdToBlock } from '@wordpress/widgets';
7+
8+
/**
9+
* External dependencies
10+
*/
11+
import { omit } from 'lodash';
212

313
/**
414
* Convert settingId to widgetId.
@@ -18,3 +28,105 @@ export function settingIdToWidgetId( settingId ) {
1828

1929
return settingId;
2030
}
31+
32+
/**
33+
* Transform a block to a customizable widget.
34+
*
35+
* @param {WPBlock} block The block to be transformed from.
36+
* @param {Object} existingWidget The widget to be extended from.
37+
* @return {Object} The transformed widget.
38+
*/
39+
export function blockToWidget( block, existingWidget = null ) {
40+
let widget;
41+
42+
const isValidLegacyWidgetBlock =
43+
block.name === 'core/legacy-widget' &&
44+
( block.attributes.id || block.attributes.instance );
45+
46+
if ( isValidLegacyWidgetBlock ) {
47+
if ( block.attributes.id ) {
48+
// Widget that does not extend WP_Widget.
49+
widget = {
50+
id: block.attributes.id,
51+
};
52+
} else {
53+
const { encoded, hash, raw, ...rest } = block.attributes.instance;
54+
55+
// Widget that extends WP_Widget.
56+
widget = {
57+
idBase: block.attributes.idBase,
58+
instance: {
59+
...existingWidget?.instance,
60+
// Required only for the customizer.
61+
is_widget_customizer_js_value: true,
62+
encoded_serialized_instance: encoded,
63+
instance_hash_key: hash,
64+
raw_instance: raw,
65+
...rest,
66+
},
67+
};
68+
}
69+
} else {
70+
const instance = {
71+
content: serialize( block ),
72+
};
73+
widget = {
74+
idBase: 'block',
75+
widgetClass: 'WP_Widget_Block',
76+
instance: {
77+
raw_instance: instance,
78+
},
79+
};
80+
}
81+
82+
return {
83+
...omit( existingWidget, [ 'form', 'rendered' ] ),
84+
...widget,
85+
};
86+
}
87+
88+
/**
89+
* Transform a widget to a block.
90+
*
91+
* @param {Object} widget The widget to be transformed from.
92+
* @param {string} widget.id The widget id.
93+
* @param {string} widget.idBase The id base of the widget.
94+
* @param {number} widget.number The number/index of the widget.
95+
* @param {Object} widget.instance The instance of the widget.
96+
* @return {WPBlock} The transformed block.
97+
*/
98+
export function widgetToBlock( { id, idBase, number, instance } ) {
99+
let block;
100+
101+
const {
102+
encoded_serialized_instance: encoded,
103+
instance_hash_key: hash,
104+
raw_instance: raw,
105+
...rest
106+
} = instance;
107+
108+
if ( idBase === 'block' ) {
109+
const parsedBlocks = parse( raw.content );
110+
block = parsedBlocks.length
111+
? parsedBlocks[ 0 ]
112+
: createBlock( 'core/paragraph', {} );
113+
} else if ( number ) {
114+
// Widget that extends WP_Widget.
115+
block = createBlock( 'core/legacy-widget', {
116+
idBase,
117+
instance: {
118+
encoded,
119+
hash,
120+
raw,
121+
...rest,
122+
},
123+
} );
124+
} else {
125+
// Widget that does not extend WP_Widget.
126+
block = createBlock( 'core/legacy-widget', {
127+
id,
128+
} );
129+
}
130+
131+
return addWidgetIdToBlock( block, id );
132+
}

packages/e2e-tests/specs/widgets/customizing-widgets.test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,69 @@ describe( 'Widgets Customizer', () => {
700700
"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."
701701
);
702702
} );
703+
704+
it( 'should move (inner) blocks to another sidebar', async () => {
705+
const widgetsPanel = await find( {
706+
role: 'heading',
707+
name: /Widgets/,
708+
level: 3,
709+
} );
710+
await widgetsPanel.click();
711+
712+
const footer1Section = await find( {
713+
role: 'heading',
714+
name: /Footer #1/,
715+
level: 3,
716+
} );
717+
await footer1Section.click();
718+
719+
await addBlock( 'Paragraph' );
720+
await page.keyboard.type( 'First Paragraph' );
721+
722+
await showBlockToolbar();
723+
await clickBlockToolbarButton( 'Options' );
724+
const groupButton = await find( {
725+
role: 'menuitem',
726+
name: 'Group',
727+
} );
728+
await groupButton.click();
729+
730+
// Refocus the paragraph block.
731+
const paragraphBlock = await find( {
732+
role: 'group',
733+
name: 'Paragraph block',
734+
value: 'First Paragraph',
735+
} );
736+
await paragraphBlock.focus();
737+
await showBlockToolbar();
738+
await clickBlockToolbarButton( 'Move to widget area' );
739+
740+
const footer2Option = await find( {
741+
role: 'menuitemradio',
742+
name: 'Footer #2',
743+
} );
744+
await footer2Option.click();
745+
746+
// Should switch to and expand Footer #2.
747+
await expect( {
748+
role: 'heading',
749+
name: 'Customizing ▸ Widgets Footer #2',
750+
} ).toBeFound();
751+
752+
// The paragraph block should be moved to the new sidebar and have focus.
753+
const movedParagraphBlockQuery = {
754+
role: 'group',
755+
name: 'Paragraph block',
756+
value: 'First Paragraph',
757+
};
758+
await expect( movedParagraphBlockQuery ).toBeFound();
759+
const movedParagraphBlock = await find( movedParagraphBlockQuery );
760+
await expect( movedParagraphBlock ).toHaveFocus();
761+
762+
expect( console ).toHaveWarned(
763+
"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."
764+
);
765+
} );
703766
} );
704767

705768
/**

0 commit comments

Comments
 (0)