Skip to content

Commit 77e3881

Browse files
committed
Media editor: fix restore details and snackbar Undo
1 parent 3248fec commit 77e3881

17 files changed

Lines changed: 736 additions & 89 deletions

File tree

‎package-lock.json‎

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/block-library/CHANGELOG.md‎

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

99
### Bug Fixes
1010

11+
- Image, Cover: Restore the block values changed by a media edit when using snackbar Undo, including image metadata, size, links, and automatic cover overlays ([#81805](https://github.com/WordPress/gutenberg/pull/81805)).
1112
- Image: Fix the lightbox being impossible to close when its overlay is not a direct child of `<body>` ([#83480](https://github.com/WordPress/gutenberg/pull/83480)).
1213
- Image: Stop the lightbox from removing `inert` that a theme set on elements outside the overlay ([#83480](https://github.com/WordPress/gutenberg/pull/83480)).
1314

‎packages/block-library/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@
146146
"@wordpress/integration-tests": "file:../../test/integration",
147147
"@wordpress/stylelint-tools": "file:../../tools/stylelint",
148148
"deep-freeze": "^0.0.1",
149-
"vitest": "^5.0.0"
149+
"vitest": "^5.0.0",
150+
"vitest-browser-react": "^2.3.0"
150151
},
151152
"peerDependencies": {
152153
"@types/react": "^18 || ^19",

‎packages/block-library/src/cover/edit/index.jsx‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,11 +543,18 @@ function CoverEdit( {
543543
return;
544544
}
545545

546+
const undoAttributes = {};
547+
let isUndone = false;
546548
openMediaEditorModal( {
547549
id,
548550
onClose: () => {
549551
editMediaButtonRef.current?.focus();
550552
},
553+
onUndo: () => {
554+
isUndone = true;
555+
setIsSwappingMedia( false );
556+
setAttributes( undoAttributes );
557+
},
551558
onUpdate: async ( { id: newId, url: newUrl } ) => {
552559
if ( typeof newId !== 'number' ) {
553560
return;
@@ -565,10 +572,17 @@ function CoverEdit( {
565572
? { sizeSlug: DEFAULT_MEDIA_SIZE_SLUG }
566573
: {} ),
567574
};
575+
for ( const key of Object.keys( nextAttributes ) ) {
576+
undoAttributes[ key ] = propsRef.current.attributes[ key ];
577+
}
568578

569579
if ( newUrl ) {
570580
const averageBackgroundColor =
571581
await getMediaColor( newUrl );
582+
// Snackbar Undo can run while the image color is loading.
583+
if ( isUndone ) {
584+
return;
585+
}
572586

573587
// Read latest values after await to avoid stale closures.
574588
const {
@@ -578,12 +592,18 @@ function CoverEdit( {
578592

579593
let newOverlayColor = currentOverlay.color;
580594
if ( ! currentAttrs.isUserOverlayColor ) {
595+
undoAttributes.overlayColor = currentAttrs.overlayColor;
596+
undoAttributes.customOverlayColor =
597+
currentAttrs.customOverlayColor;
581598
newOverlayColor = averageBackgroundColor;
582599
setOverlayColor( newOverlayColor );
583600
// Fold next attribute change into the same undo level as the setOverlayColor above.
584601
__unstableMarkNextChangeAsNotPersistent();
585602
}
586603

604+
undoAttributes.isDark = currentAttrs.isDark;
605+
undoAttributes.isUserOverlayColor =
606+
currentAttrs.isUserOverlayColor;
587607
nextAttributes.isDark = compositeIsDark(
588608
currentAttrs.dimRatio,
589609
newOverlayColor,

‎packages/block-library/src/cover/test/edit.browser.test.js‎

Lines changed: 172 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
1-
import { beforeEach, describe, expect, test } from 'vitest';
1+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
22
import { userEvent } from 'vitest/browser';
3+
import {
4+
cleanup as cleanupBrowser,
5+
render as renderBrowser,
6+
} from 'vitest-browser-react';
37
import { screen, act, within, waitFor } from '@testing-library/react';
4-
import { createBlock } from '@wordpress/blocks';
8+
import {
9+
createBlock,
10+
getBlockTypes,
11+
unregisterBlockType,
12+
} from '@wordpress/blocks';
13+
import { createElement, useState } from '@wordpress/element';
14+
import { dispatch, select } from '@wordpress/data';
15+
import {
16+
store as richTextStore,
17+
unregisterFormatType,
18+
} from '@wordpress/rich-text';
19+
import {
20+
BlockInspector,
21+
privateApis as blockEditorPrivateApis,
22+
store as blockEditorStore,
23+
} from '@wordpress/block-editor';
524
import {
625
initializeEditor,
726
selectBlock,
@@ -12,6 +31,16 @@ import { registerCoreBlocks } from '@wordpress/block-library';
1231
import '../../../../block-editor/src/components/block-popover/style.scss';
1332
// eslint-disable-next-line @wordpress/no-non-module-stylesheet-imports -- Browser fixtures need the styles WordPress normally enqueues.
1433
import '../../../../components/src/popover/style.scss';
34+
import { unlock } from '../../lock-unlock';
35+
import { getMediaColor } from '../edit/color-utils';
36+
37+
vi.mock( import( '../edit/color-utils' ), { spy: true } );
38+
39+
const {
40+
ExperimentalBlockCanvas: BlockCanvas,
41+
ExperimentalBlockEditorProvider: BlockEditorProvider,
42+
openMediaEditorModalKey,
43+
} = unlock( blockEditorPrivateApis );
1544
const defaultSettings = {
1645
__experimentalFeatures: {
1746
color: {
@@ -212,6 +241,147 @@ describe( 'Cover block', () => {
212241
} );
213242
} );
214243

244+
describe( 'media editor Undo', () => {
245+
const originalUrl =
246+
'data:image/svg+xml,' +
247+
encodeURIComponent(
248+
'<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect width="10" height="10" fill="black"/></svg>'
249+
);
250+
const updatedImage = { id: 11, url: `${ originalUrl }#edited` };
251+
252+
beforeEach( () => {
253+
vi.mocked( getMediaColor ).mockResolvedValue( '#ffffff' );
254+
} );
255+
256+
afterEach( async () => {
257+
await cleanupBrowser();
258+
getBlockTypes().forEach( ( { name } ) =>
259+
unregisterBlockType( name )
260+
);
261+
select( richTextStore )
262+
.getFormatTypes()
263+
.forEach( ( { name } ) => unregisterFormatType( name ) );
264+
} );
265+
266+
async function openMediaEditor( initialAttributes = {} ) {
267+
const openModal = vi.fn();
268+
const block = createBlock( 'core/cover', {
269+
id: 10,
270+
url: originalUrl,
271+
backgroundType: 'image',
272+
sizeSlug: 'medium',
273+
overlayColor: 'black',
274+
isDark: true,
275+
isUserOverlayColor: false,
276+
...initialAttributes,
277+
} );
278+
function TestEditor() {
279+
const [ blocks, setBlocks ] = useState( [ block ] );
280+
return createElement(
281+
BlockEditorProvider,
282+
{
283+
value: blocks,
284+
onInput: setBlocks,
285+
onChange: setBlocks,
286+
useSubRegistry: false,
287+
settings: {
288+
...defaultSettings,
289+
[ openMediaEditorModalKey ]: openModal,
290+
},
291+
},
292+
createElement( BlockInspector ),
293+
createElement( BlockCanvas, {
294+
height: '100%',
295+
shouldIframe: false,
296+
} )
297+
);
298+
}
299+
await renderBrowser( createElement( TestEditor ) );
300+
await userEvent.click( screen.getByLabelText( 'Block: Cover' ) );
301+
await userEvent.click(
302+
screen.getByRole( 'button', { name: 'Edit image' } )
303+
);
304+
const { clientId } = select( blockEditorStore ).getBlocks()[ 0 ];
305+
return {
306+
...openModal.mock.calls[ 0 ][ 0 ],
307+
getAttributes: () =>
308+
select( blockEditorStore ).getBlockAttributes( clientId ),
309+
editAttributes: ( attributes ) =>
310+
dispatch( blockEditorStore ).updateBlockAttributes(
311+
clientId,
312+
attributes
313+
),
314+
};
315+
}
316+
317+
test( 'restores the image size and automatic overlay without reverting unrelated edits', async () => {
318+
const { onUpdate, onUndo, getAttributes, editAttributes } =
319+
await openMediaEditor();
320+
const previous = getAttributes();
321+
await act( () => onUpdate( updatedImage ) );
322+
expect( getAttributes() ).toMatchObject( {
323+
id: 11,
324+
sizeSlug: 'full',
325+
overlayColor: 'white',
326+
isDark: false,
327+
} );
328+
await act( () => editAttributes( { dimRatio: 75 } ) );
329+
330+
act( () => onUndo() );
331+
332+
expect( getAttributes() ).toEqual( { ...previous, dimRatio: 75 } );
333+
} );
334+
335+
test( 'preserves a custom overlay when undoing the media update', async () => {
336+
const { onUpdate, onUndo, getAttributes, editAttributes } =
337+
await openMediaEditor( {
338+
overlayColor: undefined,
339+
customOverlayColor: '#123456',
340+
isUserOverlayColor: true,
341+
} );
342+
await act( () => onUpdate( updatedImage ) );
343+
await act( () =>
344+
editAttributes( { customOverlayColor: '#654321' } )
345+
);
346+
347+
act( () => onUndo() );
348+
349+
expect( getAttributes() ).toMatchObject( {
350+
id: 10,
351+
url: originalUrl,
352+
sizeSlug: 'medium',
353+
customOverlayColor: '#654321',
354+
isUserOverlayColor: true,
355+
} );
356+
} );
357+
358+
test( 'does not reapply a media update when Undo runs before its color loads', async () => {
359+
const { onUpdate, onUndo, getAttributes } = await openMediaEditor();
360+
const previous = getAttributes();
361+
let resolveColor;
362+
vi.mocked( getMediaColor ).mockReturnValueOnce(
363+
new Promise( ( resolve ) => {
364+
resolveColor = resolve;
365+
} )
366+
);
367+
let updatePromise;
368+
act( () => {
369+
updatePromise = onUpdate( updatedImage );
370+
} );
371+
372+
act( () => onUndo() );
373+
await act( async () => {
374+
resolveColor( '#ffffff' );
375+
await updatePromise;
376+
} );
377+
378+
expect( getAttributes() ).toEqual( previous );
379+
expect(
380+
screen.getByRole( 'button', { name: 'Edit image' } )
381+
).toBeEnabled();
382+
} );
383+
} );
384+
215385
describe( 'Inspector controls', () => {
216386
describe( 'Media settings', () => {
217387
test( 'does not display media settings panel if url is not set', async () => {

0 commit comments

Comments
 (0)