diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php
index b97cb649bb0bc..f5d40ae8a2110 100644
--- a/lib/experimental/editor-settings.php
+++ b/lib/experimental/editor-settings.php
@@ -22,6 +22,9 @@ function gutenberg_enable_experiments() {
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-color-randomizer', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableColorRandomizer = true', 'before' );
}
+ if ( $gutenberg_experiments && array_key_exists( 'gutenberg-grid-interactivity', $gutenberg_experiments ) ) {
+ wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableGridInteractivity = true', 'before' );
+ }
if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) {
wp_add_inline_script( 'wp-block-library', 'window.__experimentalDisableTinymce = true', 'before' );
}
diff --git a/lib/experiments-page.php b/lib/experiments-page.php
index 6608fbb138c58..f66e093219263 100644
--- a/lib/experiments-page.php
+++ b/lib/experiments-page.php
@@ -90,6 +90,7 @@ function gutenberg_initialize_experiments_settings() {
'id' => 'gutenberg-color-randomizer',
)
);
+
add_settings_field(
'gutenberg-form-blocks',
__( 'Form and input blocks ', 'gutenberg' ),
@@ -101,6 +102,19 @@ function gutenberg_initialize_experiments_settings() {
'id' => 'gutenberg-form-blocks',
)
);
+
+ add_settings_field(
+ 'gutenberg-grid-interactivity',
+ __( 'Grid interactivty ', 'gutenberg' ),
+ 'gutenberg_display_experiment_field',
+ 'gutenberg-experiments',
+ 'gutenberg_experiments_section',
+ array(
+ 'label' => __( 'Test enhancements to the Grid block that let you move and resize items in the editor canvas.', 'gutenberg' ),
+ 'id' => 'gutenberg-grid-interactivity',
+ )
+ );
+
add_settings_field(
'gutenberg-no-tinymce',
__( 'Disable TinyMCE and Classic block', 'gutenberg' ),
diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss
index ff21d1d8df8f3..762508f921a00 100644
--- a/packages/base-styles/_z-index.scss
+++ b/packages/base-styles/_z-index.scss
@@ -108,6 +108,9 @@ $z-layers: (
// Above the block list and the header.
".block-editor-block-popover": 31,
+ // Below the block toolbar.
+ ".block-editor-grid-visualizer": 30,
+
// Show snackbars above everything (similar to popovers)
".components-snackbar-list": 100000,
diff --git a/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js b/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js
new file mode 100644
index 0000000000000..54683e48beeea
--- /dev/null
+++ b/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js
@@ -0,0 +1,100 @@
+/**
+ * WordPress dependencies
+ */
+import { ResizableBox } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs';
+import BlockPopoverCover from '../block-popover/cover';
+import { getComputedCSS } from './utils';
+
+export function GridItemResizer( { clientId, onChange } ) {
+ const blockElement = useBlockElement( clientId );
+ if ( ! blockElement ) {
+ return null;
+ }
+ return (
+
+ {
+ const gridElement = blockElement.parentElement;
+ const columnGap = parseFloat(
+ getComputedCSS( gridElement, 'column-gap' )
+ );
+ const rowGap = parseFloat(
+ getComputedCSS( gridElement, 'row-gap' )
+ );
+ const gridColumnLines = getGridLines(
+ getComputedCSS( gridElement, 'grid-template-columns' ),
+ columnGap
+ );
+ const gridRowLines = getGridLines(
+ getComputedCSS( gridElement, 'grid-template-rows' ),
+ rowGap
+ );
+ const columnStart = getClosestLine(
+ gridColumnLines,
+ blockElement.offsetLeft
+ );
+ const rowStart = getClosestLine(
+ gridRowLines,
+ blockElement.offsetTop
+ );
+ const columnEnd = getClosestLine(
+ gridColumnLines,
+ blockElement.offsetLeft + boxElement.offsetWidth
+ );
+ const rowEnd = getClosestLine(
+ gridRowLines,
+ blockElement.offsetTop + boxElement.offsetHeight
+ );
+ onChange( {
+ columnSpan: Math.max( columnEnd - columnStart, 1 ),
+ rowSpan: Math.max( rowEnd - rowStart, 1 ),
+ } );
+ } }
+ />
+
+ );
+}
+
+function getGridLines( template, gap ) {
+ const lines = [ 0 ];
+ for ( const size of template.split( ' ' ) ) {
+ const line = parseFloat( size );
+ lines.push( lines[ lines.length - 1 ] + line + gap );
+ }
+ return lines;
+}
+
+function getClosestLine( lines, position ) {
+ return lines.reduce(
+ ( closest, line, index ) =>
+ Math.abs( line - position ) <
+ Math.abs( lines[ closest ] - position )
+ ? index
+ : closest,
+ 0
+ );
+}
diff --git a/packages/block-editor/src/components/grid-visualizer/grid-visualizer.js b/packages/block-editor/src/components/grid-visualizer/grid-visualizer.js
new file mode 100644
index 0000000000000..2ca65eb6722e4
--- /dev/null
+++ b/packages/block-editor/src/components/grid-visualizer/grid-visualizer.js
@@ -0,0 +1,81 @@
+/**
+ * WordPress dependencies
+ */
+import { useState, useEffect } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs';
+import BlockPopoverCover from '../block-popover/cover';
+import { getComputedCSS } from './utils';
+
+export function GridVisualizer( { clientId } ) {
+ const blockElement = useBlockElement( clientId );
+ if ( ! blockElement ) {
+ return null;
+ }
+ return (
+
+
+
+ );
+}
+
+function GridVisualizerGrid( { blockElement } ) {
+ const [ gridInfo, setGridInfo ] = useState( () =>
+ getGridInfo( blockElement )
+ );
+ useEffect( () => {
+ const observers = [];
+ for ( const element of [ blockElement, ...blockElement.children ] ) {
+ const observer = new window.ResizeObserver( () => {
+ setGridInfo( getGridInfo( blockElement ) );
+ } );
+ observer.observe( element );
+ observers.push( observer );
+ }
+ return () => {
+ for ( const observer of observers ) {
+ observer.disconnect();
+ }
+ };
+ }, [ blockElement ] );
+ return (
+
+ { Array.from( { length: gridInfo.numItems }, ( _, i ) => (
+
+ ) ) }
+
+ );
+}
+
+function getGridInfo( blockElement ) {
+ const gridTemplateColumns = getComputedCSS(
+ blockElement,
+ 'grid-template-columns'
+ );
+ const gridTemplateRows = getComputedCSS(
+ blockElement,
+ 'grid-template-rows'
+ );
+ const numColumns = gridTemplateColumns.split( ' ' ).length;
+ const numRows = gridTemplateRows.split( ' ' ).length;
+ const numItems = numColumns * numRows;
+ return {
+ numItems,
+ style: {
+ gridTemplateColumns,
+ gridTemplateRows,
+ gap: getComputedCSS( blockElement, 'gap' ),
+ padding: getComputedCSS( blockElement, 'padding' ),
+ },
+ };
+}
diff --git a/packages/block-editor/src/components/grid-visualizer/index.js b/packages/block-editor/src/components/grid-visualizer/index.js
new file mode 100644
index 0000000000000..add845d702203
--- /dev/null
+++ b/packages/block-editor/src/components/grid-visualizer/index.js
@@ -0,0 +1,2 @@
+export { GridVisualizer } from './grid-visualizer';
+export { GridItemResizer } from './grid-item-resizer';
diff --git a/packages/block-editor/src/components/grid-visualizer/style.scss b/packages/block-editor/src/components/grid-visualizer/style.scss
new file mode 100644
index 0000000000000..45140e59c7af9
--- /dev/null
+++ b/packages/block-editor/src/components/grid-visualizer/style.scss
@@ -0,0 +1,33 @@
+// TODO: Specificity hacks to get rid of all these darn !importants.
+
+.block-editor-grid-visualizer {
+ z-index: z-index(".block-editor-grid-visualizer") !important;
+}
+
+.block-editor-grid-visualizer .components-popover__content * {
+ pointer-events: none !important;
+}
+
+.block-editor-grid-visualizer__grid {
+ display: grid;
+}
+
+.block-editor-grid-visualizer__item {
+ border: $border-width dashed $gray-300;
+}
+
+.block-editor-grid-item-resizer {
+ z-index: z-index(".block-editor-grid-visualizer") !important;
+}
+
+.block-editor-grid-item-resizer .components-popover__content * {
+ pointer-events: none !important;
+}
+
+.block-editor-grid-item-resizer__box {
+ border: $border-width solid var(--wp-admin-theme-color);
+
+ .components-resizable-box__handle {
+ pointer-events: all !important;
+ }
+}
diff --git a/packages/block-editor/src/components/grid-visualizer/utils.js b/packages/block-editor/src/components/grid-visualizer/utils.js
new file mode 100644
index 0000000000000..a100e596a4e24
--- /dev/null
+++ b/packages/block-editor/src/components/grid-visualizer/utils.js
@@ -0,0 +1,5 @@
+export function getComputedCSS( element, property ) {
+ return element.ownerDocument.defaultView
+ .getComputedStyle( element )
+ .getPropertyValue( property );
+}
diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js
index e6227ea2b03e2..36efe3dcf409b 100644
--- a/packages/block-editor/src/hooks/index.js
+++ b/packages/block-editor/src/hooks/index.js
@@ -42,6 +42,7 @@ createBlockEditFilter(
contentLockUI,
blockHooks,
blockRenaming,
+ childLayout,
].filter( Boolean )
);
createBlockListBlockFilter( [
diff --git a/packages/block-editor/src/hooks/layout-child.js b/packages/block-editor/src/hooks/layout-child.js
index a9a6ff4db0f4a..d8333e8e0e830 100644
--- a/packages/block-editor/src/hooks/layout-child.js
+++ b/packages/block-editor/src/hooks/layout-child.js
@@ -10,6 +10,7 @@ import { useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '../store';
import { useStyleOverride } from './utils';
import { useLayout } from '../components/block-list/layout';
+import { GridVisualizer, GridItemResizer } from '../components/grid-visualizer';
function useBlockPropsChildLayoutStyles( { style } ) {
const shouldRenderChildLayoutStyles = useSelect( ( select ) => {
@@ -96,8 +97,45 @@ function useBlockPropsChildLayoutStyles( { style } ) {
return { className: `wp-container-content-${ id }` };
}
+function ChildLayoutControlsPure( { clientId, style, setAttributes } ) {
+ const parentLayout = useLayout() || {};
+ const rootClientId = useSelect(
+ ( select ) => {
+ return select( blockEditorStore ).getBlockRootClientId( clientId );
+ },
+ [ clientId ]
+ );
+ if ( parentLayout.type !== 'grid' ) {
+ return null;
+ }
+ if ( ! window.__experimentalEnableGridInteractivity ) {
+ return null;
+ }
+ return (
+ <>
+
+ {
+ setAttributes( {
+ style: {
+ ...style,
+ layout: {
+ ...style?.layout,
+ columnSpan,
+ rowSpan,
+ },
+ },
+ } );
+ } }
+ />
+ >
+ );
+}
+
export default {
useBlockProps: useBlockPropsChildLayoutStyles,
+ edit: ChildLayoutControlsPure,
attributeKeys: [ 'style' ],
hasSupport() {
return true;
diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js
index a83d07398d54a..76a5557850a60 100644
--- a/packages/block-editor/src/hooks/layout.js
+++ b/packages/block-editor/src/hooks/layout.js
@@ -135,7 +135,12 @@ export function useLayoutStyles( blockAttributes = {}, blockName, selector ) {
return css;
}
-function LayoutPanelPure( { layout, setAttributes, name: blockName } ) {
+function LayoutPanelPure( {
+ layout,
+ setAttributes,
+ name: blockName,
+ clientId,
+} ) {
const settings = useBlockSettings( blockName );
// Block settings come from theme.json under settings.[blockName].
const { layout: layoutSettings } = settings;
@@ -266,6 +271,8 @@ function LayoutPanelPure( { layout, setAttributes, name: blockName } ) {
layout={ usedLayout }
onChange={ onChangeLayout }
layoutBlockSupport={ blockSupportAndThemeSettings }
+ name={ blockName }
+ clientId={ clientId }
/>
) }
{ constrainedType && displayControlsForLegacyLayouts && (
@@ -273,6 +280,8 @@ function LayoutPanelPure( { layout, setAttributes, name: blockName } ) {
layout={ usedLayout }
onChange={ onChangeLayout }
layoutBlockSupport={ blockSupportAndThemeSettings }
+ name={ blockName }
+ clientId={ clientId }
/>
) }
@@ -282,6 +291,8 @@ function LayoutPanelPure( { layout, setAttributes, name: blockName } ) {
layout={ usedLayout }
onChange={ onChangeLayout }
layoutBlockSupport={ layoutBlockSupport }
+ name={ blockName }
+ clientId={ clientId }
/>
) }
>
diff --git a/packages/block-editor/src/layouts/grid.js b/packages/block-editor/src/layouts/grid.js
index a27d07b3854a2..0dc72694bd568 100644
--- a/packages/block-editor/src/layouts/grid.js
+++ b/packages/block-editor/src/layouts/grid.js
@@ -23,6 +23,7 @@ import { appendSelectors, getBlockGapCSS } from './utils';
import { getGapCSSValue } from '../hooks/gap';
import { shouldSkipSerialization } from '../hooks/utils';
import { LAYOUT_DEFINITIONS } from './definitions';
+import { GridVisualizer } from '../components/grid-visualizer';
const RANGE_CONTROL_MAX_VALUES = {
px: 600,
@@ -67,6 +68,7 @@ export default {
inspectorControls: function GridLayoutInspectorControls( {
layout = {},
onChange,
+ clientId,
} ) {
return (
<>
@@ -85,10 +87,13 @@ export default {
onChange={ onChange }
/>
) }
+ { window.__experimentalEnableGridInteractivity && (
+
+ ) }
>
);
},
- toolBarControls: function DefaultLayoutToolbarControls() {
+ toolBarControls: function GridLayoutToolbarControls() {
return null;
},
getLayoutStyle: function getLayoutStyle( {
diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss
index 01931adace3f1..d7aa3ebcc12d0 100644
--- a/packages/block-editor/src/style.scss
+++ b/packages/block-editor/src/style.scss
@@ -27,6 +27,7 @@
@import "./components/duotone-control/style.scss";
@import "./components/font-appearance-control/style.scss";
@import "./components/global-styles/style.scss";
+@import "./components/grid-visualizer/style.scss";
@import "./components/height-control/style.scss";
@import "./components/image-size-control/style.scss";
@import "./components/inserter-list-item/style.scss";