Skip to content

Grid: Rotate manual grid items from the canvas - #84512

Draft
draganescu wants to merge 164 commits into
add/grid-mobile-stackingfrom
add/grid-rotate-handle
Draft

draganescu wants to merge 164 commits into
add/grid-mobile-stackingfrom
add/grid-rotate-handle

Conversation

@draganescu

@draganescu draganescu commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

What?

Behind the gutenberg-grid-interactivity and gutenberg-block-rotation experiments, 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:

  1. Grid: Give manual placement grids same-size cells #84499 Same-size cells for manual placement grids
  2. Grid: Snap drags to cells in manual placement grids #84500 Drag blocks in manual grids with cell snapping
  3. Grid: Show alignment guides while dragging in manual grids #84501 Alignment guides while dragging
  4. Grid: Add and remove rows of manual placement grids #84502 Grid row handles, and resizing past the last row
  5. Grid: Draw blocks across cells in manual placement grids #84503 Draw to insert
  6. Grid: Stack manual grids on mobile, and edit them from the stack #84509 Stack manual grids on mobile
  7. Grid: Rotate manual grid items from the canvas #84512 Rotate manual grid items from the canvas (this PR), merging Add a rotation block support #84505 and Blocks: Fill fixed-size cells in manual placement grids #84504
  8. Grid: Show block toolbars over manual grids and add a drag handle #84894 Block toolbars over manual grids, and a drag handle on the canvas
  9. Grid: Show the canvas handles in a small toolbar, and add a Canvas block #84947 The canvas handles in a small toolbar, with a delete button

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.
    • The handle is a 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.
    • Dragging the handle previews the angle with an inline rotate style 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.
    • The block turns by as much as the pointer turns around the block's center, starting from the block's current angle (getPointerAngle() and the offset of getRotationFromPointer()). 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.
    • A press starts a rotation in startRotation(), from pointerdown or from a pointermove that presses the main button (button 0, buttons 1). 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.
    • Releasing the pointer writes the angle once, so a rotation is one undo step. Escape cancels it, and the inline style is cleared on release, cancel, or if the handle goes away mid-drag. Escape is handled in the capture phase in both the canvas and the editor document and claimed with preventDefault(), so writing flow doesn't also move focus away from the block.
    • The angle snaps to multiples of 15° within 5°, holding Shift rotates freely in whole degrees, and angles within 2° of 0 count as no rotation. The maths is in utils/rotation.ts, next to the support's own angle helpers (components/grid/rotation.ts was merged into it).
  • The handle shows only when the block is in a manual grid with the grid interactivity experiment on and 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.
  • The handle reads the rotation with the support's getRotateForState() and writes it with getUpdatedRotateStyle(), both in utils/rotation.ts (Add a rotation block support #84505 moved them out of the rotate hook, so grid data code such as getGridChildUpdates doesn't import the hook and its controls), so it stores style.rotate, or style['@tablet'].rotate / style['@mobile'].rotate in a viewport state, exactly as the Rotation control does. The write goes through useUpdateGridChildLayout as 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.
  • Overlays follow the rotation. getRotatedOverlayStyle() and useRotatedOverlayStyle() 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 a ResizeObserver, so the overlays follow it when an image loads or the grid changes width.
  • Grid maths ignores rotation. getGridItemPixelRect() measures blocks by their offsets, which leave out the rotate property, so dragged blocks land by the cells they take up, and alignment guides compare unrotated grid areas.
  • Mobile stacking:
    • The stacking rule adds rotate: none to 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.
    • When a stacked grid gets its own mobile layout (getUnstackedMobileUpdates), children that are rotated on mobile get style['@mobile'].rotate = 0 through getUpdatedRotateStyle(), so nothing turns when the stack goes away.
    • The Rotation control in the block settings works the same way in the Mobile state of a stacked grid: it shows 0, as the stack shows the block unrotated, and an edit goes through useUpdateGridChildLayout, so it turns stacking off for the grid first, with the same snackbar. Before, edits there had no visible effect.
  • While the handle is dragged, GridTools only keeps whether the previewed angle rotates the block, which hides the resizer. The angle itself stays in the handle, so GridTools doesn't re-render for every degree. GridVisualizer is 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 in GridTools there, and the memo keeps the visualizer's work per cell and grid item out of each degree.
  • Rotation is no longer tied to the grid, so switching a grid out of manual placement, or moving a block out of the grid, keeps its rotation.

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, keeping rotate: none. #84504's useIsInFixedGridCell also 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 in packages/block-editor/CHANGELOG.md (both entries kept) and phpunit/block-supports/layout-test.php (both sets of tests kept). Merging #84509's final version conflicted in the changelog and in the JSDoc of getUnstackedMobileUpdates, 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 every dragover. 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: none only reaches the front end in core if rotate is in core's safe_style_css, or safecss_filter_attr() drops it. #84505's core PR WordPress/wordpress-develop#14707 already adds rotate to that list, with a test that rotate: none is kept, so this PR's backport needs nothing more as long as #14707 lands first.

