Media editor: replace fine-rotation slider with RotationRuler#77906
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
| * keypresses should coalesce into a single history entry via the | ||
| * state-change debounce. | ||
| */ | ||
| commitOnKeyUp?: boolean; |
There was a problem hiding this comment.
Not sure if this is the right way, but the debounce expiry was creating multiple history entires while the user has the mouse down and still adjusting the fine rotate.
There was a problem hiding this comment.
This addition makes sense to me: we're providing a general approach for controls to use (rely on debounce expiry), but some controls will need to be slightly more specific in how committing a value to history should work.
I think at the moment we've got a pretty good default, and adding commitOnKeyUp as a flag (or options in general in order to allow more flags in the future) seems good to me 👍
|
Size Change: +2.81 kB (+0.04%) Total Size: 7.95 MB 📦 View Changed
ℹ️ View Unchanged
|
There was a problem hiding this comment.
Pull request overview
This PR updates the media editor rotation UX by replacing the fine-rotation RangeControl with a new RotationRuler component, and reorganizes the toolbar layout to group rotation controls separately from primary actions while preserving undo/redo history behavior.
Changes:
- Added a new private
RotationRulercomponent (drag-scrub ruler + accessible hidden range input) with Storybook and unit tests. - Updated the media editor toolbar UI/SCSS to use two clusters and swap the slider for
RotationRuler. - Extended
useCropGestureHandlerswith an option to control history commits on key-up to support coalesced keyboard adjustments.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/media-editor/src/style.scss | Includes the new rotation ruler stylesheet in the package styles entrypoint. |
| packages/media-editor/src/hooks/use-crop-gesture-handlers.ts | Adds commitOnKeyUp option to control history flush timing for continuous inputs. |
| packages/media-editor/src/components/rotation-ruler/use-ruler-drag.ts | Implements pointer-drag → value conversion helpers and drag handlers. |
| packages/media-editor/src/components/rotation-ruler/index.tsx | Adds the RotationRuler component with hidden range input + tick strip rendering. |
| packages/media-editor/src/components/rotation-ruler/style.scss | Provides component styling, including focus ring, tick strip, and container-query behavior. |
| packages/media-editor/src/components/rotation-ruler/test/index.tsx | Adds unit tests for math helpers and basic rendering/keyboard/disabled behavior. |
| packages/media-editor/src/components/rotation-ruler/stories/index.story.tsx | Adds a Storybook story for interactive manual verification. |
| packages/media-editor/src/components/rotation-ruler/README.md | Documents the private component’s usage, props pointer, and keyboard behavior. |
| packages/media-editor/src/components/media-editor-toolbar/style.scss | Refactors toolbar layout into rotate/primary clusters with responsive behavior and divider. |
| packages/media-editor/src/components/media-editor-toolbar/index.tsx | Swaps RangeControl for RotationRuler and wires history coalescing for keyboard input. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
61a14b6 to
6327294
Compare
|
Flaky tests detected in aa011cf. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/25411378323
|
0de1514 to
4551c94
Compare
andrewserong
left a comment
There was a problem hiding this comment.
Great work, I love the look of this new control, feels super fancy 🎩
Overall, it immediately makes the UI feel much more polished to me 👍
A couple of subjective first impressions:
- To me the snap rotation buttons are better friends with the flip and other buttons. In my mind the logical groupings are "fine grained controls" and "buttons that do things", so I'd regroup them a little. In practice this might mean moving the rotation buttons to the right on desktop with the fine-grained controls on the left. E.g. something like this (I just hacked it in the browser):
- Similarly on mobile, I'd move the rotation buttons to sit beneath the fine-grained rotation and be on the same row as the flip and reset buttons, etc
- This is very nit picky and again just a subjective opinion, but to me, visually, I kind of want the centering of the number over the marker to not include the position of the degree symbol so that the number is the thing that's centered. Then the degrees symbol might be absolute positioned to sit next to it. To my eyes it looks slightly unbalanced at the moment:
A couple of minor UX-ey things with interacting with the control:
- I find it hard to reset it back to zero. This is probably a hard one to strike a balance on, but if I want to reset just the rotation, currently it's hard to do. And if we snap to zero, it might make it hard when folks really do want to do a subtle nudge just next to the zero value. Not sure what the right solution there is.
- I found myself wanting to be able to click on the number itself to be able to do manual keyboard entry of a degrees number
- I noticed a very tiny flicker if I scroll forwards and backwards past the 0 position, but wasn't able to capture it in a screengrab
Overall though, this feels like a very solid direction to me!
f3239b3 to
d6bf3c2
Compare
of course not, i value the constant feedback! this thing gets better every day
bumping the steps up to 1deg (same as google) was a conscious measure to improve accuracy of the slider and make it easier to hit the desired number. it's a bit of a balance here. we could try how fine are you expecting? is there an example photo you were using to test?
bailing out of the event when the pointer leaves the control was also intentional to prevent the pointer from being captured, but there's probably a middle ground here too, e.g., something like, keep tracking if the drag exceeds the control bounds, and bail at the end of the drag, divorcing it from the component itself. i'll look more closely here, i think we can get to a decent experience. cheers!! 🍺 |
Fixed! Drag should now continue outside the component, and release outside. I actually went too far in the last round. What I missed was a window-level
After a bit of testing, I'd like to keep the integer drag since it's what gives the slider its snappiness. It also makes going back to BUT I believe I've found a compromise 🌮: SHIFT + arrow key or drag will move in 0.5deg increments. I think it gives fine precision without sacrificing the snappy feel. The real problem here was fractional CSS pixels, so I'd be hesitant to go any lower than 0.5. 2026-05-05.20.25.46.mp4Let me know what you reckon!
Totally, sounds like an advanced controls follow up. |
0c6fcca to
3ed7e12
Compare
andrewserong
left a comment
There was a problem hiding this comment.
Great stuff, thanks for all the nuanced follow-up here. It's feeling really nice to use, so this looks like a good place to land it to me 👍 (and overall I love having a bespoke control for this, feels fancy). I left a couple of minor comments while I was reading through, but nothing that needs changing if you're not planning to make any more changes of your own.
What follows is a little more wordy on some of the stuff we've been chatting about here 😄
On the topic of advanced controls / ultra fine rotation, I wanted to expand on it slightly — not that we need to do anything about it in this PR, but mostly to capture my own use case for very fine rotation. With photos from my phone, it's actually the main adjustment I wind doing before adding an image to my blog, or more commonly, after uploading to my blog and then finding that the horizon feels off.
Probably no-one else notices, but I do! Here's a common example for me:
how fine are you expecting? is there an example photo you were using to test?
When I take a photo of a horizon containing the sea and hills, it's very hard visually to see if you've gotten the horizon straight because it's partly obscured. So in the photo below, it's very close to level, but feels a tiny bit off to me, especially since it appears at the bottom of the image:
If I go to use Affinity and add a ruler and use the advanced controls to rotate to decimal places in the rotation tool, I find that for this image the sweet spot is somewhere around 0.3 degrees:
In this case, 1 degree rotation feels far too extreme, because the area that needs to be rotated is so subtle. 0.5 gets us really close, which for this one feels just about good enough to me:
(In the above screenshot, note that in our cropper 0.5 is a clockwise rotation, so for this image I've rotated to -0.5. For some reason in Affinity positive degrees mean an anti-clockwise rotation. I think what we're doing is the right approach as it matches the CSS transform spec for clockwise rotation)
Like I mentioned, I think in the majority of cases people will be rotating by feel rather than advanced controls, and none of what I described above needs to be addressed in this PR! But tangentially, I'm looking forward to us adding in advanced controls too 😄
LGTM 🚀
| * keypresses should coalesce into a single history entry via the | ||
| * state-change debounce. | ||
| */ | ||
| commitOnKeyUp?: boolean; |
There was a problem hiding this comment.
This addition makes sense to me: we're providing a general approach for controls to use (rely on debounce expiry), but some controls will need to be slightly more specific in how committing a value to history should work.
I think at the moment we've got a pretty good default, and adding commitOnKeyUp as a flag (or options in general in order to allow more flags in the future) seems good to me 👍
3ed7e12 to
aa011cf
Compare
|
PHP errors failing after I'll merge this when other tests pass. |
Co-authored-by: ramonjd <ramonopoly@git.wordpress.org> Co-authored-by: andrewserong <andrewserong@git.wordpress.org>


