Image editor: suppress interaction while undo/redo - #77928
Conversation
…llation for active interactions - Introduced a mechanism to suppress wheel events until the wheel stream is idle, improving user experience during state changes. - Updated cancelActiveInteraction method to support suppressing wheel momentum based on interaction cancellation signals. - Added tests to verify the functionality of wheel suppression during active interactions. - Adjusted related components to accommodate the new interaction cancellation signal.
There was a problem hiding this comment.
Pull request overview
This PR updates the media editor image cropper so undo/redo and other state-replacing actions can cancel in-flight interaction state, especially lingering wheel-zoom momentum. It fits into the cropper’s React/controller split by wiring a cancellation signal from useCropperState through Cropper into useInteraction/InteractionController.
Changes:
- Adds an interaction-cancellation signal to
useCropperStateand emits it for undo/redo, reset, image load, and several discrete transform actions. - Adds controller-side cancellation logic that clears active timers/listeners and can temporarily suppress wheel momentum until the wheel stream goes idle.
- Extends hook/controller tests to cover canceling active wheel interactions and momentum suppression.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
packages/media-editor/src/image-editor/react/hooks/use-interaction.ts |
React hook now reacts to cancellation signals and resets local interaction state. |
packages/media-editor/src/image-editor/react/hooks/use-cropper-state.ts |
Cropper state hook now emits a cancellation signal during state-replacing actions. |
packages/media-editor/src/image-editor/react/hooks/test/use-interaction.ts |
Adds hook-level tests for canceling wheel placement and suppressing momentum. |
packages/media-editor/src/image-editor/react/components/cropper.tsx |
Threads the new cancellation signal from the controller into useInteraction. |
packages/media-editor/src/image-editor/core/test/interaction-controller.ts |
Adds controller-level tests for canceling wheel zoom and swallowing momentum. |
packages/media-editor/src/image-editor/core/interaction-controller.ts |
Implements interaction cancellation and wheel-idle suppression in the controller. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| suppressWheel: true, | ||
| } ); | ||
| }, [ cancelInteractionSignal ] ); |
| it( 'cancels wheel placement and ignores wheel momentum when the cancel signal changes', () => { | ||
| jest.useFakeTimers(); | ||
| const actions = createActions(); | ||
| const { result, rerender } = renderHook( | ||
| ( { cancelSignal }: { cancelSignal: number } ) => | ||
| useInteraction( | ||
| makeState(), | ||
| actions, | ||
| CONTAINER_SIZE, | ||
| IMAGE_SIZE, | ||
| { cancelInteractionSignal: cancelSignal } | ||
| ), | ||
| { initialProps: { cancelSignal: 0 } } | ||
| ); | ||
|
|
||
| act( () => { | ||
| result.current.onWheelNative( | ||
| createWheelEvent( { deltaY: -100, currentTarget: null } ) | ||
| ); | ||
| } ); | ||
|
|
||
| expect( result.current.isPlacementActive ).toBe( true ); | ||
| expect( actions.setZoom ).toHaveBeenCalledTimes( 1 ); | ||
|
|
||
| rerender( { cancelSignal: 1 } ); | ||
|
|
||
| expect( result.current.isPlacementActive ).toBe( false ); | ||
|
|
||
| actions.setZoom.mockClear(); | ||
| act( () => { | ||
| result.current.onWheelNative( | ||
| createWheelEvent( { deltaY: -100, currentTarget: null } ) | ||
| ); | ||
| } ); | ||
|
|
||
| expect( actions.setZoom ).not.toHaveBeenCalled(); | ||
|
|
||
| act( () => { | ||
| jest.advanceTimersByTime( 300 ); | ||
| result.current.onWheelNative( | ||
| createWheelEvent( { deltaY: -100, currentTarget: null } ) | ||
| ); | ||
| } ); | ||
|
|
||
| expect( actions.setZoom ).toHaveBeenCalledTimes( 1 ); | ||
| } ); |
| /** Changes when state-replacing actions should cancel active interactions. */ | ||
| interactionCancellationSignal?: number; |
|
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. |
|
Size Change: +227 B (0%) Total Size: 7.91 MB 📦 View Changed
ℹ️ View Unchanged
|
|
Flaky tests detected in f34ad23. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/25359584510
|
|
Closing in favour of #77930 |
What?
Fixes undo/redo while an image-editor interaction is still active, especially wheel zoom with trackpad/mouse momentum.
Before
2026-05-05.15.26.28.mp4
After
2026-05-05.15.25.10.mp4
Why?
If a user scroll-zooms the image and immediately hits undo, the cropper state is restored but the interaction controller can keep its active zoom/gesture timers alive. That lets the animated zoom/gesture state continue after undo, making the restored state feel like it is still being driven by the prior wheel gesture.
How?
useCropperState.useInteractionto cancel the active controller state when the signal changes.InteractionController.cancelActiveInteraction(), which clears active rAF/timers/listeners and resets drag/zoom status.Testing Instructions
Cmd+Z/Ctrl+Z, before the zoom motion has fully settled.Cmd+Z/Ctrl+Zas well. That should interrupt the drag.