Differences from #84340:

Still to do:

  • Resizing a rotated block works along the screen's axes rather than the block's own: the handles sit on the rotated edges, but dragging them changes the block's span in screen directions. This was on Grid: Canvas exploration #84340's to-do list too.

Testing Instructions

  1. In Gutenberg > Experiments, enable "Grid interactivity" and "Block rotation".
  2. Add a Grid block, set "Grid item position" to Manual, and add a few blocks, including an Image.
  3. Select a block. A handle that looks like a one-button toolbar appears outside its bottom right corner. Drag it around the block: the block turns, the handle stays upright, and it shows the angle. It snaps to 15° steps; hold Shift to rotate freely.
  4. Release. Undo once and check the block goes back to its previous angle in one step.
  5. Start another rotation and press Escape before releasing. The block goes back to its angle and nothing is stored.
  6. With a rotated block selected, check the resize handles sit on its edges, and resizing still snaps to cells.
  7. Drag a rotated block by its toolbar drag handle to another cell. It lands by the cells it takes up, not by its rotated bounding box.
  8. Open the Code editor and check the block has "style":{"rotate":30} (or the angle you chose), not a layout.rotate value. The block settings' Rotation control in the Dimensions panel shows the same angle.
  9. Save and view the front end: the block is rotated. At 480px wide or less, the grid stacks, the block is unrotated, and images have their natural height.
  10. In the editor, switch the preview to Mobile and rotate a stacked block. The grid gets its own mobile layout, the other blocks stay unrotated, and one undo brings the stack back.
  11. Switch the grid back to Auto placement. Rotated blocks keep their rotation, and there is no rotate handle.
  12. Disable "Block rotation". The handle is gone, but rotated blocks still show rotated. Disable "Grid interactivity" too, and check the grid's CSS matches trunk.

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

draganescu and others added 13 commits October 10, 2026 11:35
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>
@github-actions github-actions Bot added [Package] Block library /packages/block-library [Package] Block editor /packages/block-editor labels Oct 10, 2026
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

📦 Bundle size

Size Change: +3.77 kB (+0.04%)

Total Size: 8.6 MB

📦 View Changed
Filename Size Change
build/modules/lazy-editor/index.min.js 14.4 kB +13 B (+0.09%)
build/scripts/block-editor/index.min.js 555 kB +2.73 kB (+0.49%)
build/scripts/block-library/index.min.js 415 kB +30 B (+0.01%)
build/scripts/blocks/index.min.js 44.9 kB +18 B (+0.04%)
build/scripts/core-data/index.min.js 76.7 kB +16 B (+0.02%)
build/scripts/edit-site/index.min.js 376 kB +30 B (+0.01%)
build/scripts/editor/index.min.js 559 kB +76 B (+0.01%)
build/scripts/style-engine/index.min.js 2.34 kB +95 B (+4.23%)
build/styles/block-editor/style-rtl.css 21 kB +169 B (+0.81%)
build/styles/block-editor/style-rtl.min.css 18.1 kB +177 B (+0.99%)
build/styles/block-editor/style.css 21.1 kB +169 B (+0.81%)
build/styles/block-editor/style.min.css 18 kB +176 B (+0.99%)
build/styles/block-library/image/style-rtl.css 3.12 kB +9 B (+0.29%)
build/styles/block-library/image/style-rtl.min.css 2.05 kB +10 B (+0.49%)
build/styles/block-library/image/style.css 3.13 kB +9 B (+0.29%)
build/styles/block-library/image/style.min.css 2.04 kB +10 B (+0.49%)
build/styles/block-library/style-rtl.css 23.2 kB +11 B (+0.05%)
build/styles/block-library/style-rtl.min.css 19.6 kB +7 B (+0.04%)
build/styles/block-library/style.css 23.3 kB +11 B (+0.05%)
build/styles/block-library/style.min.css 19.6 kB +8 B (+0.04%)

3ce4afd Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 9492791 trunk % Change
timeToFirstByte 56.25 ms +6.67% -3.82% 55.1 ms +6.53% -2.27% 2.09%
largestContentfulPaint 90 ms +11.11% -2.22% 88 ms +9.09% -0% 2.27%
lcpMinusTtfb 34.05 ms +15.27% -5.58% 34.15 ms +12.3% -3.66% -0.29%
wpBeforeTemplate 29.26 ms +7.72% -3.55% 29 ms +2.48% -2.55% 0.9%
wpTemplate 23.01 ms +4.56% -5.52% 22.48 ms +2.36% -4.09% 2.36%
wpTotal 52.48 ms +6.5% -3.81% 51.15 ms +7.12% -1.92% 2.6%
wpMemoryUsage 7.71 MB +0% -0% 7.67 MB +0% -0% 0.49%
wpDbQueries 16 +0% -0% 16 +0% -0% 0%

