Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/block-library/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Icon: Match an icon's keywords as well as its name and label when searching the icon library ([#82367](https://github.com/WordPress/gutenberg/pull/82367)).
- Button: Only open the link popover from the toolbar button or keyboard shortcut instead of whenever a linked button is selected. The toolbar button now shows a pressed state when a link is set ([#30166](https://github.com/WordPress/gutenberg/pull/30166)).
- Use the `inertValue` helper to set `inert` attributes, for better React 18/19 compatibility ([#83871](https://github.com/WordPress/gutenberg/pull/83871)).
- Image, Cover: Restore the previous attachment with snackbar Undo, including attachment-derived Image attributes while preserving Cover settings ([#81805](https://github.com/WordPress/gutenberg/pull/81805)).

### Bug Fixes

Expand Down
3 changes: 2 additions & 1 deletion packages/block-library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@
"@wordpress/integration-tests": "file:../../test/integration",
"@wordpress/stylelint-tools": "file:../../tools/stylelint",
"deep-freeze": "^0.0.1",
"vitest": "^5.0.3"
"vitest": "^5.0.3",
"vitest-browser-react": "^2.3.0"
},
"peerDependencies": {
"@types/react": "^18 || ^19",
Expand Down
11 changes: 11 additions & 0 deletions packages/block-library/src/cover/edit/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -564,11 +564,18 @@ function CoverEdit( {
return;
}

const previousImage = { id, url: propsRef.current.attributes.url };
let isUndone = false;
openMediaEditorModal( {
id,
onClose: () => {
editMediaButtonRef.current?.focus();
},
onUndo: () => {
isUndone = true;
setIsSwappingMedia( false );
setAttributes( previousImage );
},
onUpdate: async ( { id: newId, url: newUrl } ) => {
if ( typeof newId !== 'number' ) {
return;
Expand All @@ -590,6 +597,10 @@ function CoverEdit( {
if ( newUrl ) {
const averageBackgroundColor =
await getMediaColor( newUrl );
// Snackbar Undo can run while the image color is loading.
if ( isUndone ) {
return;
}

// Read latest values after await to avoid stale closures.
const {
Expand Down
181 changes: 179 additions & 2 deletions packages/block-library/src/cover/test/edit.browser.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
import { beforeEach, describe, expect, test } from 'vitest';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { userEvent } from 'vitest/browser';
import {
cleanup as cleanupBrowser,
render as renderBrowser,
} from 'vitest-browser-react';
import { screen, act, within, waitFor } from '@testing-library/react';
import { createBlock } from '@wordpress/blocks';
import {
createBlock,
getBlockTypes,
unregisterBlockType,
} from '@wordpress/blocks';
import { createElement, useState } from '@wordpress/element';
import { dispatch, select } from '@wordpress/data';
import {
store as richTextStore,
unregisterFormatType,
} from '@wordpress/rich-text';
import {
BlockInspector,
privateApis as blockEditorPrivateApis,
store as blockEditorStore,
} from '@wordpress/block-editor';
import {
initializeEditor,
selectBlock,
Expand All @@ -10,6 +29,16 @@ import { registerCoreBlocks } from '@wordpress/block-library';
// Keep the matrix popover above the block toolbar, as in the editor.
import '../../../../block-editor/src/components/block-popover/style.scss';
import '../../../../components/src/popover/style.scss';
import { unlock } from '../../lock-unlock';
import { getMediaColor } from '../edit/color-utils';

vi.mock( import( '../edit/color-utils' ), { spy: true } );

const {
ExperimentalBlockCanvas: BlockCanvas,
ExperimentalBlockEditorProvider: BlockEditorProvider,
openMediaEditorModalKey,
} = unlock( blockEditorPrivateApis );
const defaultSettings = {
__experimentalFeatures: {
color: {
Expand Down Expand Up @@ -210,6 +239,154 @@ describe( 'Cover block', () => {
} );
} );

describe( 'media editor Undo', () => {
const originalUrl =
'data:image/svg+xml,' +
encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect width="10" height="10" fill="black"/></svg>'
);
const updatedImage = { id: 11, url: `${ originalUrl }#edited` };

beforeEach( () => {
vi.mocked( getMediaColor ).mockResolvedValue( '#ffffff' );
} );

afterEach( async () => {
await cleanupBrowser();
getBlockTypes().forEach( ( { name } ) =>
unregisterBlockType( name )
);
select( richTextStore )
.getFormatTypes()
.forEach( ( { name } ) => unregisterFormatType( name ) );
} );

async function openMediaEditor( initialAttributes = {} ) {
const openModal = vi.fn();
const block = createBlock( 'core/cover', {
id: 10,
url: originalUrl,
backgroundType: 'image',
sizeSlug: 'medium',
overlayColor: 'black',
isDark: true,
isUserOverlayColor: false,
...initialAttributes,
} );
function TestEditor() {
const [ blocks, setBlocks ] = useState( [ block ] );
return createElement(
BlockEditorProvider,
{
value: blocks,
onInput: setBlocks,
onChange: setBlocks,
useSubRegistry: false,
settings: {
...defaultSettings,
[ openMediaEditorModalKey ]: openModal,
},
},
createElement( BlockInspector ),
createElement( BlockCanvas, {
height: '100%',
shouldIframe: false,
} )
);
}
await renderBrowser( createElement( TestEditor ) );
await userEvent.click( screen.getByLabelText( 'Block: Cover' ) );
await userEvent.click(
screen.getByRole( 'button', { name: 'Edit image' } )
);
const { clientId } = select( blockEditorStore ).getBlocks()[ 0 ];
return {
...openModal.mock.calls[ 0 ][ 0 ],
getAttributes: () =>
select( blockEditorStore ).getBlockAttributes( clientId ),
editAttributes: ( attributes ) =>
dispatch( blockEditorStore ).updateBlockAttributes(
clientId,
attributes
),
};
}

test( 'restores only the previous image without reverting Cover settings', async () => {
const { onUpdate, onUndo, getAttributes, editAttributes } =
await openMediaEditor();
await act( () => onUpdate( updatedImage ) );
expect( getAttributes() ).toMatchObject( {
id: 11,
sizeSlug: 'full',
overlayColor: 'white',
isDark: false,
} );
await act( () =>
editAttributes( { dimRatio: 75, sizeSlug: 'large' } )
);
const beforeUndo = getAttributes();

act( () => onUndo() );

expect( getAttributes() ).toEqual( {
...beforeUndo,
id: 10,
url: originalUrl,
} );
} );

test( 'preserves an overlay chosen after saving when undoing the media update', async () => {
const { onUpdate, onUndo, getAttributes, editAttributes } =
await openMediaEditor();
await act( () => onUpdate( updatedImage ) );
await act( () =>
editAttributes( {
overlayColor: undefined,
customOverlayColor: '#000000',
isUserOverlayColor: true,
dimRatio: 90,
isDark: true,
} )
);

const beforeUndo = getAttributes();
act( () => onUndo() );

expect( getAttributes() ).toEqual( {
...beforeUndo,
id: 10,
url: originalUrl,
} );
} );

test( 'does not reapply a media update when Undo runs before its color loads', async () => {
const { onUpdate, onUndo, getAttributes } = await openMediaEditor();
const previous = getAttributes();
let resolveColor;
vi.mocked( getMediaColor ).mockReturnValueOnce(
new Promise( ( resolve ) => {
resolveColor = resolve;
} )
);
let updatePromise;
act( () => {
updatePromise = onUpdate( updatedImage );
} );

act( () => onUndo() );
await act( async () => {
resolveColor( '#ffffff' );
await updatePromise;
} );

expect( getAttributes() ).toEqual( previous );
expect(
screen.getByRole( 'button', { name: 'Edit image' } )
).toBeEnabled();
} );
} );

describe( 'Inspector controls', () => {
describe( 'Media settings', () => {
test( 'does not display media settings panel if url is not set', async () => {
Expand Down
Loading
Loading