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
1 change: 1 addition & 0 deletions packages/block-library/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Bug Fixes

- Image: Fix cropped galleries rendering images at their natural height in the editor canvas. The baseline inline `height: auto` is no longer emitted for images inside a cropped gallery, so the gallery's own cropping CSS applies ([#82318](https://github.com/WordPress/gutenberg/pull/82318)).
- Tabs: Activate the tab that a URL hash points into, so an anchor set on a block inside a tab panel can be reached. Anchor links followed after the page has loaded are handled too, matching the Accordion block ([#81744](https://github.com/WordPress/gutenberg/pull/81744)).
- Accordion Panel: Reset padding-block when panel is hidden ([#81782](https://github.com/WordPress/gutenberg/pull/81782)).
- Query: Stop writing `excludeCurrent: null` into the `query` attribute of blocks that never had the key. The mount effect that clears a stale exclusion treated the absent key as stale, changing the serialized markup of every pre-existing Query block as soon as the editor opened it ([#82147](https://github.com/WordPress/gutenberg/pull/82147)).
Expand Down
28 changes: 26 additions & 2 deletions packages/block-library/src/gallery/dynamic-gallery.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { __, sprintf } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { useMemo, useState } from '@wordpress/element';
import {
Button,
Notice,
Expand All @@ -19,6 +19,7 @@ import {
__experimentalUseBlockPreview as useBlockPreview,
} from '@wordpress/block-editor';
import { sharedIcon } from './shared-icon';
import { isGalleryFlexLayout } from './shared';
import { Caption } from '../utils/caption';
import { DEFAULT_ORDERBY, DEFAULT_ORDER, MAX_IMAGES } from './dynamic-source';

Expand Down Expand Up @@ -282,12 +283,21 @@ export function GallerySourcePanel( {
* stays editable. This relies on the gallery's image styles using descendant
* (not direct-child) selectors, which the box-less wrapper leaves intact.
*
* The gallery's layout is passed through so the previewed images see the same
* parent layout that real inner blocks would (`useBlockPreview` provides it to
* their layout context). Without it they resolve to the default flow layout and
* behave as if they weren't in a gallery — most visibly, an image inside a
* cropped gallery would keep its baseline `height: auto` and defeat the
* gallery's cropping CSS.
*
* @param {Object} props
* @param {Object[]} props.imageBlocks Non-persisted `core/image` blocks to preview.
* @param {Object} props.layout The gallery's layout, for the preview's layout context.
*/
function GalleryImagesPreview( { imageBlocks } ) {
function GalleryImagesPreview( { imageBlocks, layout } ) {
const { children, ref, className } = useBlockPreview( {
blocks: imageBlocks,
layout,
} );
return (
<div
Expand Down Expand Up @@ -353,6 +363,19 @@ export function GalleryDynamicView( {

const [ isConfirmingDetach, setIsConfirmingDetach ] = useState( false );

// The layout the previewed images sit in. Normalized the same way the
// gallery's own classes are (`isGalleryFlexLayout`), so a layout that the
// wrapper treats as flex — including a missing or typeless one — is reported
// as flex to the images rather than resolving to the default flow layout.
// Memoized because it becomes the preview's layout context value.
const previewLayout = useMemo(
() =>
isGalleryFlexLayout( attributes.layout )
? { ...attributes.layout, type: 'flex' }
: attributes.layout,
[ attributes.layout ]
);

// Empty-state copy for the preview. Framed as forward-looking ("… will appear
// here") rather than as an error, since the same empty result covers both a
// post with no matching images and a template with no post in context yet —
Expand Down Expand Up @@ -396,6 +419,7 @@ export function GalleryDynamicView( {
<BlockContextProvider value={ galleryContext }>
<GalleryImagesPreview
imageBlocks={ dynamicImageBlocks }
layout={ previewLayout }
/>
</BlockContextProvider>
) : (
Expand Down
24 changes: 17 additions & 7 deletions packages/block-library/src/image/image.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,11 @@ export default function Image( {
setOffsetTop( imageElement?.offsetTop ?? 0 );
}, [ imageElement ] );
const setRefs = useMergeRefs( [ setImageElement, setResizeObserved ] );
const { allowResize = true } = context;
const { allowResize = true, imageCrop = false } = context;
// Only a cropped gallery (flex layout) controls the image height via its
// own CSS. Grid galleries and standalone images keep the baseline
// `height: auto` so a theme can't squish them.
const isCroppedGalleryImage = imageCrop && parentLayoutType === 'flex';

const { image, attachmentResolutionError } = useSelect(
( select ) => {
Expand Down Expand Up @@ -1222,17 +1226,23 @@ export default function Image( {
? `${ width }px`
: width;
}
if (
height === 'auto' ||
height === undefined ||
height === null
) {
if ( height === 'auto' ) {
style.height = 'auto';
} else {
} else if (
height !== undefined &&
height !== null
) {
style.height =
typeof height === 'number'
? `${ height }px`
: height;
} else if ( ! isCroppedGalleryImage ) {
// Default to `height: auto` so a
// theme that sets an explicit height
// on images can't squish them. Inside
// a cropped gallery the gallery's own
// CSS controls the height instead.
style.height = 'auto';
}
return style;
} )() ),
Expand Down
165 changes: 165 additions & 0 deletions test/e2e/specs/editor/blocks/gallery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ test.use( {

test.describe( 'Gallery', () => {
let uploadedMedia;
let landscapeMedia;

test.beforeAll( async ( { requestUtils } ) => {
await requestUtils.deleteAllMedia();

uploadedMedia = await requestUtils.uploadMedia(
'./assets/10x10_e2e_test_image_z9T8jK.png'
);
landscapeMedia = await requestUtils.uploadMedia(
'./assets/200x150_e2e_test_image_opaque.png'
);
} );

test.afterEach( async ( { requestUtils } ) => {
Expand Down Expand Up @@ -400,6 +404,167 @@ test.describe( 'Gallery', () => {
);
expect( numbers ).not.toEqual( imageAltTexts );
} );

// Regression test for https://github.com/WordPress/gutenberg/issues/82252.
test( 'crops linked images to a uniform height in the editor canvas', async ( {
admin,
editor,
} ) => {
await admin.createNewPost();
await editor.insertBlock( {
name: 'core/gallery',
attributes: {
linkTo: 'media',
},
innerBlocks: [
{
name: 'core/image',
attributes: {
id: landscapeMedia.id,
url: landscapeMedia.source_url,
sizeSlug: 'full',
linkDestination: 'media',
href: landscapeMedia.source_url,
},
},
{
name: 'core/image',
attributes: {
id: uploadedMedia.id,
url: uploadedMedia.source_url,
sizeSlug: 'full',
linkDestination: 'media',
href: uploadedMedia.source_url,
},
},
],
} );

const images = editor.canvas.locator(
'role=document[name="Block: Gallery"i] >> role=img'
);
await expect( images ).toHaveCount( 2 );

// With cropping enabled, both images in the row render at the same
// height. Poll so the assertion waits for both images to finish
// loading and for the layout to settle.
await expect
.poll( async () => {
const landscapeBox = await images.nth( 0 ).boundingBox();
const squareBox = await images.nth( 1 ).boundingBox();
if ( ! landscapeBox || ! squareBox ) {
return Number.POSITIVE_INFINITY;
}
return Math.abs( landscapeBox.height - squareBox.height );
} )
.toBeLessThanOrEqual( 1 );
} );

test( 'does not crop images to a uniform height when cropping is disabled', async ( {
admin,
editor,
} ) => {
await admin.createNewPost();
await editor.insertBlock( {
name: 'core/gallery',
attributes: {
imageCrop: false,
},
innerBlocks: [
{
name: 'core/image',
attributes: {
id: landscapeMedia.id,
url: landscapeMedia.source_url,
sizeSlug: 'full',
},
},
{
name: 'core/image',
attributes: {
id: uploadedMedia.id,
url: uploadedMedia.source_url,
sizeSlug: 'full',
},
},
],
} );

const images = editor.canvas.locator(
'role=document[name="Block: Gallery"i] >> role=img'
);
await expect( images ).toHaveCount( 2 );

// With cropping disabled, each image renders at its natural aspect
// ratio, so the two images have different heights. Poll so the
// assertion waits for both images to finish loading.
await expect
.poll( async () => {
const landscapeBox = await images.nth( 0 ).boundingBox();
const squareBox = await images.nth( 1 ).boundingBox();
if ( ! landscapeBox || ! squareBox ) {
return 0;
}
return Math.abs( landscapeBox.height - squareBox.height );
} )
.toBeGreaterThan( 10 );
} );

// Regression test for the dynamic gallery preview, where the gallery's
// layout must be passed to `useBlockPreview` for the crop to apply.
test( 'crops images to a uniform height in a dynamic gallery', async ( {
admin,
editor,
requestUtils,
} ) => {
// A dynamic gallery resolves the images attached to the post being
// edited, so create a post and attach two different-aspect-ratio
// images to it.
const post = await requestUtils.createPost( {
title: 'Dynamic gallery',
status: 'draft',
} );
for ( const file of [
'./assets/200x150_e2e_test_image_opaque.png',
'./assets/10x10_e2e_test_image_z9T8jK.png',
] ) {
const media = await requestUtils.uploadMedia( file );
await requestUtils.rest( {
method: 'POST',
path: `/wp/v2/media/${ media.id }`,
data: { post: post.id },
} );
}

await admin.editPost( post.id );
await editor.insertBlock( {
name: 'core/gallery',
attributes: {
dynamicContent: { source: 'core/attached-media' },
linkTo: 'media',
},
} );

const images = editor.canvas.locator(
'[data-type="core/gallery"] img'
);
// Wait for the dynamic source to resolve and preview both images.
await expect( images ).toHaveCount( 2 );

// With cropping enabled, both previewed images render at the same
// height. Poll so the assertion waits for the images to load and the
// layout to settle.
await expect
.poll( async () => {
const firstBox = await images.nth( 0 ).boundingBox();
const secondBox = await images.nth( 1 ).boundingBox();
if ( ! firstBox || ! secondBox ) {
return Number.POSITIVE_INFINITY;
}
return Math.abs( firstBox.height - secondBox.height );
} )
.toBeLessThanOrEqual( 1 );
} );
} );

class GalleryBlockUtils {
Expand Down
Loading
Loading