front-end-classic-theme

Metric 9492791 trunk % Change
timeToFirstByte 42 ms +10.71% -1.79% 47.2 ms +5.19% -2.54% -11.02%
largestContentfulPaint 88 ms +6.82% -0% 100 ms +2% -0% -12%
lcpMinusTtfb 46.75 ms +1.39% -1.07% 53.35 ms +1.87% -1.78% -12.37%
wpBeforeTemplate 26.72 ms +12.76% -1.61% 27.05 ms +8.58% -2.29% -1.22%
wpTemplate 12.47 ms +8.1% -4.49% 16.61 ms +8.31% -3.55% -24.92%
wpTotal 39.14 ms +10.02% -1.71% 43.9 ms +6.22% -1.87% -10.84%
wpMemoryUsage 6.29 MB +0% -0% 6.36 MB +0% -0% -1.16%
wpDbQueries 9 +0% -0% 13 +0% -0% -30.77%

media-processing

Metric 9492791 trunk % Change
mediaProcessingJpeg 404.49 ms +4.79% -0.09% 402.44 ms +1% -1% 0.51%
mediaProcessingAvif 6178.78 ms +0.54% -0.22% 6209.58 ms +0.04% -0.57% -0.5%
mediaProcessingJpegToAvif 4249.42 ms +0.47% -0.2% 4273.08 ms +0.65% -0.21% -0.55%

media-upload

Metric 9492791 trunk % Change
jpegUploadProcessing 1726.91 ms +14.91% -15.82% 1457.47 ms +2.76% -0.44% 18.49%
pngUploadProcessing 213.16 ms +18.08% -17.92% 170.19 ms +6.37% -2.76% 25.25%
largeJpegUploadProcessing 1456.06 ms +0.51% -0.57% 1449.03 ms +0.57% -0.36% 0.49%
multipleImageUploadProcessing 2036.89 ms +0.37% -1.14% 2012.8 ms +0.87% -0.43% 1.2%

post-editor

Metric 9492791 trunk % Change
serverResponse 426.41 ms +4.09% -5.44% 439.15 ms +1.01% -0.9% -2.9%
firstPaint 424.73 ms +14.06% -24.44% 485.95 ms +1.97% -36.62% -12.6%
domContentLoaded 853.83 ms +3.13% -0.78% 846.34 ms +1.68% -0.31% 0.88%
loaded 854.75 ms +3.14% -0.76% 847.44 ms +1.67% -0.31% 0.86%
firstContentfulPaint 497.44 ms +1.21% -2.61% 496.42 ms +1.01% -1.7% 0.21%
firstBlock 2152.48 ms +3.86% -0.75% 2153.96 ms +0.52% -0.86% -0.07%
type 18.94 ms +6.23% -10.19% 18.19 ms +11% -5.28% 4.12%
typeWithoutInspector 20.02 ms +1.9% -10.99% 19.21 ms +21.86% -12.08% 4.22%
typeWithTopToolbar 23.67 ms +4.52% -7.39% 23.96 ms +12.35% -11.44% -1.21%
typeContainer 8.3 ms +19.04% -4.82% 8.43 ms +11.15% -4.98% -1.54%
focus 122.01 ms +3.33% -2.48% 123.12 ms +4.41% -6.44% -0.9%
firstFocus 272.05 ms +0% -0% 259.12 ms +0% -0% 4.99%
selectAll 410.64 ms +0.62% -2.64% 405.03 ms +6.73% -2.97% 1.39%
undo 30.47 ms +3.58% -4.86% 32.71 ms +6.94% -8.13% -6.85%
listViewOpen 77.15 ms +3.49% -6.1% 80.46 ms +6.49% -8.35% -4.11%
inserterOpen 41.96 ms +10.75% -10.1% 42.38 ms +10.71% -10.1% -0.99%
inserterHover 11.68 ms +5.14% -6.42% 11.04 ms +10.33% -8.88% 5.8%
inserterSearch 9.28 ms +4.31% -10.67% 8.31 ms +6.5% -6.38% 11.67%
loadPatterns 660.08 ms +2.64% -2.48% 630.64 ms +6.83% -4.03% 4.67%
wpTotal 415.49 ms +4.21% -5.62% 427.61 ms +1.09% -0.77% -2.83%
wpMemoryUsage 14.76 MB +0% -0% 14.72 MB +0% -0% 0.27%
wpDbQueries 55 +0% -0% 55 +0% -0% 0%

