Repository navigation
Grid: Draw blocks across cells in manual placement grids - #84503
Draft
draganescu wants to merge 70 commits into
Draft
draganescu wants to merge 70 commits into
draganescu wants to merge 70 commits into
Conversation
Pressing on an empty cell of a manual grid and dragging selects a rectangle of cells. On release, the inserter opens, and the block inserted covers the selected cells. A press without a drag still opens the inserter for the one cell, and so does the keyboard. Inserted blocks get their position and span in the selected style state, like blocks moved by dragging, and keep any style the inserter gave them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # packages/block-editor/src/components/grid/test/utils.test.js
A block inserted into a manual grid always gets a placement in the default style state: the cells drawn, fitted to the grid's own columns and rows. When a viewport is selected, the drawn cells are also its placement there. Before, a block drawn on Tablet or Mobile only had a placement for that viewport, so the grid's layout sync put it in a cell of its own choosing in the default grid, and could add rows. Spans of one cell are written as no span, and placements the block had in other viewports are removed, so an inserted pattern's root block covers the cells drawn. The placement is worked out by a new pure helper, getInsertedChildLayoutStyle, which is unit tested. Drawing ends when the pointer loses its capture, and Escape cancels it without opening the inserter. The flag that swallows the click after a drag is cleared after the current task, so it can't swallow a later keyboard activation. Touches don't draw, so the page still scrolls under a finger, and a tap opens the inserter for one cell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🤖 PR meta 🤖📦 Bundle sizeSize Change: +4.67 kB (+0.05%) Total Size: 9.08 MB 📦 View Changed
⚡ PerformanceShow the resultsClient side metrics exclude the server response time. front-end-block-theme
front-end-classic-theme
media-processing
media-upload
post-editor
site-editor
🏁 Flaky testsSome tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information. Custom Completer: should allow newlines after multiple option completions in
|
# Conflicts: # packages/block-editor/CHANGELOG.md
draganescu
added this pull request to stack #84511
October 10, 2026 17:34
This was referenced Oct 10, 2026
# Conflicts: # packages/block-editor/CHANGELOG.md
Drawing a block across the cells of a manual placement grid now draws the lines of the cells that blocks are in, as dragging a block does, until the block is inserted or the inserter closes. The marquee's state moves up to the manual grid visualizer, which draws the cells. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # packages/block-editor/src/components/grid/grid-visualizer.jsx
# Conflicts: # packages/block-editor/src/components/grid/grid-visualizer.jsx
The cells being drawn moved up to the manual grid visualizer so that it could draw the lines over blocks while a block was drawn. The lines are drawn over every cell again, so the state goes back to the marquee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pressing on an empty cell and dragging across a manual placement grid selects a rectangle of cells, but the grid only drew its cells while a block was dragged into it. The cells are now drawn, the same way, while a selection is shown, including while its inserter is open, so the cells being selected can be seen against the rest of the grid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An empty cell now draws its rounded square while it is hovered or focused, or its inserter is open, so it is clear which cell a click inserts a block into. The square is also the appender's focus ring, and the appender drops its own square border, line and tint in these cells. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a manual grid with many columns, on a tablet or mobile canvas, a cell can be a few dozen pixels across, too small for a block like Image to show its placeholder. A block inserted into one cell, or into a drawn range of cells, now gets more of the free cells around them until it is at least 160px across and down, the size at which `Placeholder` switches to its medium layout. It grows right and down first, then left and up, and never past the grid's tracks or into cells other blocks cover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moving a block placed by the grid layout sync into reading order moved the focused block's element, so the selection went back to the block it was added after, and typing went there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Converts getInsertedChildLayoutStyle and getMinimumInsertionRect, with their unit tests, to TypeScript so that the files this PR adds satisfy the gutenberg/use-typescript rule. getInsertedBlockRect now imports the GridTrack type from get-minimum-insertion-rect instead of redeclaring it. No runtime changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # packages/block-editor/src/components/grid/grid-visualizer.tsx # packages/block-editor/src/components/grid/use-grid-layout-sync.ts # packages/block-editor/src/components/grid/utils.ts # packages/block-editor/src/components/inserter-draggable-blocks/index.tsx # packages/block-editor/src/hooks/layout-child.tsx # packages/block-editor/src/hooks/layout.tsx
…iment - Drop the global blocks.switchToBlockType.transformedBlock filter, which changed saved content for transforms in every layout; it is proposed on its own in #85045. Blocks that replace the selected item of a manual grid still take its place through the layout sync. - Mark blocks picked in an inserter with a private key in the insert action's meta instead of the public meta.isFromInserter. - Without the gutenberg-grid-interactivity experiment, useGridLayoutSync places blocks as trunk does (first free cell after the selected block) and selects only the selected block's layout. With it, it only selects the style of this grid's selected item, so styling other blocks doesn't re-render every grid. - Inserter drags only show and feed the grid tools of manual grids with the experiment on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
draganescu
removed this pull request from stack #84511
October 22, 2026 14:01
draganescu
added this pull request to stack #85397
October 22, 2026 14:02
# Conflicts: # packages/block-editor/src/components/grid/grid-visualizer.tsx
Blocks dragged from an inserter were kept in a `draggedInserterBlocks` store state, so that manual grids could take them, which stayed behind if the inserter closed during the drag. Trunk's inserter already adds a `wp-block:<name>` data type for each dragged block, which can be read while dragging. Manual grids now read the names from the drag events, with `useInserterDragBlockNames()`, to know whether they can take the blocks, and read the blocks from the drop's data. `startDragging()` takes no blocks again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # packages/block-editor/src/hooks/layout-child.tsx
Every manual grid child injected the same placeholder rule, so a grid with 100 children had 100 copies. The grid's layout style now has one rule for all of its children. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The layout sync of every grid selected the whole style of its selected item, so any style change of that item, such as dragging a color or spacing slider, re-rendered the grid. It now selects only the item's default and viewport layouts, the same objects until a layout changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
25 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
Behind the
gutenberg-grid-interactivityexperiment, pressing on an empty cell of a manual placement grid with a mouse or pen and dragging selects a rectangle of cells. On release, the inserter opens, and the inserted block covers the selected cells. Part of the stack split out of #84340, on top of #84502.Why?
Until now a block inserted into a manual grid always took one cell and had to be resized afterwards. Drawing the area first lets you place and size a block in one step, which is how layout tools usually work.
How?
GridVisualizerMarqueein the grid visualizer listens for pointer events on the visualizer's cells. A press on an empty cell's appender starts a drag, and pointer capture keeps the drag going across the canvas iframe. Each move works out the cell under the pointer withgetGridDropTarget, and the newgetBoundingGridRecthelper gives the rectangle from the start cell to that cell.GridVisualizerAppendercovering the rectangle and opens its inserter. The click that follows the release is swallowed, so the start cell's own inserter doesn't open too.touch-action: noneon the cells, because the cells cover the whole grid and that would stop the page from scrolling under a finger.pointermovethat presses the main button (button0,buttons1), not as apointerdown: WebKit acts as if the button were still held from the drag. The Pointer Events spec reports a button pressed while another is down that way (a chorded button press), so such a move starts drawing too. Without it, drawing only started on the second press.GridVisualizerAppendergainscolumnSpanandrowSpan. The inserted block's placement is worked out by a new pure helper,getInsertedChildLayoutStyle:getMinimumInsertionRect()). A block inserted into a single cell always grows to that size, to the right and down, then left and up at the grid's edges, but never past half the grid's columns or rows (getMaximumInsertionSpan()), so a block in a 2 by 2 grid stays one cell. A drawn rectangle is the size that was asked for: a Paragraph drawn across three cells of a row stays three cells wide and one row tall. Once the block renders, it only grows if it shows a placeholder (a.components-placeholderelement) and the rectangle is narrower or shorter than the minimum.columnCountandrowCount. Without it, the grid's layout sync would put the block in a cell of its own choosing in the default grid, and could add rows.styleattribute with{ layout: { columnStart, rowStart } }. A pattern inserted into a cell lost its root block's styles, such as colors and spacing. The rest of the block's style is now kept.Blocks inserted without a cell
Blocks can also arrive in a manual grid without a cell of their own: from the plus next to the parent selector in a grid item's toolbar, the block inserter sidebar, Add before or Add after, Enter, paste, or a block moved in from elsewhere. The grid's layout sync (
useGridLayoutSync) used to put each of them in the first free cell after the selected block, one cell big, which looked random. Now:/headingin one, takes that block's place and size.How it works:
getInsertedBlockRect()andgetInsertionSize()inget-inserted-block-rect.ts, with unit tests.useInsertionPointnow marks the insertion'smetawith a private key (isFromInserterKeyinstore/private-keys.ts, a symbol, so it doesn't become a publicmetacontract), and the block editor store keeps it with the last inserted blocks. A new private selector,wasBlockJustInsertedFromInserter(), reads it. It is a separate key, notmeta.source, because the Search block checkswasBlockJustInserted()without a source.startDragging()action takes the blocks dragged from an inserter, kept in a newdraggedInserterBlocksstate and read with the privategetDraggedInserterBlocks()selector, so grids know what is being dragged before it's dropped.components/grid/child-layout.ts(copyChildLayout()), used by the layout sync. This PR doesn't add ablocks.switchToBlockType.transformedBlockfilter: a global one would change saved content for transforms in every layout type, and it's proposed on its own in Block Editor: Keep a block's grid position and span when it is transformed #85045. Within a manual grid, a transform replaces the selected block, so the sync gives the result its place.useGridLayoutSyncplaces blocks as on trunk (the first free cell after the selected block, one cell big) and selects only the selected block's default layout, and blocks dragged from an inserter don't show any grid's tools. With the experiment, the sync only selects the style of this grid's selected item, so styling a block elsewhere doesn't re-render every grid.Testing Instructions
Enable the "Grid interactivity" experiment in Gutenberg > Experiments.
Add a Grid block, and in its settings switch to manual placement with a few columns and rows.
Select the grid. Press on an empty cell and drag across several cells. A highlighted rectangle follows the pointer.
Release. The inserter opens over the rectangle. Pick a block; it covers the selected cells.
Repeat and press Escape or click elsewhere instead of picking a block. The rectangle goes away and nothing is inserted.
Click an empty cell without dragging. The inserter opens for that cell. The inserted block grows from that cell to about 300 by 300 pixels, so that a placeholder fits.
Draw three cells of one row and insert a Paragraph: it stays three cells wide and one row tall. Draw the same and insert an Image: it grows to fit its placeholder.
Start drawing a rectangle and press Escape before releasing. The rectangle goes away, and releasing doesn't open an inserter.
Switch the style state to Tablet, with the grid set to more columns on Tablet than on Desktop if you like. Draw a rectangle and insert a block. It covers the drawn cells on Tablet. Switch back to Desktop: the block has a placement there too, the same cells fitted to the Desktop grid, and the grid gains no rows.
Insert a pattern whose root block has a background color into an empty cell. The inserted block keeps its background color.
On a touch device, or with touch emulation, swipe across the grid. The page scrolls and no rectangle is drawn. Tapping an empty cell opens its inserter.
Select a block in a manual grid and use the plus next to the parent selector in its toolbar to insert an Image. It goes in the middle of the grid, about 300 by 300 pixels. Do it again: the second one is a cell down and to the right of the first.
Put the cursor at the end of a paragraph in the grid and press Enter, or use Add after from the block's options. The new paragraph is next to it, at the minimum size, and keeps the cursor: typing goes into it.
In that empty paragraph, type
/headingand press Enter. The heading takes the paragraph's place and size.Open the block inserter sidebar and drag an Image onto the grid. A drop indicator follows the pointer, and the image lands where it's dropped.
In a grid with two columns and two rows, insert a block from the plus: it takes one cell.
Testing Instructions for Keyboard
Screenshots or screencast
To add.
Use of AI Tools
Split out of #84340 with Claude Code (Claude Opus 5.5), which ported the code and tests from that branch. The split and its decisions were made in conversation with the PR author.
🤖 Generated with Claude Code