What
Part of:
Replaces the
RangeControlfor fine-tune rotation in the media editor toolbar withRotationRuler— a horizontal ruler-slider with drag-the-strip scrubbing, labelled major ticks, and a prominent active-value readout that sits over the pointer.Kapture.2026-05-05.at.11.25.24.mp4
The toolbar is also reorganised: the ruler is one flex item, the action buttons (snap-rotate, flip, undo, redo, reset) are another. Below the modal-sidebar breakpoint (782 px) they stack — ruler on top, actions below.
Component
packages/media-editor/src/components/rotation-ruler/— private to@wordpress/media-editor.value,onChange,label, plusmin/max/step/unit/pixelsPerStep/className/id/disabled.<input type="range">is the source of truth for accessibility —aria-label,aria-valuetext(e.g.-14°), keyboard, focus, form association.useRulerDrag:setPointerCaptureonpointerdown, px-delta translated to value-delta via the configurablepixelsPerStep. Drag values are quantized to multiples ofstepvia a purequantize()helper.pointermovechecks the bounding rect and releases capture if outside.pixelsPerStep / stepso SVG tick spacing always matches the gesture.step, ArrowRight / ArrowUp increment bystep. Home / End / PageUp / PageDown fall through to the native input. Arrow keyspreventDefaultso the native input's own stepping doesn't double-fireonChange.var(--wp-admin-theme-color, #3858e9)fallback for the pointer triangle) so the component renders correctly in Storybook and in any host that isn't setting the admin theme color.Toolbar
.media-editor-toolbar__rotation-slider(still thedata-crop-controlancestor for the modal's Cmd+Z handler), grows to absorb extra horizontal space, capped at 360 px.[Rotate -90°] [Rotate +90°] [Flip H] [Flip V] [Undo] [Redo] [Reset],flex-wrap: nowrap.$gray-300left border so the two regions are visually divided.History coalescing
useCropGestureHandlersgained acommitOnKeyUpoption (defaulttrue— preserves existing behaviour). The rotation ruler passescommitOnKeyUp: falseso rapid arrow-key adjustments collapse into a single undo entry via the existing 300 ms state-change debounce. Pointer drags still commit on release as before.Testing
8 tests in
packages/media-editor/src/components/rotation-ruler/test/index.tsx:pxToValueDelta,clampValue,quantize.aria-valuetext.Pointer-drag and bounds-leave behaviours are intentionally not unit-tested in jsdom (would require
getBoundingClientRectstubbing for marginal value); covered end-to-end via the Storybook story and the toolbar.Test plan
MediaEditor / RotationRuler / Default) renders styled with admin-blue pointer