site-editor

Metric 9492791 trunk % Change
serverResponse 293.14 ms +1.28% -2.76% 285.32 ms +4.98% -4.17% 2.74%
firstPaint 290.4 ms +23.49% -12.62% 252.13 ms +7.6% -8.72% 15.18%
domContentLoaded 613.99 ms +1.43% -2.08% 618.46 ms +3.18% -3.54% -0.72%
loaded 614.9 ms +1.44% -2.09% 619.35 ms +3.19% -3.53% -0.72%
firstContentfulPaint 360.36 ms +5.25% -2.51% 358.63 ms +4.57% -2.19% 0.48%
firstBlock 1809.38 ms +5.08% -2.78% 1800.59 ms +2.47% -0.98% 0.49%
type 14.69 ms +8.17% -7.9% 15.16 ms +4.29% -6.07% -3.1%
navigate 105.76 ms +4.99% -9.12% 96.43 ms +5.48% -6.9% 9.68%
loadPatterns 926.96 ms +14.72% -11.56% 982.38 ms +8.42% -8.01% -5.64%
loadPages 1018.45 ms +1.46% -2.36% 1045.91 ms +1.09% -3.42% -2.63%
wpTotal 286.31 ms +1.38% -2.72% 278.64 ms +5.01% -4.3% 2.75%
wpMemoryUsage 12.77 MB +0% -0% 12.74 MB +0% -0% 0.31%
wpDbQueries 45 +0% -0% 45 +0% -0% 0%

3ce4afd 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.

Page List converts to Navigation Links with entity bindings in /test/e2e/specs/editor/blocks/navigation.spec.js, passed after 1 failed attempt.
Error: expect(locator).toContainText(expected) failed

Locator: getByRole('region', { name: 'Editor settings' }).getByRole('tabpanel', { name: 'Content' }).getByRole('button', { name: /Link to:/ })
Expected substring: "test-page-1"
Received string:    "Link to:Cat/catPagePublished"
Timeout: 5000ms

Call log:
  - Expect "toContainText" getByRole('region', { name: 'Editor settings' }).getByRole('tabpanel', { name: 'Content' }).getByRole('button', { name: /Link to:/ }) with timeout 5000ms
  - waiting for getByRole('region', { name: 'Editor settings' }).getByRole('tabpanel', { name: 'Content' }).getByRole('button', { name: /Link to:/ })
    14 × locator resolved to <button type="button" aria-expanded="false" aria-haspopup="dialog" class="components-button link-preview-button is-next-40px-default-size is-secondary">…</button>
       - unexpected value "Link to:Cat/catPagePublished"

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/navigation.spec.js:1949:31
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": 1,
+   "offset": 1,
  }

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
should keep correct selection when dragging outside block (-firefox) 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 {
      "attributes": Object {
-       "content": "1",
+       "content": "13",
      },
      "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:787:41
should select by dragging into separator in /test/e2e/specs/editor/various/multi-block-selection.spec.js, passed after 1 failed attempt.
Error: expect(received).toEqual(expected) // deep equality

- Expected  - 1
+ Received  + 0

  Array [
    1,
-   2,
  ]

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:2182:5
should undo without broken temporary state in /test/e2e/specs/editor/blocks/image.spec.js, passed after 1 failed attempt.
Error: expect(received).toBe(expected) // Object.is equality

- Expected  - 2
+ Received  + 2

- <!-- wp:image -->
- <figure class="wp-block-image"><img alt=""/></figure>
+ <!-- wp:image {"id":23,"sizeSlug":"full","linkDestination":"none"} -->
+ <figure class="wp-block-image size-full"><img src="http://localhost:8889/wp-content/uploads/2026/11/6f8b4ea7-9c5f-4583-b87b-aaaa9261bb50.png" alt="" class="wp-image-23"/></figure>
  <!-- /wp:image -->
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/image.spec.js:432:49

From 99f3f97, not the current head. Run

@draganescu draganescu mentioned this pull request Oct 10, 2026
1 of 3 tasks
@draganescu
draganescu added this pull request to stack #84511 October 10, 2026 17:39
draganescu and others added 2 commits October 10, 2026 12:40
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>
@draganescu draganescu mentioned this pull request Oct 10, 2026
3 tasks
draganescu and others added 9 commits October 10, 2026 13:51
…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
draganescu and others added 28 commits October 27, 2026 14:16
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>
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>
@github-actions github-actions Bot added [Package] Blocks /packages/blocks [Package] Style Engine /packages/style-engine labels Nov 3, 2026

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 [Package] Block library /packages/block-library [Package] Blocks /packages/blocks [Package] Style Engine /packages/style-engine [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants