Skip to content

Grid: Drag and resize blocks freely in manual grids, Shift to snap - #85210

Draft
draganescu wants to merge 46 commits into
add/grid-item-handles-toolbarfrom
add/grid-free-placement
Draft

draganescu wants to merge 46 commits into
add/grid-item-handles-toolbarfrom
add/grid-free-placement

Conversation

@draganescu

@draganescu draganescu commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

What?

Behind the gutenberg-grid-interactivity experiment, blocks in a manual placement grid are dragged and resized freely: they land exactly where they're let go, at exactly the size they were dragged to. Holding Shift snaps to the grid lines. Part of the grid stack, on top of #84947.

  • Dragging a block drops it at the point it's let go, kept inside the grid. Holding Shift while dragging snaps its top-left corner to the closest grid lines and keeps its size.
  • Resizing a block keeps the size it was resized to. Holding Shift when the resize stops snaps the edge being dragged to the closest line. Resizing past the last row still adds rows.
  • Changing the number of columns no longer moves or resizes anything. Cells stay close to square, so the grid gets as many rows as keep it about as tall.
  • The cell movers and the Column and Row fields move a free block by whole cells and keep its shape. Typing a span snaps it to the cells.
  • Touch drags have no Shift, so they're always free.

This is the reverse of the rotate handle (#84512), which snaps to 15° and goes free with Shift.

Why?

Snapping every move to whole cells makes the canvas feel like a spreadsheet. Laying out a section visually means putting things where you want them, and the grid lines are most useful as a ruler you snap to when you choose to. Adding columns should give finer lines to snap to, not rearrange the layout you built.

How?

The grid still does the layout. No block is positioned outside CSS grid, and the front end runs no JavaScript for it.

  • The data. A block keeps columnStart, columnSpan, rowStart and rowSpan in style.layout, and for a free block these are the smallest cells that contain it. A free block also gets style.layout.box: left, top, width and height as fractions of those cells. The offsets are fractions of the cells' width, which is what CSS resolves margin percentages against, and the height is a fraction of their height. Everything that reads cells today (mobile stacking, block order, overlaps, the inspector) keeps working.
  • The CSS. The box is published as four custom properties on the block (--wp--grid-item--box-left, -top, -width, -height) and applied as margins and a size by one rule inside @container style(--wp--style--grid-cells: fixed). Grids whose rows are sized by their content, viewports with aspect-ratio: auto, and stacked grids already set that variable to auto, so there the box switches off and the block fills its cells. The margins are !important, because a block's own margin is an inline style. A block with a box in any state sets all four properties in its base rule, so a grid nested in a free block doesn't inherit them. The editor (hooks/layout-child.jsx) and the front end (lib/block-supports/layout.php) output the same CSS, and both ignore a box whose values aren't numbers in range. box-sizing is added to the safe CSS properties, since the style engine would drop it otherwise. That change is split out as Layout: Keep box-sizing for fixed-width children of flex layouts #85373 (with its Core backport, KSES: Allow box-sizing in safecss_filter_attr() wordpress-develop#14708), because trunk already needs it. Temporary copy: gutenberg_add_box_sizing_to_safe_style_css() stays in lib/compat/wordpress-7.2/kses.php here until Layout: Keep box-sizing for fixed-width children of flex layouts #85373 lands. When trunk is next merged into the stack after that, this copy conflicts with Layout: Keep box-sizing for fixed-width children of flex layouts #85373's: drop this one and keep trunk's.
  • Viewports. The box belongs to the placement: a tablet or mobile override that sets any of the placement keys has its own box, or none. An override without placement keys inherits both.
  • The math (components/grid/free-placement.ts, pure) turns a rectangle in pixels into cells plus a box and back, keeps a rectangle inside the grid (rotated blocks by their outline, and their stored shape too), and snaps a corner or edges to the lines. Edges within a pixel of a line count as on it, since the canvas measures blocks in whole pixels.
  • Drop (get-grid-drop-target.js, grid-visualizer.jsx). Where the grid's cells have a fixed size, getFreeGridDropTarget() replaces the cell-snapped target, and the drop indicator shows exactly where the block lands. Shift is read from the drag events.
  • Resize (grid-item-resizer.jsx). The resizer already resized freely and snapped when it stopped. Where cells have a fixed size, it now keeps the size, and snaps only the dragged edge when Shift is held.
  • Column count (remap-grid-placements.ts, use-change-grid-column-count.ts). Changing the columns from the block settings measures the grid, works out each block's place in pixels with the old cells and back with the new ones, and sets the row count, in one change with the grid's layout. It accounts for the grid's padding and borders, since aspect-ratio sizes a Group's border box. From the default state, blocks with their own tablet or mobile places are remapped too, in viewports that use the default counts.

Testing Instructions

  1. In Gutenberg > Experiments, enable "Grid interactivity". Insert a Canvas (or a Grid in manual placement mode) and add a few blocks.
  2. Drag a block and let go between cells. It lands exactly there, and the drop indicator follows it.
  3. Start dragging again and hold Shift. The block's top-left corner snaps to the grid lines, and its size stays the same.
  4. Resize a block by one edge. It keeps the exact size. Resize again and press Shift before letting go: only that edge snaps, and the selection doesn't change.
  5. In the block settings, add and remove columns. Nothing moves or changes size, and rows are added or removed to keep the grid about as tall.
  6. Move a free block with the toolbar's movers, or the Column field. It moves by a cell and keeps its shape. Type a span: it snaps to the cells.
  7. Turn on "Stack on mobile" and preview Mobile: blocks stack full width, as before.
  8. View the post. Free blocks are where they are in the editor.
  9. Without the experiment, blocks snap to cells as before.

Checked by hand on a live site (Firefox). New unit tests cover the placement math, free drop targets, resize results, the column count remap (including a padded grid), the box CSS in the editor and its parity checks, the movers and the inspector. PHPUnit tests cover the front-end CSS and the box checks.

Testing Instructions for Keyboard

The movers and the Column and Row fields keep a free block's shape (step 6). Dragging, resizing and Shift-snapping are pointer interactions, as before.

Known issues

  • A block's size can drift by up to half a pixel over repeated drags, since it's measured in whole pixels.
  • Resizing a rotated block keeps its unrotated shape inside the grid, not its rotated outline. A rotated block dragged in from another container is kept inside as if it weren't rotated.
  • The Columns field changes the layout, and adds an undo step, on every keystroke.

Use of AI Tools

Designed with Andrei in conversation, with a throwaway prototype, then written, tested and reviewed with Claude Code. Andrei reviewed the behaviour on a live site.

🤖 Generated with Claude Code

Andrei Draganescu and others added 10 commits October 20, 2026 14:03
Gets a block's place in a manual grid from its rectangle in pixels: the
smallest range of cells that contains it and, when it doesn't fill them,
its box inside them, as fractions of the cells' area. Also converts back,
keeps a rectangle inside the grid by its rotated outline, and snaps a
rectangle's corner or edges to the grid lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A block placed freely in a manual grid keeps the cells that contain it,
and a box inside them. The box is published as custom properties, in the
base state and in each viewport that sets its own placement, and applied
as margins and a size inside a container style query on
`--wp--style--grid-cells: fixed`, so it only applies where the cells
have a fixed size. Grids whose rows are sized by their content, and
stacked grids, leave blocks to fill their cells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirrors the editor: a block with a box in any state publishes its box as
custom properties, in the base rule and in each viewport that sets its
own placement, and applies it as margins and a size inside a container
style query on `--wp--style--grid-cells: fixed`. Box values that aren't
numbers in range are ignored. `box-sizing` is added to the safe CSS
properties, since the style engine would drop it otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where a manual grid's cells have a fixed size, a dragged block lands
exactly where it's let go, kept inside the grid by its rotated outline,
and the drop indicator shows exactly where. Holding Shift snaps its
top-left corner to the grid lines and keeps its size. Touch drags have no
Shift, so they are always free. The drop writes the cells that contain
the block and its box inside them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where a manual grid's cells have a fixed size, a resized block keeps the
size it was resized to, inside the grid's left, top and right edges.
Resizing past the last row still grows the grid. Holding Shift when the
resize stops snaps the edge being dragged to the closest grid line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cell movers and the Column and Row inputs move a block placed freely
by whole cells and keep its box, so it keeps its exact shape, including in
a viewport that sets its own place. Typing a span, or resetting the
placement, snaps the block to its cells and removes its box. The movers
get the box of the selected viewport, which a viewport that sets its own
place doesn't inherit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nges

Changing the number of columns of a manual grid whose cells have a fixed
size changes the size of its cells. Every block now stays where it is on
screen, at the same size: it gets the cells that contain it and its box
inside them, and the grid gets as many rows as keep it about as tall, or
more if a block needs them. Editing the default state also updates the
blocks' own places in viewports that use its counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Treat edges within a pixel of a grid line as on it, and keep a box inside
  its cells, so that blocks the canvas measures in whole pixels don't get
  boxes that start before their cells or reach into an extra cell.
- Keep a rotated block's place inside the grid without its rotation too,
  since that is what is stored.
- Check boxes the same way in the editor and on the front end, so that the
  editor never shows a box the front end ignores.
- Account for the grid's padding and borders when the column count changes,
  since its aspect ratio sizes its border box.
- Make the box's margins win over a block's own inline margin.
- In a viewport state, keep the box of the block's default place when its
  column or row is edited from the block settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the [Package] Block editor /packages/block-editor label Oct 20, 2026
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

Preview in WordPress Playground

🏷️ Labels

This pull request needs exactly one label indicating its type, and has 0.

  • Required: any label starting with [Type].
  • Found: none.

Read more about Type labels in Gutenberg. If you cannot add labels yourself, a reviewer can do it for you.

📦 Bundle size

Size Change: +3.1 kB (+0.03%)

Total Size: 9.09 MB

📦 View Changed
Filename Size Change
build/scripts/block-editor/index.min.js 623 kB +3.1 kB (+0.5%)

8e80005 Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 74c3a4a trunk % Change
timeToFirstByte 54.45 ms +8.08% -2.57% 54.15 ms +9.05% -3.51% 0.55%
largestContentfulPaint 94 ms +4.26% -6.38% 92 ms +6.52% -4.35% 2.17%
lcpMinusTtfb 36.35 ms +13.2% -5.5% 34.85 ms +15.93% -4.73% 4.3%
wpBeforeTemplate 28.83 ms +16.58% -1.01% 28.59 ms +7% -2.06% 0.84%
wpTemplate 21.04 ms +6.13% -1.57% 20.95 ms +6.01% -2.48% 0.43%
wpTotal 50.95 ms +8.58% -2.55% 50.17 ms +9.07% -3.03% 1.55%
wpMemoryUsage 7.68 MB +0% -0% 7.65 MB +0% -0% 0.44%
wpDbQueries 16 +0% -0% 16 +0% -0% 0%

front-end-classic-theme

Metric 74c3a4a trunk % Change
timeToFirstByte 45.5 ms +4.51% -1.54% 41.2 ms +6.43% -1.33% 10.44%
largestContentfulPaint 100 ms +4% -0% 88 ms +2.27% -0% 13.64%
lcpMinusTtfb 54.5 ms +1.74% -2.29% 46.9 ms +0.96% -1.6% 16.2%
wpBeforeTemplate 26.45 ms +1.63% -1.63% 26.43 ms +2.42% -1.51% 0.08%
wpTemplate 16.02 ms +1.06% -1.69% 12.12 ms +3.63% -1.16% 32.18%
wpTotal 42.34 ms +5.1% -0.97% 38.61 ms +6.19% -1.45% 9.66%
wpMemoryUsage 6.37 MB +0% -0% 6.23 MB +0% -0% 2.35%
wpDbQueries 13 +0% -0% 9 +0% -0% 44.44%

media-processing

Metric 74c3a4a trunk % Change
mediaProcessingJpeg 401.22 ms +0.8% -1% 399.31 ms +1.69% -1.47% 0.48%
mediaProcessingAvif 6070.64 ms +0.08% -0.2% 6084.57 ms +0.09% -0.15% -0.23%
mediaProcessingJpegToAvif 4207.1 ms +0.21% -0.37% 4218.79 ms +0.1% -0.4% -0.28%

media-upload

Metric 74c3a4a trunk % Change
jpegUploadProcessing 1431.79 ms +3.46% -0.64% 1442.14 ms +0.67% -1.27% -0.72%
pngUploadProcessing 172.44 ms +24.61% -2.45% 179.33 ms +21.5% -4.82% -3.84%
largeJpegUploadProcessing 1420.44 ms +2.28% -0.62% 1414.25 ms +1.01% -0.53% 0.44%
multipleImageUploadProcessing 1557.24 ms +0.27% -0.47% 1626.52 ms +28.95% -1.31% -4.26%

post-editor

Metric 74c3a4a trunk % Change
serverResponse 376.06 ms +2.54% -5.85% 367.13 ms +3.2% -10.03% 2.43%
firstPaint 422.18 ms +6.1% -23.2% 386.84 ms +13.2% -41.62% 9.14%
domContentLoaded 1020.67 ms +0.28% -2.02% 991.3 ms +1.13% -1.19% 2.96%
loaded 1021.9 ms +0.37% -2.05% 992.29 ms +1.14% -1.2% 2.98%
firstContentfulPaint 443.2 ms +2.41% -5.29% 435.68 ms +2.26% -2.22% 1.73%
firstBlock 2820.14 ms +1.72% -0.6% 2777.24 ms +1.05% -1.7% 1.54%
type 18.46 ms +5.04% -4.93% 17.96 ms +3.4% -5.9% 2.78%
typeWithoutInspector 18.19 ms +3.79% -8.91% 17.23 ms +6.04% -1.97% 5.57%
typeWithTopToolbar 22.62 ms +3.27% -2.34% 21.55 ms +4.36% -1.25% 4.97%
typeContainer 7.69 ms +3.77% -6.89% 7.53 ms +3.59% -4.38% 2.12%
focus 107.63 ms +7.14% -0.59% 97.43 ms +9.07% -9.55% 10.47%
firstFocus 198.21 ms +0% -0% 232.63 ms +0% -0% -14.8%
selectAll 453.39 ms +2.32% -3.73% 449.96 ms +0.27% -3.28% 0.76%
listViewOpen 64.66 ms +10.73% -6.73% 63.77 ms +7.79% -3.9% 1.4%
inserterOpen 36.74 ms +6.75% -5.8% 38.3 ms +4.73% -12.53% -4.07%
inserterHover 11.02 ms +14.61% -4.54% 11.22 ms +4.46% -7.66% -1.78%
inserterSearch 8.42 ms +3.33% -9.74% 7.15 ms +13.99% -6.57% 17.76%
loadPatterns 575.48 ms +3.12% -6.4% 526.75 ms +4.06% -3.21% 9.25%
wpTotal 366.7 ms +2.8% -5.81% 358.6 ms +3.33% -10.24% 2.26%
wpMemoryUsage 14.72 MB +0% -0% 14.68 MB +0% -0% 0.24%
wpDbQueries 55 +0% -0% 55 +0% -0% 0%

site-editor

Metric 74c3a4a trunk % Change
serverResponse 330.33 ms +10.36% -5.32% 333.08 ms +0.71% -6.33% -0.83%
firstPaint 221.79 ms +1.18% -15.91% 193.56 ms +22.69% -7.82% 14.58%
domContentLoaded 1017.94 ms +0.74% -1.82% 975.27 ms +0.96% -2.56% 4.38%
loaded 1019.24 ms +0.69% -1.83% 976.03 ms +0.96% -2.56% 4.43%
firstContentfulPaint 412.17 ms +3.37% -1.01% 399.62 ms +3.11% -4.28% 3.14%
firstBlock 3636.98 ms +0.86% -1.05% 3424.31 ms +1.49% -0.82% 6.21%
type 16.63 ms +7.46% -4.81% 15.11 ms +3.18% -2.38% 10.06%
navigate 95.94 ms +8.39% -7.44% 81.03 ms +13.14% -0.39% 18.4%
loadPatterns 1019.37 ms +2.58% -6.15% 1002.5 ms +3.74% -6.11% 1.68%
loadPages 961.39 ms +2.12% -3.24% 1017.04 ms +0.87% -2.31% -5.47%
wpTotal 322.22 ms +10.56% -5.37% 325.2 ms +0.75% -6.58% -0.92%
wpMemoryUsage 12.74 MB +0% -0% 12.71 MB +0% -0% 0.24%
wpDbQueries 44 +2.27% -0% 44 +2.27% -0% 0%

8e80005 Run

🏁 Flaky tests

Some 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.

Backspace in /test/e2e/specs/editor/blocks/list.spec.js, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  -  5
+ Received  + 11

@@ -1,25 +1,31 @@
  Array [
    Object {
      "innerBlocks": Array [
        Object {
          "attributes": Object {
-           "content": "1",
+           "content": "1‸a",
          },
-         "innerBlocks": Array [
+         "innerBlocks": Array [],
+         "name": "core/list-item",
+       },
        Object {
+         "attributes": Object {
+           "content": "2",
+         },
          "innerBlocks": Array [
            Object {
              "attributes": Object {
-                   "content": "a‸2",
+               "ordered": false,
+               "values": "",
              },
-                 "name": "core/list-item",
-               },
+             "innerBlocks": Array [
                Object {
                  "attributes": Object {
                    "content": "b",
                  },
+                 "innerBlocks": Array [],
                  "name": "core/list-item",
                },
              ],
              "name": "core/list",
            },

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/list.spec.js:2041:42
Custom Completer: should allow newlines after multiple option completions in /test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js, passed after 1 failed attempt.
Error: expect(received).toBe(expected) // Object.is equality

- Expected  - 1
+ Received  + 1

@@ -9,11 +9,11 @@
  <!-- wp:paragraph -->
  <p>🫐 test</p>
  <!-- /wp:paragraph -->

  <!-- wp:paragraph -->
- <p>🫐 test</p>
+ <p>~🍎 test</p>
  <!-- /wp:paragraph -->

  <!-- wp:paragraph -->
  <p></p>
  <!-- /wp:paragraph -->

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js:466:6
Custom Completer: should not insert disabled options in /test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js, passed after 1 failed attempt.
Error: expect(received).toBe(expected) // Object.is equality

- Expected  - 1
+ Received  + 1

  <!-- wp:paragraph -->
- <p>Sorry, we are all out of ~g grapes.</p>
+ <p>Sorry, we are all out of 🍎 grapes.</p>
  <!-- /wp:paragraph -->

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js:391:7
Regression: updating a specific option will only change its value and will not corrupt others in /test/e2e/specs/editor/various/core-settings.spec.js, passed after 1 failed attempt.
Error: expect(received).toStrictEqual(expected) // deep equality

- Expected  - 1
+ Received  + 1

  Array [
    "rewrite_rules",
-   "SERIALIZED DATA",
+   "",
  ]
    at forEach (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/core-settings.spec.js:46:26)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/core-settings.spec.js:43:35
Undo restores the post selection when another synced entity is loaded (RTC WebSocket) in /test/e2e/specs/editor/collaboration/collaboration-undo-redo.spec.ts, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 2
+ Received  + 2

  Object {
    "attributeKey": "content",
    "blockIndex": 0,
    "content": "abcdef",
-   "endOffset": 3,
-   "offset": 3,
+   "endOffset": 2,
+   "offset": 2,
  }

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/collaboration-undo-redo.spec.ts:363:5
Undo restores the post selection when another synced entity is loaded in /test/e2e/specs/editor/collaboration/collaboration-undo-redo.spec.ts, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 2
+ Received  + 2

  Object {
    "attributeKey": "content",
    "blockIndex": 0,
    "content": "abcdef",
-   "endOffset": 3,
-   "offset": 3,
+   "endOffset": 6,
+   "offset": 6,
  }

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/collaboration-undo-redo.spec.ts:363:5
User Mention: should allow newlines after multiple mention completions in /test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js, passed after 1 failed attempt.
Error: expect(received).toBe(expected) // Object.is equality

- Expected  - 1
+ Received  + 1

@@ -1,11 +1,11 @@
  <!-- wp:paragraph -->
  <p>@buddytheelf test</p>
  <!-- /wp:paragraph -->

  <!-- wp:paragraph -->
- <p>@buddytheelf test</p>
+ <p>@@thebetterhobbit test</p>
  <!-- /wp:paragraph -->

  <!-- wp:paragraph -->
  <p>@buddytheelf test</p>
  <!-- /wp:paragraph -->

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js:466:6
can be converted to a quote in /test/e2e/specs/editor/blocks/list.spec.js, passed after 1 failed attempt.
Error: expect(received).toBe(expected) // Object.is equality

- Expected  - 1
+ Received  + 1

@@ -1,9 +1,9 @@
  <!-- wp:quote -->
  <blockquote class="wp-block-quote"><!-- wp:list -->
  <ul class="wp-block-list"><!-- wp:list-item -->
- <li>one</li>
+ <li>ne</li>
  <!-- /wp:list-item -->

  <!-- wp:list-item -->
  <li>two</li>
  <!-- /wp:list-item --></ul>

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/list.spec.js:436:52
inserts a block dragged from the inserter where it is dropped in /test/e2e/specs/editor/various/grid-drag-and-drop.spec.js, passed after 1 failed attempt.
Error: expect(received).toBeLessThanOrEqual(expected)

Expected: <= 1
Received:    35.984375

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at GridDragUtils.expectToLandOnPreview (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/grid-drag-and-drop.spec.js:522:5)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/grid-drag-and-drop.spec.js:230:23
refuses the drop and uploads nothing in /test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js, passed after 1 failed attempt.
Error: expect(received).toHaveLength(expected)

Expected length: 0
Received length: 1
Received array:  [{"_links": {"about": [{"href": "http://localhost:8889/wp-json/wp/v2/types/attachment"}], "author": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/users/1"}], "collection": [{"href": "http://localhost:8889/wp-json/wp/v2/media"}], "curies": [{"href": "https://api.w.org/{rel}", "name": "wp", "templated": true}], "replies": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/comments?post=180"}], "self": [{"href": "http://localhost:8889/wp-json/wp/v2/media/180", "targetHints": {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE"]}}], "wp:attached-to": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/posts/179", "id": 179, "post_type": "post"}]}, "alt_text": "", "author": 1, "caption": {"rendered": ""}, "class_list": ["post-180", "attachment", "type-attachment", "status-inherit", "hentry", "entry"], "comment_status": "open", "date": "2026-10-23T22:06:17", "date_gmt": "2026-10-23T22:06:17", "description": {"rendered": "<p class=\"attachment\"><a href='http://localhost:8889/wp-content/uploads/2026/10/830498e6-f165-4b42-bd77-a4074b61cf58.png'><img loading=\"lazy\" decoding=\"async\" width=\"10\" height=\"10\" src=\"http://localhost:8889/wp-content/uploads/2026/10/830498e6-f165-4b42-bd77-a4074b61cf58.png\" class=\"attachment-medium size-medium\" alt=\"\" style=\"width:100%;height:100%;max-width:10px;\" /></a></p>
"}, "featured_media": 0, "filename": "830498e6-f165-4b42-bd77-a4074b61cf58.png", "filesize": 80, "guid": {"rendered": "http://localhost:8889/wp-content/uploads/2026/10/830498e6-f165-4b42-bd77-a4074b61cf58.png"}, "id": 180, "link": "http://localhost:8889/?attachment_id=180", "media_details": {"file": "2026/10/830498e6-f165-4b42-bd77-a4074b61cf58.png", "filesize": 80, "height": 10, "image_meta": {"alt": "", "aperture": "0", "camera": "", "caption": "", "copyright": "", "created_timestamp": "0", "credit": "", "focal_length": "0", "iso": "0", "keywords": [], "orientation": "0", "shutter_speed": "0", "title": ""}, "sizes": {}, "width": 10}, "media_type": "image", "meta": [], "mime_type": "image/png", "modified": "2026-10-23T22:06:17", "modified_gmt": "2026-10-23T22:06:17", "ping_status": "closed", "post": 179, "slug": "830498e6-f165-4b42-bd77-a4074b61cf58", "source_url": "http://localhost:8889/wp-content/uploads/2026/10/830498e6-f165-4b42-bd77-a4074b61cf58.png", "status": "inherit", "template": "", "title": {"rendered": "830498e6-f165-4b42-bd77-a4074b61cf58"}, "type": "attachment"}]
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js:71:45
should create paragraph on Enter in quote block in /test/e2e/specs/editor/blocks/list.spec.js, passed after 1 failed attempt.
TimeoutError: locator.click: Timeout 10000ms exceeded.
Call log:
  - waiting for getByRole('option', { name: 'List', exact: true })

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/list.spec.js:751:67
should cut and paste individual blocks with collapsed selection in /test/e2e/specs/editor/various/copy-cut-paste.spec.js, passed after 1 failed attempt.
Error: expect(string).toMatchSnapshot(expected) failed

  @@ -1,3 +1,3 @@
   <!-- wp:paragraph -->
  -<p>2Cut - collapsed selection</p>
  +<p>2</p>
   <!-- /wp:paragraph -->


    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/copy-cut-paste.spec.js:45:49
should gradually multi-select in /test/e2e/specs/editor/various/multi-block-selection.spec.js, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 1
+ Received  + 1

  Array [
    Object {
-     "name": "core/column",
+     "name": "core/paragraph",
    },
  ]

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/multi-block-selection.spec.js:1145:5
should keep a full selection when outdenting nested items with Shift+Tab in /test/e2e/specs/editor/blocks/list.spec.js, passed after 1 failed attempt.
TimeoutError: locator.click: Timeout 10000ms exceeded.
Call log:
  - waiting for locator('role=toolbar[name="Block tools"i]').locator('role=button[name="Indent"]')

    at Editor.clickBlockToolbarButton (/home/runner/work/gutenberg/gutenberg/packages/e2e-test-utils-playwright/src/editor/click-block-toolbar-button.ts:17:15)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/list.spec.js:1166:3
should merge a following sibling list into the outermost list with Delete from a nested item (#77245) in /test/e2e/specs/editor/blocks/list.spec.js, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 2
+ Received  + 2

@@ -8,11 +8,11 @@
          "innerBlocks": Array [
            Object {
              "innerBlocks": Array [
                Object {
                  "attributes": Object {
-                   "content": "b‸",
+                   "content": "b",
                  },
                  "name": "core/list-item",
                },
              ],
              "name": "core/list",
@@ -25,11 +25,11 @@
    },
    Object {
      "innerBlocks": Array [
        Object {
          "attributes": Object {
-           "content": "1",
+           "content": "‸1",
          },
          "innerBlocks": Array [
            Object {
              "innerBlocks": Array [
                Object {

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/list.spec.js:2854:41
should replace selected non-breaking spaces from the toolbar in /test/e2e/specs/editor/various/rich-text.spec.js, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 1
+ Received  + 1

  Array [
    Object {
      "attributes": Object {
-       "content": "one two three",
+       "content": "e two three",
      },
      "name": "core/paragraph",
    },
  ]

Truncated. See the full report.

8e80005 Run

@draganescu
draganescu added this pull request to stack #84511 October 20, 2026 16:22
@tellthemachines

Copy link
Copy Markdown
Contributor

Snapping every move to whole cells makes the canvas feel like a spreadsheet. Laying out a section visually means putting things where you want them, and the grid lines are most useful as a ruler you snap to when you choose to.

I'd love to know more about where this assumption is coming from, because my approach to layouts is pretty much the opposite. Unless you're making a freeform drawing sort of thing, you usually want items in a layout to align with each other. It's fine to have a freeform option, but I'd make it activate on shift instead of being the default. Assuming most people are going to be using this for a specific goal, not just to make content for art class 😅

draganescu and others added 3 commits October 21, 2026 11:26
A block dragged from an inserter into a grid with fixed-size cells lands
exactly where the drop indicator shows it, like a block moved within the
grid, instead of in the cells around that place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@draganescu

Copy link
Copy Markdown
Contributor Author

Yes this is going to be one of the major points to figure out @tellthemachines and that is why I made this as a PR on top. To see how this works with snap as a default switch to the PR below this #84947.

you usually want items in a layout to align with each other

Absolutely. We do preserve even in this "free" version the align guides that appear to help things align to one another. The issue is "snap to grid".

Snapping every move to whole cells makes the canvas feel like a spreadsheet.
I'd love to know more about where this assumption is coming from

It's mostly about default size of the grid. If the col/row number is small the snapping makes placing extremely constrained. If col/row number is big then snapping becomes frankly annoying as you feel you fight the "pull" of the snap. That is why snap to grid makes sense only if the user has a plan about that grid before hand.

Watching users build with Gogh, Squarespace and Wix shows people mostly expect relative alignment - things to one another. One way to solve layout drift in a PR like this one is to include a cleanup tool - where tiny offsets from the underlying grid are cleaned up - but that, again, goes back to the problem of what is the grid size.

It's also worth asking if for manual grids we should just assume a grid/cell size and handle grid expansion behind the scenes without controls of rows/cols so that maybe we find the sweet spot for the rigt rows/cols number?

…ment

# Conflicts:
#	packages/block-editor/src/components/grid/get-grid-drop-target.js
#	tools/eslint/suppressions.json
draganescu and others added 3 commits October 22, 2026 10:52
…mitted

The Columns field remapped the blocks of a fixed-cell manual grid on every
keystroke, so typing 12 remapped 12, then 1, then 12, leaving the grid
about three times taller and adding an undo step for each digit. In
manual grids a typed count now applies on Enter or when leaving the field;
arrow keys and the spin buttons still step it right away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@draganescu
draganescu removed this pull request from stack #84511 October 22, 2026 14:01
@draganescu
draganescu added this pull request to stack #85397 October 22, 2026 14:02
draganescu and others added 13 commits October 22, 2026 19:33
…ag from the filter

The drop layer read whether the grid's cells have a fixed size on every
`dragover`; it is now part of the measurements taken when the drag starts.
The free placement box in the layout support reads the
`gutenberg_layout_has_grid_interactivity` filter, like the rest of the
support. `getGridItemBox()` moves to `components/grid/free-placement`, so
`components/grid` and the child layout control no longer import
`hooks/layout-child`, which imports them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers, in Chromium and WebKit, moving a block by its body, the block
toolbar's drag handle and the grid item drag handle, dragging a
multi-selection, and dragging blocks and files in from outside the grid.
In a grid with cells of a fixed size, a dropped block lands where the grid
showed it would.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@draganescu draganescu added the No Core Sync Required Indicates that any changes do not need to be synced to WordPress Core label Oct 23, 2026
draganescu and others added 4 commits October 24, 2026 00:46
A stored box was accepted with `left` up to 100 and with `left + width`
past its cells. In JS and PHP, `left` must now be at most 1 and the box
must end inside its cells, with a small tolerance for rounded values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… type switched

Resetting Columns and rows now goes through the same remap as a typed
column count. Switching between Auto and Manual keeps the current column
count, so a manual grid comes back with the count its blocks were placed
for, instead of a count saved when the control was shown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The drop target was state of the whole manual grid visualizer, and a
block placed freely lands somewhere new on almost every dragover, so every
cell re-rendered on almost every event. The drop layer now owns the drop
target, follows the pointer once per animation frame (the first move of a
drag, which finds the grab point, and the drop are handled at once), and
moves the landing preview without rendering while its cells, overlaps and
guides stay the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

No Core Sync Required Indicates that any changes do not need to be synced to WordPress Core [Package] Block editor /packages/block-editor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants