Repository navigation
Grid: Drag and resize blocks freely in manual grids, Shift to snap - #85210
draganescu wants to merge 46 commits into
Conversation
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>
🤖 PR meta 🤖🏷️ LabelsThis pull request needs exactly one label indicating its type, and has 0.
Read more about Type labels in Gutenberg. If you cannot add labels yourself, a reviewer can do it for you. 📦 Bundle sizeSize Change: +3.1 kB (+0.03%) Total Size: 9.09 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. Backspace in
|
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 😅 |
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>
|
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.
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".
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
…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>
…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>
…ment Turning stacking on also removes a block's mobile free-placement box.
…ment # Conflicts: # packages/block-editor/CHANGELOG.md
…ment # Conflicts: # packages/block-editor/CHANGELOG.md
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>
What?
Behind the
gutenberg-grid-interactivityexperiment, 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.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.
columnStart,columnSpan,rowStartandrowSpaninstyle.layout, and for a free block these are the smallest cells that contain it. A free block also getsstyle.layout.box:left,top,widthandheightas 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.--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 withaspect-ratio: auto, and stacked grids already set that variable toauto, 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-sizingis 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 inlib/compat/wordpress-7.2/kses.phphere 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.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.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.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.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, sinceaspect-ratiosizes 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
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
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