Skip to content

Commit

Permalink
[Masonry] Observe every masonry child to trigger computation when nee…
Browse files Browse the repository at this point in the history
…ded (mui#29896)
  • Loading branch information
hbjORbj authored and wladimirguerra committed Feb 2, 2022
1 parent ea1aeaf commit d094172
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Masonry from '@mui/lab/Masonry';
import {
Accordion,
AccordionDetails,
AccordionSummary,
Typography,
} from '@mui/material';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import { styled } from '@mui/material/styles';
import * as React from 'react';

const heights = [150, 30, 90, 70, 90, 100, 150, 30, 50, 80];

const Item = styled(Paper)(({ theme }) => ({
...theme.typography.body2,
color: theme.palette.text.secondary,
border: '1px solid black',
}));

export default function MasonryWithVariableHeightItems() {
return (
<Box sx={{ width: 500, minHeight: 377 }}>
<Masonry columns={3} spacing={1}>
{heights.map((height, index) => (
<Item key={index}>
<Accordion sx={{ minHeight: height }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Accordion {index + 1}</Typography>
</AccordionSummary>
<AccordionDetails>Contents</AccordionDetails>
</Accordion>
</Item>
))}
</Masonry>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Masonry from '@mui/lab/Masonry';
import {
Accordion,
AccordionDetails,
AccordionSummary,
Typography,
} from '@mui/material';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import { styled } from '@mui/material/styles';
import * as React from 'react';

const heights = [150, 30, 90, 70, 90, 100, 150, 30, 50, 80];

const Item = styled(Paper)(({ theme }) => ({
...theme.typography.body2,
color: theme.palette.text.secondary,
border: '1px solid black',
}));

export default function MasonryWithVariableHeightItems() {
return (
<Box sx={{ width: 500, minHeight: 377 }}>
<Masonry columns={3} spacing={1}>
{heights.map((height, index) => (
<Item key={index}>
<Accordion sx={{ minHeight: height }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Accordion {index + 1}</Typography>
</AccordionSummary>
<AccordionDetails>Contents</AccordionDetails>
</Accordion>
</Item>
))}
</Masonry>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Masonry columns={3} spacing={1}>
{heights.map((height, index) => (
<Item key={index}>
<Accordion sx={{ minHeight: height }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Accordion {index + 1}</Typography>
</AccordionSummary>
<AccordionDetails>Contents</AccordionDetails>
</Accordion>
</Item>
))}
</Masonry>
13 changes: 10 additions & 3 deletions docs/src/pages/components/masonry/masonry.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ githubLabel: 'component: Masonry'

# Masonry

<p class="description">Masonry lays out contents of different sizes as blocks of the same width and variable height with configurable gaps.</p>
<p class="description">Masonry lays out contents of varying dimensions as blocks of the same width and different height with configurable gaps.</p>

Masonry maintains a list of content blocks with a consistent width but variable height.
Masonry maintains a list of content blocks with a consistent width but different height.
The contents are ordered by row.
If a row is already filled with the specified number of columns, the next item starts another row, and it is added to the shortest column.
If a row is already filled with the specified number of columns, the next item starts another row, and it is added to the shortest column in order to optimize the use of space.

{{"component": "modules/components/ComponentLinkHeader.js", "design": false}}

Expand All @@ -27,6 +27,13 @@ If you'd like to order images by column, check out [ImageList](/components/image

{{"demo": "pages/components/masonry/ImageMasonry.js", "bg": true}}

## Items with variable height

This example demonstrates the use of `Masonry` for items with variable height.
Items can move to other columns in order to abide by the rule that items are always added to the shortest column and hence optimize the use of space.

{{"demo": "pages/components/masonry/MasonryWithVariableHeightItems.js", "bg": true}}

## Columns

This example demonstrates the use of the `columns` to configure the number of columns of a `Masonry`.
Expand Down
39 changes: 12 additions & 27 deletions packages/mui-lab/src/Masonry/Masonry.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,27 +182,16 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) {

const classes = useUtilityClasses(ownerState);

const handleResize = (elements) => {
if (!elements) {
const handleResize = (masonryChildren) => {
if (!masonryRef.current || !masonryChildren || masonryChildren.length === 0) {
return;
}
let masonry;
let masonryFirstChild;
let parentWidth;
let childWidth;
if (elements[0].target.className.includes(classes.root)) {
masonry = elements[0].target;
parentWidth = elements[0].contentRect.width;
masonryFirstChild = elements[1]?.target || masonry.firstChild;
childWidth = masonryFirstChild?.contentRect?.width || masonryFirstChild?.clientWidth || 0;
} else {
masonryFirstChild = elements[0].target;
childWidth = elements[0].contentRect.width;
masonry = elements[1]?.target || masonryFirstChild.parentElement;
parentWidth = masonry.contentRect?.width || masonry.clientWidth;
}
const masonry = masonryRef.current;
const masonryFirstChild = masonryRef.current.firstChild;
const parentWidth = masonry.clientWidth;
const firstChildWidth = masonryFirstChild.clientWidth;

if (parentWidth === 0 || childWidth === 0 || !masonry || !masonryFirstChild) {
if (parentWidth === 0 || firstChildWidth === 0) {
return;
}

Expand All @@ -211,7 +200,7 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) {
const firstChildMarginRight = parseToNumber(firstChildComputedStyle.marginRight);

const currentNumberOfColumns = Math.round(
parentWidth / (childWidth + firstChildMarginLeft + firstChildMarginRight),
parentWidth / (firstChildWidth + firstChildMarginLeft + firstChildMarginRight),
);

const columnHeights = new Array(currentNumberOfColumns).fill(0);
Expand Down Expand Up @@ -265,14 +254,10 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) {
return undefined;
}

const container = masonryRef.current;
if (container && resizeObserver) {
// only the masonry container and its first child are observed for resizing;
// this might cause unforeseen problems in some use cases;
resizeObserver.observe(container);
if (container.firstChild) {
resizeObserver.observe(container.firstChild);
}
if (masonryRef.current) {
masonryRef.current.childNodes.forEach((childNode) => {
resizeObserver.observe(childNode);
});
}
return () => (resizeObserver ? resizeObserver.disconnect() : {});
}, [columns, spacing, children]);
Expand Down
32 changes: 32 additions & 0 deletions packages/mui-lab/src/Masonry/Masonry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createRenderer, describeConformance } from 'test/utils';
import Masonry, { masonryClasses as classes } from '@mui/lab/Masonry';
import { expect } from 'chai';
import { createTheme } from '@mui/material/styles';
import defaultTheme from '@mui/material/styles/defaultTheme';
import { getStyle, parseToNumber } from './Masonry';

describe('<Masonry />', () => {
Expand Down Expand Up @@ -63,6 +64,37 @@ describe('<Masonry />', () => {
});
});

it('should re-compute the height of masonry when dimensions of any child change', function test() {
if (/jsdom/.test(window.navigator.userAgent)) {
// only run on browser
this.skip();
}
const spacingProp = 1;
const secondChildInitialHeight = 20;
const secondChildNewHeight = 10;

const { getByTestId } = render(
<Masonry columns={2} spacing={spacingProp} data-testid="container">
<div sx={{ height: 10 }} />
</Masonry>,
);
const masonry = getByTestId('container');
const secondItem = document.createElement('div');
secondItem.style.height = `${secondChildInitialHeight}px`;
masonry.appendChild(secondItem);

const topAndBottomMargin = parseToNumber(defaultTheme.spacing(spacingProp)) * 2;
expect(window.getComputedStyle(masonry).height).to.equal(
`${secondChildInitialHeight + topAndBottomMargin}px`,
);

secondItem.style.height = `${secondChildNewHeight}px`;

expect(window.getComputedStyle(masonry).height).to.equal(
`${secondChildNewHeight + topAndBottomMargin}px`,
);
});

it('should throw console error when children are empty', function test() {
if (!/jsdom/.test(window.navigator.userAgent)) {
this.skip();
Expand Down

0 comments on commit d094172

Please sign in to comment.