Repository navigation
Grid: Rotate manual grid items from the canvas - #84512
Draft
draganescu wants to merge 164 commits into
Draft
draganescu wants to merge 164 commits into
draganescu wants to merge 164 commits into
Conversation
Every block that doesn't opt out with `supports.rotate: false` can now be rotated, behind the new "Block rotation" experiment (`gutenberg-block-rotation`, which sets `window.__experimentalEnableBlockRotation`). The Dimensions panel gets an optional Rotation item with an angle picker. The rotation is a number of degrees in (-180, 180] stored in `style.rotate`. It follows the selected viewport style state: overrides live in `style['@tablet'].rotate` and `style['@mobile'].rotate`, where 0 is stored and undoes the default rotation on that viewport. In the default state, 0 is not stored. "Reset all" only clears the selected state's rotation. Blocks are rotated with the CSS `rotate` property rather than `transform`, which the editor writes to animate block moves. In the editor, the rules are added under a generated class, with viewport overrides in the responsive media queries. On the front end, `lib/block-supports/rotate.php` adds the same rules to the block supports stylesheet under a class generated from them, treating the stored value as untrusted. Saved markup doesn't change, and stored rotation is shown whether or not the experiment is on. `getRotateForState()` and `getUpdatedRotateStyle()` read and write the rotation of a style state, for tools such as a canvas rotate handle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Classic, Custom HTML, Missing, More, Page Break, Pattern, Shortcode and synced patterns opt out with `supports.rotate: false`, like they opt out of custom CSS and visibility: they have no wrapper of their own to rotate, or lose it on save or on the front end. A block whose own `style` attribute is not an object doesn't support rotation, and style values that are not objects are read as no rotation. JS reads stored values like PHP: only decimal numeric strings, and halves rounded away from zero after removing floating point noise, so -12.345 is -12.35 on both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # packages/block-editor/CHANGELOG.md # phpunit/block-supports/layout-test.php
Behind the grid interactivity and block rotation experiments, a selected block in a manual placement grid gets a rotate handle below it. The handle reads and writes the rotate support's style.rotate for the selected style state. - Dragging the handle previews the angle with an inline rotate style on the block, which wins over the rotate support's class rules, and shows the angle. Releasing writes it once, as one undo step; Escape cancels. Rotation snaps to multiples of 15 degrees unless Shift is held. - The rotation goes through useUpdateGridChildLayout, so rotating a block of a grid stacked on mobile first gives the grid its own mobile layout. Blocks rotated on mobile get a mobile rotation of 0 then, since stacked blocks are shown unrotated. - The resize handles follow the block's rotation, in any grid. Dragged blocks land by the cells they take up, measured without rotation. - The mobile stacking rule unrotates stacked blocks, with more specificity than the rotate support's rules, and lets images keep their own size instead of covering cells. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PHPCompatibility flags the '0x10' test value, which checks that hexadecimal strings are rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Rotated overlays track the block's size with a ResizeObserver, only while it is rotated, instead of reading it during render. The rotate and resize handles no longer drift after an image loads or the grid changes width. - Escape is handled in the capture phase in both the canvas and the editor document, and claimed, so writing flow doesn't also move focus away from the block. - Like the Rotation control, the handle is only offered for blocks in the default editing mode. - Component tests for the handle (one write on release, Escape cancels, unmounting mid-drag clears the preview) and for when it is offered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # packages/block-editor/CHANGELOG.md
# Conflicts: # packages/block-editor/CHANGELOG.md # packages/block-editor/src/components/grid/mobile-stacking.js
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🤖 PR meta 🤖📦 Bundle sizeSize Change: +3.77 kB (+0.04%) Total Size: 8.6 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. Page List converts to Navigation Links with entity bindings in
|
1 of 3 tasks
PHP 7 rejects numeric strings with trailing whitespace, while PHP 8 accepts them, so the PHP 7.4 test run failed on ' 15 '. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o add/grid-rotate-handle # Conflicts: # packages/block-editor/CHANGELOG.md # phpunit/block-supports/layout-test.php
# Conflicts: # lib/block-supports/layout.php # packages/block-editor/CHANGELOG.md # packages/block-editor/src/hooks/test/layout.jsdom.test.js # packages/block-editor/src/layouts/grid.jsx # packages/block-editor/src/layouts/test/grid.jsdom.test.jsx # phpunit/block-supports/layout-test.php
useIsInFixedGridCell also returns false on mobile when the grid stacks its blocks, since stacked blocks are sized by their content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # packages/block-editor/CHANGELOG.md # packages/block-library/CHANGELOG.md
# Conflicts: # packages/block-editor/CHANGELOG.md # packages/block-library/CHANGELOG.md
# Conflicts: # packages/block-library/CHANGELOG.md
# Conflicts: # packages/block-library/CHANGELOG.md
# Conflicts: # packages/block-editor/src/components/grid/get-grid-drop-target.js # packages/block-editor/src/components/grid/grid-visualizer.jsx
Remove core's wp_render_rotate_support filter when it exists, as the custom CSS and block visibility supports do, so a WordPress version that ships rotation doesn't output the rules twice. The angle picker now rounds to two decimals, the same as stored rotations, through a shared roundAngle helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`getRotateValue`, `getRotateForState` and `getUpdatedRotateStyle` read and write `style.rotate` without any React or UI code, but lived in the rotate hook module, which imports the inspector controls. Grid code that only needs the data helpers had to import that module. They now live in `utils/rotation.ts` with the other angle helpers. The rotation utils test runs in jsdom, since the block style state helpers need it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The block style state helpers only needed `hooks/utils` for `cleanEmptyObject`, and that module imports the components and the store. Through them, `utils/rotation.ts` couldn't load outside a browser environment. `cleanEmptyObject` is a plain object helper, so it now lives in `utils/object.ts`, and `hooks/utils` re-exports it for its existing importers. The rotation utils test runs in node again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
add/block-rotate-support was rebased onto trunk, so its rotate commits came back with new hashes. This branch already had the same changes from the earlier merges, so the merge keeps this branch's tree and applies only what is new on #84505: the rotation support is now opt-in (`supports.rotation`), no longer behind the block rotation experiment, renamed to hooks/rotation.tsx and lib/block-supports/rotation.php, and shown in the Position panel. The grid rotate handle follows: it checks `hasRotateSupport()` instead of `isRotateEnabled()`, and its tests opt their block in instead of turning the experiment on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nsform block support
Only the blocks from the original rotate PR opt in: Group, Heading, Image and Video. The :hover rotation tests now expect Button's root selector, and the e2e spec drops the Button hover test.
theme.json and Global Styles rotations rendered on the front end but not in the post or site editor canvas, because STYLE_KEYS left out transform. Also lists transform in the Global Styles change summary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fractional angle, such as 1rad shown as 57.3, failed the angle picker's step and was rewritten as soon as the field lost focus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PHP's $ matched before a final newline, so "45deg\n" passed in PHP but not in JS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Elements already forbid transform, but their :hover and other states referenced the complete style properties, which now include it. The PHP side strips it, so the schema now rejects it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o add/grid-rotate-handle # Conflicts: # docs/reference-guides/core-blocks/README.md # lib/load.php
The rotate handle now reads and writes the transform block support's `style.transform.rotate`, a CSS angle, instead of the rotation block support from #84505, which was closed in favour of #86240. The old support, its Rotation control in the Position panel and its PHP are removed; the Transform panel in the Styles tab is the control now. The stacking rule of grids stacked on mobile unrotates blocks with `rotate: 0deg !important`: the transform support rotates blocks inline and its viewport rules are important, and the KSES filter only allows a single angle. The handle puts the block's own inline `rotate` back after a preview, since the support sets it inline too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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-interactivityandgutenberg-block-rotationexperiments, a selected block in a manual placement grid has a rotate handle outside its bottom right corner on the canvas. Part of the stack split out of #84340, on top of #84509. It also merges #84505 (the rotate block support) and #84504 (blocks fill fixed-size cells in manual grids), so from this branch up, the stack has the complete feature set of #84340. #84894 builds on it.The stack:
Why?
#84505 adds rotation as a general block support with a Rotation control in the block settings. In a manual grid, where blocks are placed and sized directly on the canvas, rotating them should work the same way, without going to the sidebar.
How?
GridItemRotator(components/grid/grid-item-rotator.tsx) is a handle outside the block's bottom right corner, clear of the resize handles in the middle of each side, in a popover over the block.GridItemHandle(components/grid/grid-item-handle.tsx), which looks like a block toolbar with a single button: the toolbar's border and corners, a 32px square and a 24px icon. Its colors and corners come from the design system tokens (--wpds-*). No stroke token is as dark as the toolbar's border, so the border stays$gray-900. The handle turns against the block's rotation, so it stays upright like the toolbar. Grid: Show block toolbars over manual grids and add a drag handle #84894's drag handle shares it.rotatestyle on the block element, and shows the angle in the handle in place of its icon. The rotate support's rules have no!important, so the inline preview wins over them. Nothing is written to the block while dragging.getPointerAngle()and theoffsetofgetRotationFromPointer()). The handle is at a corner, so an angle taken straight from the pointer's direction would make the block jump as soon as the handle is pressed.startRotation(), frompointerdownor from apointermovethat presses the main button (button0,buttons1). In WebKit, the first press after a drag that started outside the canvas comes that way, as if the button were still held from the drag, and the block only turned on a second press.preventDefault(), so writing flow doesn't also move focus away from the block.utils/rotation.ts, next to the support's own angle helpers (components/grid/rotation.tswas merged into it).isRotateEnabled( blockName )is true, which means the block rotation experiment is on and the block supports rotation. Like the support's control, it is only offered for blocks in the default editing mode, and not in a pseudo state such as:hover.getRotateForState()and writes it withgetUpdatedRotateStyle(), both inutils/rotation.ts(Add a rotation block support #84505 moved them out of the rotate hook, so grid data code such asgetGridChildUpdatesdoesn't import the hook and its controls), so it storesstyle.rotate, orstyle['@tablet'].rotate/style['@mobile'].rotatein a viewport state, exactly as the Rotation control does. The write goes throughuseUpdateGridChildLayoutas a style function, so rotating a block of a grid stacked on mobile first gives the grid its own mobile layout, in the same undo step.getRotatedOverlayStyle()anduseRotatedOverlayStyle()move a block popover from the corner of the rotated block's bounding box to its center and rotate it by the same angle, so the resize handles stay on the block's edges, in any grid, and the rotate handle stays at the block's corner. While a block is rotated, its unrotated size is tracked with aResizeObserver, so the overlays follow it when an image loads or the grid changes width.getGridItemPixelRect()measures blocks by their offsets, which leave out therotateproperty, so dragged blocks land by the cells they take up, and alignment guides compare unrotated grid areas.rotate: noneto stacked children, in the editor (getMobileStackingStyle) and on the front end (layout.php). Its repeated container class (.c.c > *) is more specific than the support's single.wp-rotate-…class, inside and outside the media query.getUnstackedMobileUpdates), children that are rotated on mobile getstyle['@mobile'].rotate = 0throughgetUpdatedRotateStyle(), so nothing turns when the stack goes away.useUpdateGridChildLayout, so it turns stacking off for the grid first, with the same snackbar. Before, edits there had no visible effect.GridToolsonly keeps whether the previewed angle rotates the block, which hides the resizer. The angle itself stays in the handle, soGridToolsdoesn't re-render for every degree.GridVisualizeris also memoized: from Grid: Show block toolbars over manual grids and add a drag handle #84894 up, the drag handle and the handles toolbar turn with the preview, so the angle is back inGridToolsthere, and the memo keeps the visualizer's work per cell and grid item out of each degree.Merge notes: #84505 merged cleanly. After #84504 moved filling into block styles, merging it and #84509 again replaced the image-specific stacking rules from this branch with
--wp--style--grid-cells: auto, keepingrotate: none. #84504'suseIsInFixedGridCellalso returns false on mobile here when the grid stacks its blocks (isGridStackedOnMobile), so the size controls it hides come back in the stack. Merging #84504 conflicted inpackages/block-editor/CHANGELOG.md(both entries kept) andphpunit/block-supports/layout-test.php(both sets of tests kept). Merging #84509's final version conflicted in the changelog and in the JSDoc ofgetUnstackedMobileUpdates, which now describes both skipping blocks hidden on mobile and unrotating rotated ones. The merge that brought in #84500's measuring once per drag (2840864) also moved this PR's rotation into it, because the two conflicted:getGridItemMeasurements()now reads each block's rotation when the drag starts, with its position, instead of the drop layer reading every block's rotation on everydragover. It has its own unit test. The grid item handles also use the visualizer's z-index again (7d5b2df), since the visualizer no longer remounts during drags.Core backport: the stacking rule's
rotate: noneonly reaches the front end in core ifrotateis in core'ssafe_style_css, orsafecss_filter_attr()drops it. #84505's core PR WordPress/wordpress-develop#14707 already addsrotateto that list, with a test thatrotate: noneis kept, so this PR's backport needs nothing more as long as #14707 lands first.Differences from #84340:
style.rotate, behind its own experiment, with the Rotation control in the Dimensions panel), not a grid child layout value (style.layout.rotate) with a control in the grid item settings.--wp--style--grid-cells: auto, which turns off the fill rules from Blocks: Fill fixed-size cells in manual placement grids #84504. Grid: Canvas exploration #84340 reset image sizes in the stacking rule instead.centerin guide names, are kept.Still to do:
Testing Instructions
"style":{"rotate":30}(or the angle you chose), not alayout.rotatevalue. The block settings' Rotation control in the Dimensions panel shows the same angle.Testing Instructions for Keyboard
The rotate handle only works with a pointer and is hidden from assistive technology. Keyboard users rotate blocks with the Rotation control from #84505: open the Dimensions panel's options menu, turn on Rotation, and type an angle. In the Mobile preview of a grid stacked on mobile, the control shows 0, and typing an angle turns stacking off for the grid, as the handle does.
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