-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Try using responsive images in dataview grid layouts #70493
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Warning: Type of PR label mismatch To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.
Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task. |
media?.media_details?.sizes?.thumbnail?.source_url || media?.source_url; | ||
const url = view ? media?.source_url : thumbnailURL; | ||
|
||
// @ts-ignore layout exists on ViewGrid; not sure what the problem is here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like it's because only ViewGrid has the previewSize
prop.
I guess something like this would work:
const columnNumberSetting =
'grid' === view.type ? view.layout?.previewSize || 0 : 0;
Flaky tests detected in 29cc5de. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/16614506144
|
I've updated this to have all dataview layouts pass a "sizes" string to the mediaField. That way, no field has to be responsible for knowing images sizes per breakpoint, and these can change within the dataviews package without causing breaking changes. It also means new layout types will work out of the box with any media fields that provide responsive image markup. |
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. |
sizes = `(max-width: 480px) 100vw, (max-width: 782px) ${ | ||
100 / usedPreviewSize | ||
}vw, calc( (100vw - 400px) / ${ usedPreviewSize } )`; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where do these calculations come from?
What happens for grid items other than images?
Why can't we use a resize observer, and then calculate sizes from that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where do these calculations come from?
The image widths at each breakpoint are based on the layout grid CSS and take the UI into account as mentioned in the comments above.
What happens for grid items other than images?
Nothing 😄 this functionality only exists for img
tags.
Why can't we use a resize observer, and then calculate sizes from that?
In principle I think it's better to avoid throwing a ton of JS at a problem that's been solved in native HTML already. Also, if we have to wait for the resize observer (which means waiting for layout to happen on screen) it's too late for the browser to fetch the right image at that point - either it has to load a backup image first (adding unnecessary downloads) or it'll be slow to appear on screen which is also not an optimal experience.
}: GridItemProps< Item > ) { | ||
const { showTitle = true, showMedia = true, showDescription = true } = view; | ||
const hasBulkAction = useHasAPossibleBulkAction( actions, item ); | ||
const id = getItemId( item ); | ||
const instanceId = useInstanceId( GridItem ); | ||
const isSelected = selection.includes( id ); | ||
const renderedMediaField = mediaField?.render ? ( | ||
<mediaField.render item={ item } /> | ||
<mediaField.render item={ item } sizes={ sizes } /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for looking into this.
One advantage of using this approach instead of passing the view object is that we can use information that may be private to the view to calculate the size.
One downside is that we need to figure out is how to support any kind of media field (or the most common ones). That I know of, I think we've used images and iframes as media fields. Perhaps video would be useful as well, though I'm not convinced. We'd need to understand what each of them needs, so we can support them.
To discriminate among the different types of media fields, we need to evolve the field types available. For example, introducing image, iframe, etc. field types. That could signal which props to use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One downside is that we need to figure out is how to support any kind of media field (or the most common ones). That I know of, I think we've used images and iframes as media fields. Perhaps video would be useful as well, though I'm not convinced. We'd need to understand what each of them needs, so we can support them.
Afaik the HTML spec only provides tools for serving images responsively, so the "sizes" attribute won't be of use to other media types. I think it's fine to implement this behaviour only for images though. Other media fields can just ignore the attribute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps video would be useful as well, though I'm not convinced. We'd need to understand what each of them needs, so we can support them.
I was thinking about this for the admin redesign, and was wondering about the possibility of a single, generic media props object. Example:
interface MediaFieldProps {
sizes?: string; // for images
aspectRatio?: string; // for videos, iframes
maxWidth?: string | number; // for any media type
maxHeight?: string | number;
objectFit?: 'cover' | 'contain' | 'fill'; // for any media type
}
I think it'd scale better for different media types, e.g., can be expanded without breaking changes.
Then the media field render func can destructure only the props it needs.
It's also self-documenting through its TS interface
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think the idea of making it more generic is a good one and it'll be more flexible. Maybe it could be called mediaAppearance
.
I also personally prefer more specific types like VideoAppearance
/ ImageAppearance
/ BlockPreviewAppearance
, as then the user can't specify an invalid combination of props, but this discussion has also come up on other PRs and there are mixed opinions about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that could work!
Size Change: -148 B (-0.01%) Total Size: 1.91 MB
ℹ️ View Unchanged
|
packages/dataviews/src/types.ts
Outdated
@@ -193,6 +193,7 @@ export type DataFormControlProps< Item > = { | |||
|
|||
export type DataViewRenderFieldProps< Item > = { | |||
item: Item; | |||
sizes?: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is worth adding a comment here as an explainer? I see the other types are documented.
}: GridItemProps< Item > ) { | ||
const { showTitle = true, showMedia = true, showDescription = true } = view; | ||
const hasBulkAction = useHasAPossibleBulkAction( actions, item ); | ||
const id = getItemId( item ); | ||
const instanceId = useInstanceId( GridItem ); | ||
const isSelected = selection.includes( id ); | ||
const renderedMediaField = mediaField?.render ? ( | ||
<mediaField.render item={ item } /> | ||
<mediaField.render item={ item } sizes={ sizes } /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps video would be useful as well, though I'm not convinced. We'd need to understand what each of them needs, so we can support them.
I was thinking about this for the admin redesign, and was wondering about the possibility of a single, generic media props object. Example:
interface MediaFieldProps {
sizes?: string; // for images
aspectRatio?: string; // for videos, iframes
maxWidth?: string | number; // for any media type
maxHeight?: string | number;
objectFit?: 'cover' | 'contain' | 'fill'; // for any media type
}
I think it'd scale better for different media types, e.g., can be expanded without breaking changes.
Then the media field render func can destructure only the props it needs.
It's also self-documenting through its TS interface
@@ -117,27 +117,15 @@ | |||
} | |||
|
|||
.dataviews-view-grid.dataviews-view-grid { | |||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is creating a regression in the default width of the images.
The default value of usedPreviewSize
is undefined
, but if you change it using the slider, you can't get back to the default state.
Kapture.2025-06-26.at.11.50.33.mp4
I guess it's no big deal, but it's odd not to be able to return to the original width as loaded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm yeah I'll look into it.
I guess we could try changing usedPreviewSize
to work with min-widths instead of number of columns, or we could just unset the value if it equates to default
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we could try changing usedPreviewSize to work with min-widths instead of number of columns
I just tried this and although it drastically simplifies the code I don't much like the experience of the un-stepped range control, because results feel a bit arbitrary. I'll play with this a bit further to see if I can keep the columns logic while using image widths in the background.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok this is now fixed and I've been able to retain most of the drastic simplification 😄
b000498
to
c482754
Compare
className="dataviews-wrapper" | ||
ref={ useMergeRefs( [ containerRef, resizeObserverRef ] ) } | ||
> | ||
<div className="dataviews-wrapper" ref={ containerRef }> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that containerRef
needs to be on this wrapper because its use is related to the scrolling behaviour of the wrapper.
The fact that this is the scrollable container for the layout is what makes it unsuitable to use with the resizeObserver, because the presence of a scrollbar will throw off the size of that actual layout. This is why I'm moving resizeObserverRef to context so it can be used directly in the grid layout.
const usedPreviewSize = view.layout?.previewSize; | ||
|
||
// Calculate possible media sizes in grid for responsive images. | ||
let sizes = '400px'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm now wondering whether it's really worth switching the value of sizes for different grid sizes, because it means that changing the grid size in the UI can download a whole new set of images 😅
We could just go with a fixed maximum size of 900px here, because with grid-template-columns
using auto-fill
, even the biggest grid size on a really big screen won't go over that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good rationale 👍
aria-busy={ isLoading } | ||
ref={ resizeObserverRef } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned above, we need this ref here so DataViews can set the containerWidth in its context to the actual grid width, instead of the width of its wrapper that may or may not include scrollbars.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Also I changed this to use a div
instead of Grid
so I could pass it the ref)
}, | ||
{ | ||
value: 430, | ||
breakpoint: 588, // at minimum image width, 2 images display at this container size |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This tries to emulate the current behaviour, where setting the grid at its biggest size on a small container and resizing the window preserves the biggest size setting, even when more size options appear. This is probably not too important a point in user-facing terms though, because people don't go around resizing windows that much 😄
@container (max-width: 480px) { | ||
padding-left: $grid-unit-30; | ||
padding-right: $grid-unit-30; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't changed this (only moved it around in the file) but this padding logic could probably be improved to be fully independent of sidebar width (which is specific to the site editor) and gutenberg media queries breakpoints.
<mediaField.render | ||
item={ item } | ||
field={ mediaField } | ||
mediaAppearance={ mediaAppearance } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest for me, right now, this is way too advanced/complex for a small gain. I'd rather pass the "view" object here as a "temporary API". I can't imagine this "mediaAppearance" API sticking forever either.
I would like to understand better what other kind of "rendering config" we need per field before having a more formal/complex API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But now, that I think about itrender
is also called on DataForm
which doesn't have the concept of "view". So my question here, what would be the simplest common denominator thing we could pass here that allow us to achieve the use-case without complexity. For instance I want to avoid resize observers as much as possible personally, can we instead having
config={ config }
with the following type
interface config {
size?: "small" | "medium" | "large"
}
and each view could decide to pass the right "size" as needed.
- For instance the "media" field in table would always pass "small"
- The "media" field in grid would always pass "medium"
- DataForm would probably pass "small" too
I realize this is close to what you're proposing but there are two changes:
- Simpler because there's no need for resize observers
- More generic because it's not just about "media", later we might want other kind of "rendering config" for fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simpler because there's no need for resize observers
The simpler config won't fix the need for resize observer, because that's already being used to build the grid layout. I just moved it to a different element because it isn't giving accurate numbers in its current location (the layout wrapper).
I did simplify the grid logic as much as I could so it's less dependent on breakpoints, but unfortunately we do still need to know the container size in order to display the right options in the preview size picker.
interface config {
size?: "small" | "medium" | "large"
}
small
, medium
, large
won't really work here because the responsive image logic requires actual numeric values for its sizes
attribute. The layout is responsible for setting the image size, so we should get that size from it. Otherwise we're putting the burden on the consumer to figure out what those sizes mean in px and produce a suitable number themselves, which might break if we ever change the layout.
We can call the object something more generic than mediaAppearance
; it could be config
or something else. But we do need those numeric widths for the responsive images to work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated to a generic config
with a size
property that takes a string with the size in px.
c8fe2d1
to
0237139
Compare
Update: I rebased and tested this against the Grouped Grid Layout that has since been merged and it seems to be working as expected. |
0237139
to
dfcc42d
Compare
.join( ', ' ) | ||
: undefined | ||
} | ||
sizes={ config?.size || '100vw' } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the usage we expect for this prop, I'd probably call it config.sizes
, like the img attribute name — even if for now we just want to pass the target source size.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated!
@@ -30,6 +32,17 @@ export const FeaturedImageView = ( { | |||
className="fields-controls__featured-image-image" | |||
src={ url } | |||
alt="" | |||
srcSet={ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the Edit function for the FeaturedImage field we could also use this technique to prevent the full image being requested for such small needs 24px (note the quick edit button for media):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I totally missed the quick edit experiment! I think I'd rather address that in a follow-up though, because it's not clear what the best approach might be.
Arguably, we could just pass in the thumbnail URL directly instead of the full image, because FeaturedImageEdit
defines its own image size. Whether we do that or use srcset
and sizes
, there's a side-effect we need to be aware of: the image will no longer show at its true aspect ratio, but in a square crop.
An alternative that preserves the aspect ratio is to use the medium
size URL if it exists and default to full if not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, the public API changes in this PR (adding a config.sizes
prop to field's render function) looks good to me.
Each layout provides a fixed target size for the image, and the field decides what to do with that info. I've tested that this works by setting a feature image (600kb) to a page and verifying that:
- only the smallest image available from the srcset is downloaded in list and table layouts (150x150 in my case)
- a larger image, but not the largest, is downloaded for the grid
so that looks good and it's working as expected.
I haven't been able to review today the other internal changes to resize observer, but I understand they're unrelated and aim to fix a bug.
Thanks for reviewing, André!
The changes to resize observer are actually related to the responsive image feature: it's a bug that the current grid logic doesn't use the correct size, but these changes also ensure that the max image size never goes over 900px. For clarity, I wasn't aiming specifically for that number; it just happens to be the max size that the images can get to after I adjusted the grid to be more responsive at super-wide screen sizes. What matters is that we can know what the max size is, so we can use it in the responsive image logic. |
}, | ||
{ | ||
value: 350, | ||
breakpoint: 1636, // at minimum image width, 6 images display at this container size |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tried this on a large screen (2560px), way past the max breakpoint (1636). With these changes, the images aren't that big and the space looks more balanced. Of course, I'm not designer, so others may have different thoughts 😅
In any case, I like that the mapping from a minimum breakpoint to a minimum image size is clearer and easier to tweak.
Before | After | |
---|---|---|
Min width (6 images before, 8 after) | ||
Max width (3 images before, 5 after) |
: {}; | ||
const usedPreviewSize = view.layout?.previewSize; | ||
// This is the maximum width that an image can achieve in the grid. | ||
const size = '900px'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a gift to our future selves, would you expand why this is the maximum width an image can achieve in the grid?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure!
I would love CHANGELOG and README updates about this PR. |
* Try using responsive images in dataview grid layouts * Move determination of sizes to dataviews * Simplify grid logic and sizes * use range of image widths for grid size picker * Make picker discrete and improve breakpoints * remove unused prop * ref no worky again * smol improvement * Update to use an object instead of sizes string and fix grid size at max * Adjust table max img width * Change `mediaAppearance` to the more generic `config` * Change `size` to `sizes` * Explain magic number in comment.
Sorry folks, I forgot about the props in the commit message 🤦 |
Why?
I noticed that the
FeaturedImageView
component in dataviews (seewp-admin/site-editor.php?p=%2Fpage
) loads the full-sized image regardless of whether it's in list, table or grid mode. This means an unsuspecting user with multiple pages with featured images is potentially loading several megabytes of images even if they're displaying as tiny thumbnails.This PR
mediaAppearance
object prop to MediaField, with amaxImageWidth
attribute, so we can pass in an appropriate image width from each layout type;maxImageWidth
;resizeObserverRef
to dataviews context instead of passing it to the layout wrapper, so that it can be passed directly to each layout container as needed (currently only the grid layout needs it, and observing the container gives us a more accurate size than its wrapper, which might include a scrollbar)FeaturedImageView
to use themaxImageWidth
prop from MediaField to determine the appropriate size of image to serve, usingsrcset
andsizes
attributes on theimg
tag.Testing Instructions
wp-admin/site-editor.php?p=%2Fpage
Testing Instructions for Keyboard
Screenshots or screencast