Skip to content

Commit bedba56

Browse files
draganescuclaude
andcommitted
Grid: Route the manual grid drag through the layout type
The block list's drag handler checked for a manual placement grid itself. Layout types can now take over the drag of their blocks with `startChildDrag()`, which the grid layout implements with `startGridItemDrag()` for manual grids with the experiment on, and the handler only asks the parent's layout type. A multi-selection drag still follows the pointer in every layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5d81c33 commit bedba56

6 files changed

Lines changed: 314 additions & 77 deletions

File tree

‎packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.ts‎

Lines changed: 26 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useSelect, useDispatch } from '@wordpress/data';
55
import { useRefEffect } from '@wordpress/compose';
66
import { store as blockEditorStore } from '../../../store';
77
import { unlock } from '../../../lock-unlock';
8+
import { getLayoutType } from '../../../layouts';
89

910
// The distance of the dragged blocks from the pointer.
1011
const PILE_OFFSET = 8;
@@ -333,6 +334,31 @@ export function useEventHandlers( {
333334
}
334335
return;
335336
}
337+
338+
// A layout can take over the drag of its blocks, such as a
339+
// manual placement grid, which shows the cells the block will
340+
// land in. A multi-selection drag follows the pointer in every
341+
// layout.
342+
const rootClientId = getBlockRootClientId( clientId );
343+
const rootLayout = getBlockAttributes( rootClientId )?.layout;
344+
if (
345+
! isMultiSelectionDrag &&
346+
getLayoutType( rootLayout?.type )?.startChildDrag?.(
347+
event,
348+
{
349+
layout: rootLayout,
350+
clientId,
351+
// A block that can be dragged is in a block list.
352+
rootClientId: rootClientId!,
353+
blockElement: node,
354+
startDraggingBlocks,
355+
stopDraggingBlocks,
356+
}
357+
)
358+
) {
359+
return;
360+
}
361+
336362
const clientIds = isMultiSelectionDrag
337363
? getSelectedBlockClientIds()
338364
: [ clientId ];
@@ -367,83 +393,6 @@ export function useEventHandlers( {
367393
ownerDocument.body.appendChild( dragElement );
368394
dataTransfer.setDragImage( dragElement, 0, 0 );
369395

370-
// In a manual placement grid, the grid shows the cells the
371-
// block will land in, so the block stays in place, dimmed,
372-
// instead of following the pointer. A multi-selection drag
373-
// can't be placed in cells, so it follows the pointer.
374-
const rootLayout = getBlockAttributes(
375-
getBlockRootClientId( clientId )
376-
)?.layout;
377-
if (
378-
! isMultiSelectionDrag &&
379-
rootLayout?.type === 'grid' &&
380-
rootLayout.isManualPlacement &&
381-
window.__experimentalEnableGridInteractivity
382-
) {
383-
const originalOpacity = node.style.opacity;
384-
node.style.opacity = '0.5';
385-
386-
// The grid's drop layer is in the editor's document,
387-
// outside the canvas, and a drop there can re-render the
388-
// block, after which its `dragend` no longer reaches the
389-
// canvas. The drag ends on whichever comes first.
390-
const endTargets = new Set( [
391-
ownerDocument,
392-
document,
393-
node,
394-
] );
395-
396-
// The grid's drop layer covers the block once dragging
397-
// starts. Chrome cancels a drag when that happens during
398-
// `dragstart`, so it waits for the first `dragover`.
399-
function startGridDrag() {
400-
ownerDocument.removeEventListener(
401-
'dragover',
402-
startGridDrag
403-
);
404-
startDraggingBlocks( [ clientId ] );
405-
document.body.classList.add(
406-
'is-dragging-components-draggable'
407-
);
408-
ownerDocument.documentElement.classList.add(
409-
'is-dragging'
410-
);
411-
}
412-
413-
function endGridDrag() {
414-
ownerDocument.removeEventListener(
415-
'dragover',
416-
startGridDrag
417-
);
418-
for ( const endTarget of endTargets ) {
419-
endTarget.removeEventListener(
420-
'dragend',
421-
endGridDrag
422-
);
423-
endTarget.removeEventListener(
424-
'drop',
425-
endGridDrag
426-
);
427-
}
428-
node.style.opacity = originalOpacity;
429-
dragElement.remove();
430-
stopDraggingBlocks();
431-
document.body.classList.remove(
432-
'is-dragging-components-draggable'
433-
);
434-
ownerDocument.documentElement.classList.remove(
435-
'is-dragging'
436-
);
437-
}
438-
439-
for ( const endTarget of endTargets ) {
440-
endTarget.addEventListener( 'dragend', endGridDrag );
441-
endTarget.addEventListener( 'drop', endGridDrag );
442-
}
443-
ownerDocument.addEventListener( 'dragover', startGridDrag );
444-
return;
445-
}
446-
447396
const rect = node.getBoundingClientRect();
448397

449398
// Remove the id and leave it on a shallow clone so that drop
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
interface GridItemDragOptions {
2+
/** Client ID of the dragged block. */
3+
clientId: string;
4+
/** The block's element in the canvas. */
5+
blockElement: HTMLElement;
6+
/** Marks blocks as being dragged. */
7+
startDraggingBlocks: ( clientIds: string[] ) => void;
8+
/** Marks the drag as over. */
9+
stopDraggingBlocks: () => void;
10+
}
11+
12+
/**
13+
* Starts dragging a block of a manual placement grid by its body. The grid
14+
* shows the cells the block will land in, so the block stays in place, dimmed,
15+
* instead of following the pointer, and the browser's drag image is a hidden
16+
* pixel.
17+
*
18+
* @param event The `dragstart` event.
19+
* @param options Options.
20+
* @param options.clientId Client ID of the dragged block.
21+
* @param options.rootClientId Client ID of the grid.
22+
* @param options.blockElement The block's element in the canvas.
23+
* @param options.startDraggingBlocks Marks blocks as being dragged.
24+
* @param options.stopDraggingBlocks Marks the drag as over.
25+
*/
26+
export function startGridItemDrag(
27+
event: DragEvent,
28+
{
29+
clientId,
30+
rootClientId,
31+
blockElement,
32+
startDraggingBlocks,
33+
stopDraggingBlocks,
34+
}: GridItemDragOptions & {
35+
/** Client ID of the grid. */
36+
rootClientId: string;
37+
}
38+
) {
39+
// A `dragstart` event always has drag data.
40+
const dataTransfer = event.dataTransfer!;
41+
42+
dataTransfer.effectAllowed = 'move'; // remove "+" cursor
43+
dataTransfer.clearData();
44+
dataTransfer.setData(
45+
'wp-blocks',
46+
JSON.stringify( {
47+
type: 'block',
48+
srcClientIds: [ clientId ],
49+
srcRootClientId: rootClientId,
50+
} )
51+
);
52+
53+
const { ownerDocument } = blockElement;
54+
ownerDocument.defaultView!.getSelection()!.removeAllRanges();
55+
56+
// Chrome shows a globe icon if the drag image has no dimensions.
57+
const dragElement = ownerDocument.createElement( 'div' );
58+
dragElement.style.width = '1px';
59+
dragElement.style.height = '1px';
60+
dragElement.style.position = 'fixed';
61+
dragElement.style.visibility = 'hidden';
62+
ownerDocument.body.appendChild( dragElement );
63+
dataTransfer.setDragImage( dragElement, 0, 0 );
64+
65+
const originalOpacity = blockElement.style.opacity;
66+
blockElement.style.opacity = '0.5';
67+
68+
// The grid's drop layer is in the editor's document, outside the canvas,
69+
// and a drop there can re-render the block, after which its `dragend` no
70+
// longer reaches the canvas. The drag ends on whichever comes first.
71+
const endTargets = new Set< EventTarget >( [
72+
ownerDocument,
73+
document,
74+
blockElement,
75+
] );
76+
77+
// The grid's drop layer covers the block once dragging starts. Chrome
78+
// cancels a drag when that happens during `dragstart`, so it waits for
79+
// the first `dragover`.
80+
function startGridDrag() {
81+
ownerDocument.removeEventListener( 'dragover', startGridDrag );
82+
startDraggingBlocks( [ clientId ] );
83+
document.body.classList.add( 'is-dragging-components-draggable' );
84+
ownerDocument.documentElement.classList.add( 'is-dragging' );
85+
}
86+
87+
function endGridDrag() {
88+
ownerDocument.removeEventListener( 'dragover', startGridDrag );
89+
for ( const endTarget of endTargets ) {
90+
endTarget.removeEventListener( 'dragend', endGridDrag );
91+
endTarget.removeEventListener( 'drop', endGridDrag );
92+
}
93+
blockElement.style.opacity = originalOpacity;
94+
dragElement.remove();
95+
stopDraggingBlocks();
96+
document.body.classList.remove( 'is-dragging-components-draggable' );
97+
ownerDocument.documentElement.classList.remove( 'is-dragging' );
98+
}
99+
100+
for ( const endTarget of endTargets ) {
101+
endTarget.addEventListener( 'dragend', endGridDrag );
102+
endTarget.addEventListener( 'drop', endGridDrag );
103+
}
104+
ownerDocument.addEventListener( 'dragover', startGridDrag );
105+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { startGridItemDrag } from '../start-grid-item-drag';
3+
4+
function setUp() {
5+
const blockElement = document.createElement( 'div' );
6+
document.body.append( blockElement );
7+
const dataTransfer = {
8+
clearData: vi.fn(),
9+
setData: vi.fn(),
10+
setDragImage: vi.fn(),
11+
};
12+
const event = new window.Event( 'dragstart', {
13+
bubbles: true,
14+
} ) as Event & { dataTransfer: typeof dataTransfer };
15+
event.dataTransfer = dataTransfer;
16+
Object.defineProperty( event, 'target', { value: blockElement } );
17+
const startDraggingBlocks = vi.fn();
18+
const stopDraggingBlocks = vi.fn();
19+
// @ts-expect-error The test passes a plain event with a partial data transfer.
20+
startGridItemDrag( event, {
21+
clientId: 'block',
22+
rootClientId: 'grid',
23+
blockElement,
24+
startDraggingBlocks,
25+
stopDraggingBlocks,
26+
} );
27+
return {
28+
blockElement,
29+
dataTransfer,
30+
startDraggingBlocks,
31+
stopDraggingBlocks,
32+
};
33+
}
34+
35+
describe( 'startGridItemDrag', () => {
36+
it( 'sets the dragged block as the drag data', () => {
37+
const { dataTransfer } = setUp();
38+
expect( dataTransfer.setData ).toHaveBeenCalledWith(
39+
'wp-blocks',
40+
JSON.stringify( {
41+
type: 'block',
42+
srcClientIds: [ 'block' ],
43+
srcRootClientId: 'grid',
44+
} )
45+
);
46+
expect( dataTransfer.setDragImage ).toHaveBeenCalled();
47+
} );
48+
49+
it( 'dims the block and starts dragging on the first dragover', () => {
50+
const { blockElement, startDraggingBlocks } = setUp();
51+
expect( blockElement ).toHaveStyle( { opacity: '0.5' } );
52+
expect( startDraggingBlocks ).not.toHaveBeenCalled();
53+
document.dispatchEvent( new window.Event( 'dragover' ) );
54+
document.dispatchEvent( new window.Event( 'dragover' ) );
55+
expect( startDraggingBlocks ).toHaveBeenCalledTimes( 1 );
56+
expect( startDraggingBlocks ).toHaveBeenCalledWith( [ 'block' ] );
57+
} );
58+
59+
it( 'stops dragging once on dragend', () => {
60+
const { blockElement, stopDraggingBlocks } = setUp();
61+
document.dispatchEvent( new window.Event( 'dragover' ) );
62+
blockElement.dispatchEvent(
63+
new window.Event( 'dragend', { bubbles: true } )
64+
);
65+
expect( stopDraggingBlocks ).toHaveBeenCalledTimes( 1 );
66+
expect( blockElement ).not.toHaveStyle( { opacity: '0.5' } );
67+
document.dispatchEvent( new window.Event( 'drop' ) );
68+
expect( stopDraggingBlocks ).toHaveBeenCalledTimes( 1 );
69+
} );
70+
} );

‎packages/block-editor/src/layouts/grid.tsx‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import { getGapCSSValue, getGapBoxControlValueFromStyle } from '../hooks/gap';
1818
import { getSpacingPresetCssVar } from '../components/spacing-sizes-control/utils';
1919
import { cleanEmptyObject, shouldSkipSerialization } from '../hooks/utils';
2020
import { LAYOUT_DEFINITIONS } from './definitions';
21+
import { startGridItemDrag } from '../components/grid/start-grid-item-drag';
2122
import type {
23+
ChildDragOptions,
2224
Layout,
2325
LayoutAlignment,
2426
LayoutControlProps,
@@ -433,6 +435,35 @@ export default {
433435
getAlignments(): LayoutAlignment[] {
434436
return [];
435437
},
438+
/**
439+
* A manual grid shows the cells a dragged block will land in, so the block
440+
* stays in place instead of following the pointer, as part of the grid
441+
* interactivity experiment.
442+
*
443+
* @param event The `dragstart` event.
444+
* @param options Options.
445+
* @param options.layout The grid's layout.
446+
* @param options.clientId Client ID of the dragged block.
447+
* @param options.rootClientId Client ID of the grid.
448+
* @param options.blockElement The dragged block's element in the canvas.
449+
* @param options.startDraggingBlocks Marks blocks as being dragged.
450+
* @param options.stopDraggingBlocks Marks the drag as over.
451+
*
452+
* @return Whether the grid took over the drag.
453+
*/
454+
startChildDrag(
455+
event: DragEvent,
456+
{ layout, ...options }: ChildDragOptions & { layout?: Layout }
457+
): boolean {
458+
if (
459+
! layout?.isManualPlacement ||
460+
! window.__experimentalEnableGridInteractivity
461+
) {
462+
return false;
463+
}
464+
startGridItemDrag( event, options );
465+
return true;
466+
},
436467
};
437468

438469
// Enables setting minimum width of grid items.

0 commit comments

Comments
 (0)