Skip to content

Commit 186dfe5

Browse files
Media Editor: Stop the crop pixel snap from recording undo history (#83571)
Co-authored-by: ramonjd <ramonopoly@git.wordpress.org> Co-authored-by: andrewserong <andrewserong@git.wordpress.org>
1 parent 511d7ce commit 186dfe5

4 files changed

Lines changed: 91 additions & 6 deletions

File tree

‎packages/media-editor/CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Stop a pan drag started just after a wheel zoom from recording one undo entry per frame. The drag now ends the pending wheel gesture instead of letting its debounce timer close the drag's gesture mid-drag ([#83558](https://github.com/WordPress/gutenberg/pull/83558)).
88
- Keep the editor open with an error notice when saving attachment details fails ([#83560](https://github.com/WordPress/gutenberg/pull/83560)).
9+
- Stop the crop's source-pixel snap from recording its own undo entry. A handle crop that magnified the image past 1:1 added an undo step that changed nothing visible, and redoing back onto that crop cleared the rest of the redo history ([#83571](https://github.com/WordPress/gutenberg/pull/83571)).
910

1011
## 0.19.0 (2026-09-23)
1112

‎packages/media-editor/src/image-editor/react/components/cropper.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -571,14 +571,17 @@ function CropperInner(
571571
return;
572572
}
573573

574-
setCropRect( snappedCropRect );
574+
// The display scale crossing the threshold is not an edit, so the
575+
// snap must not record an undo entry. Recording it here would also
576+
// clear the redo stack whenever an undo or redo crosses it.
577+
adjustCropRectForViewport( snappedCropRect );
575578
}, [
579+
adjustCropRectForViewport,
576580
aspectRatio,
577581
displayScale,
578582
freeformCrop,
579583
naturalWidth,
580584
naturalHeight,
581-
setCropRect,
582585
state,
583586
] );
584587

‎packages/media-editor/src/image-editor/react/components/test/cropper.browser.test.tsx‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,7 +1345,7 @@ describe( 'Cropper', () => {
13451345
);
13461346

13471347
await screen.findByTestId( 'cropper-image' );
1348-
expect( controller.setCropRect ).not.toHaveBeenCalled();
1348+
expect( controller.adjustCropRectForViewport ).not.toHaveBeenCalled();
13491349

13501350
controller.state = { ...controller.state, zoom: 3 };
13511351
await rerender(
@@ -1357,9 +1357,12 @@ describe( 'Cropper', () => {
13571357
);
13581358

13591359
await waitFor( () =>
1360-
expect( controller.setCropRect ).toHaveBeenCalledTimes( 1 )
1360+
expect(
1361+
controller.adjustCropRectForViewport
1362+
).toHaveBeenCalledTimes( 1 )
13611363
);
1362-
const rect = ( controller.setCropRect as Mock ).mock.calls[ 0 ][ 0 ];
1364+
const rect = ( controller.adjustCropRectForViewport as Mock ).mock
1365+
.calls[ 0 ][ 0 ];
13631366
const region = getSourceRegion(
13641367
{ ...controller.state, cropRect: rect },
13651368
{ width: image.naturalWidth, height: image.naturalHeight }
@@ -1412,7 +1415,7 @@ describe( 'Cropper', () => {
14121415
/>
14131416
);
14141417

1415-
expect( controller.setCropRect ).not.toHaveBeenCalled();
1418+
expect( controller.adjustCropRectForViewport ).not.toHaveBeenCalled();
14161419
} );
14171420

14181421
function imageRendering(): string {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { userEvent } from 'vitest/browser';
3+
import { act, screen, waitFor } from '@testing-library/react';
4+
import { render } from 'vitest-browser-react';
5+
import { useEffect } from '@wordpress/element';
6+
import { Cropper } from '../../image-editor';
7+
import {
8+
useMediaEditorState,
9+
type MediaEditorController,
10+
} from '../use-media-editor-state';
11+
// Browser Mode needs the package's real styles for layout and transitions.
12+
// eslint-disable-next-line @wordpress/no-non-module-stylesheet-imports
13+
import '../../image-editor/style.scss';
14+
15+
// Shown below 1:1 in the fixture, so pixel snapping stays off until a crop
16+
// magnifies the image past one screen pixel per source pixel.
17+
const IMAGE = {
18+
src: 'large.png',
19+
naturalWidth: 999,
20+
naturalHeight: 999,
21+
};
22+
23+
function CropperWithHistory( {
24+
onController,
25+
}: {
26+
onController: ( controller: MediaEditorController ) => void;
27+
} ) {
28+
const controller = useMediaEditorState( { cropper: { image: IMAGE } } );
29+
useEffect( () => onController( controller ), [ controller, onController ] );
30+
return (
31+
<div style={ { width: 648, height: 448 } }>
32+
<Cropper
33+
src={ IMAGE.src }
34+
controller={ controller }
35+
freeformCrop
36+
onGestureStart={ controller.beginGesture }
37+
onGestureEnd={ controller.endGesture }
38+
/>
39+
</div>
40+
);
41+
}
42+
43+
describe( 'useMediaEditorState with a Cropper', () => {
44+
it( 'keeps redo steps after redoing a handle crop that turns on pixel snapping', async () => {
45+
let controller!: MediaEditorController;
46+
const onController = ( next: MediaEditorController ) => {
47+
controller = next;
48+
};
49+
await render( <CropperWithHistory onController={ onController } /> );
50+
const handle = await screen.findByRole( 'button', {
51+
name: 'Resize from bottom-right corner',
52+
} );
53+
54+
// Shrink the crop to 30% in steps of 10% of a 999px image, so its
55+
// edges fall between source pixels, then wait for it to settle.
56+
handle.focus();
57+
await userEvent.keyboard(
58+
'{Shift>}' +
59+
'{ArrowLeft}'.repeat( 7 ) +
60+
'{ArrowUp}'.repeat( 7 ) +
61+
'{/Shift}'
62+
);
63+
await waitFor( () =>
64+
expect( controller.state.zoom ).toBeGreaterThan( 1 )
65+
);
66+
67+
act( () =>
68+
controller.setFlip( { horizontal: true, vertical: false } )
69+
);
70+
71+
for ( let i = 0; i < 10 && controller.hasUndo; i++ ) {
72+
act( () => controller.undo() );
73+
}
74+
act( () => controller.redo() );
75+
76+
expect( controller.hasRedo ).toBe( true );
77+
} );
78+
} );

0 commit comments

Comments
 (0)