-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
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
[Masonry] Implement Masonry using Flexbox #28059
Conversation
ab6c06e
to
ec9b82a
Compare
We might want to give it more time for developers to test the Masonry (not release yet) in #28059. For instance:
|
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.
Can't say much to the implementation. Just some general question and code nits.
3af341e
to
75006ec
Compare
0b90478
to
14e3341
Compare
@oliviertassinari Sounds good! I agree that we need feedback from users. @mnajdova @mbrookes @eps1lon I just wanted to point out that I resolved this limitation This could be our advantage because most flexbox-based masonry libraries out there (e.g., https://github.com/paulcollett/react-masonry-css, https://github.com/gilbitron/flexmasonry) lack this property, and this becomes problematic especially when items' heights have a wide range because masonry looks unbalanced/broken in such case (ex: https://codepen.io/sldisek783/pen/OJgNgbg). Anyways, the only known limitation of this implementation compared to the current one using CSS Grid is the column spanning option. Preview: https://deploy-preview-28059--material-ui.netlify.app/components/masonry/ |
d4bfae6
to
df21ffd
Compare
df21ffd
to
7ac8250
Compare
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 general, I find the flexbox approach much more sensible than the grid with gazillion rows :)
import PropTypes from 'prop-types'; | ||
import clsx from 'clsx'; | ||
import { unstable_composeClasses as composeClasses } from '@mui/core'; | ||
import { styled, useThemeProps } from '@mui/material/styles'; | ||
import { | ||
createUnarySpacing, | ||
getValue, | ||
handleBreakpoints, |
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.
(not related to this PR) This is the first time I encounter these functions and I can't figure out by their names what they are supposed to do. @mnajdova how would you feel about giving them more meaningful names?
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.
Open to ideas, maybe applyBreakpoints
is better name :) Let's just make sure it is not a public API before renaming
@siriwatknp Yep, I am working on it. :) |
e5dfb93
to
cf44711
Compare
cf44711
to
976db5a
Compare
const container = masonryRef.current; | ||
container.addEventListener('scroll', handleScroll); | ||
resizeObserver.observe(container); | ||
// Observing window in addition to masonry container is more stable |
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.
"is more stable" compared to what and in what regard is it more stable?
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.
The use case here is that the masonry's dimensions may not change even if user resizes the window; in case of responsive columns
/spacing
, when window resizes, @media query responsive values can kick in and configure new width
/margin
. However, because resize observer doesn't fire any event, computation is not re-run and hence causes a situation like below (CSS expects 5 columns while computation of new order
for each item and height
for masonry is still for 4 columns). I will add a comment above the code!
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.
@media query responsive values can kick in and configure new width/margin. However, because resize observer doesn't fire any event,
I haven't tested this but reading the MDN page naively, ResizeObserver should fire an event when a media query changes the dimensions of an element:
reports changes to the dimensions of an Element's content or border box
-- https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
Could you provide a codesandbox that shows that media queries affecting the content or border box will not trigger ResizeObserver?
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.
@eps1lon I think this may be because a media query changes the dimensions of items inside the masonry rather than the container. If this is the reason, maybe it's a better idea to observe the first child of masonry than to observe the window? Or, my initial idea itself is wrong.
This is a demo prior to adding a listener to the window:
https://codesandbox.io/s/imagemasonry-material-demo-forked-ck9x0
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 may be because a media query changes the dimensions of items inside the masonry rather than the container. If this is the reason, maybe it's a better idea to observe the first child of masonry than to observe the window?
This sounds like it would also not catch resizes of relevant elements by other means. I'm not sure if this can or cannot happen but the code should clearly document its limitation: It's not about ResizeObserver not firing on media query changes but rather the code not observing all relevant elements.
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 see. So, I think I will replace the window listener with the first child (I just tested and bug is not reproducible this way too) and add a comment saying that we only observe the container and the first child.
const container = masonryRef.current;
// only the masonry container and its first child are observed for resizing;
// this might cause unforeseen problems in some use cases;
resizeObserver.observe(container);
resizeObserver.observe(container?.firstChild);
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.
Or, do you think we should add every child to the observer?
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 don't know enough about the implementation to make that call. How expensive is running the observer callback? How often would that work be wasted when run on every child?
Documenting limitations is a normal thing to do. It's more important to know about limitations than actually fixing them right away.
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 don't know enough about the implementation to make that call. How expensive is running the observer callback? How often would that work be wasted when run on every child?
Since we don't know how many items will be passed, observing every child can be very expensive. Also, since we don't yet know any edge case where this limitation is causing a bug, I'd say observing the container and the first child is enough for now.
Documenting limitations is a normal thing to do. It's more important to know about limitations than actually fixing them right away.
Thanks, I am glad to learn that :)
columns={4} | ||
spacing={1} | ||
defaultHeight={450} | ||
defaultColumns={4} |
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.
Why do we need both columns
and defaultColumns
?
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.
Because SSR can support only column value of number type while columns
support both types of responsive value and number. Only with columns
, we don't support users who want to have responsive values of columns
/spacing
but also want to support SSR. I can take some value out of given responsive values of columns
/spacing
and use it for SSR, but I think this adds unnecessary complexity and user documentation.
} | ||
// if there is a nested image that isn't rendered yet, masonry's height shouldn't be computed yet | ||
child.childNodes.forEach((nestedChild) => { | ||
// eslint-disable-next-line |
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 this eslint rule is here for a reason. So you'd have to either use the good old for
loop, or (perhaps even preferably now that I think of it) the some
method of the array.
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 prefer some()
method but I need to do Array.from
to do that, which contradicts our initial purpose of this change, so I will go with the old for loop here!
0d54459
to
9341378
Compare
9341378
to
55affcb
Compare
c449262
to
8463cd2
Compare
1f6522e
to
016703c
Compare
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.
👌
Closes #27934
The current implementation of
Masonry
relies on CSS Grid. It fails to render all items on Chrome if masonry's height goes beyond 2,000px. This is because Chrome limits the number of grid rows to 1000 at the maximum. Overflowing items beyond the 1000th row are simply squashed.1. Solution
2. Requirements of
Masonry
A. The items can have arbitrary heights. [Must-have]
B. The next item is pushed to the shortest column. [Must-have]
C. The items should be ordered by row. [Must-have]
D. The number of columns
cols
and the spacing between itemsspacing
can be passed into the component. [Should-have]E. Responsive number of columns (breakpoints) [Nice-to-have]
F. Server side rendering [Nice-to-have]
G. Performance [Nice-to-have]
H. Logical tab order [Should-have]
I. Support for lazy loading/virtualisation [Nice-to-have]
2. Integrity of this implementation
A+
B- (Update: addressed in this commit: dae7e0e and hence +)
C+
D+
E+
F+
G+
H+
I+
B might be able to be resolved as @oliviertassinari said
It is worth mentioning that the column spanning of the current
Masonry
will no longer be supported.