From d09417215244b70498518ce8abdbbf057f3c1620 Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Fri, 14 Jan 2022 16:17:49 +0000 Subject: [PATCH] [Masonry] Observe every masonry child to trigger computation when needed (#29896) --- .../masonry/MasonryWithVariableHeightItems.js | 39 +++++++++++++++++++ .../MasonryWithVariableHeightItems.tsx | 39 +++++++++++++++++++ ...MasonryWithVariableHeightItems.tsx.preview | 12 ++++++ docs/src/pages/components/masonry/masonry.md | 13 +++++-- packages/mui-lab/src/Masonry/Masonry.js | 39 ++++++------------- packages/mui-lab/src/Masonry/Masonry.test.js | 32 +++++++++++++++ 6 files changed, 144 insertions(+), 30 deletions(-) create mode 100644 docs/src/pages/components/masonry/MasonryWithVariableHeightItems.js create mode 100644 docs/src/pages/components/masonry/MasonryWithVariableHeightItems.tsx create mode 100644 docs/src/pages/components/masonry/MasonryWithVariableHeightItems.tsx.preview diff --git a/docs/src/pages/components/masonry/MasonryWithVariableHeightItems.js b/docs/src/pages/components/masonry/MasonryWithVariableHeightItems.js new file mode 100644 index 00000000000000..e430258d2a990a --- /dev/null +++ b/docs/src/pages/components/masonry/MasonryWithVariableHeightItems.js @@ -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 ( + + + {heights.map((height, index) => ( + + + }> + Accordion {index + 1} + + Contents + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/MasonryWithVariableHeightItems.tsx b/docs/src/pages/components/masonry/MasonryWithVariableHeightItems.tsx new file mode 100644 index 00000000000000..e430258d2a990a --- /dev/null +++ b/docs/src/pages/components/masonry/MasonryWithVariableHeightItems.tsx @@ -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 ( + + + {heights.map((height, index) => ( + + + }> + Accordion {index + 1} + + Contents + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/MasonryWithVariableHeightItems.tsx.preview b/docs/src/pages/components/masonry/MasonryWithVariableHeightItems.tsx.preview new file mode 100644 index 00000000000000..b6d62d78dda74a --- /dev/null +++ b/docs/src/pages/components/masonry/MasonryWithVariableHeightItems.tsx.preview @@ -0,0 +1,12 @@ + + {heights.map((height, index) => ( + + + }> + Accordion {index + 1} + + Contents + + + ))} + \ No newline at end of file diff --git a/docs/src/pages/components/masonry/masonry.md b/docs/src/pages/components/masonry/masonry.md index ee7f53af4397d3..563e9e8ba48bd1 100644 --- a/docs/src/pages/components/masonry/masonry.md +++ b/docs/src/pages/components/masonry/masonry.md @@ -6,11 +6,11 @@ githubLabel: 'component: Masonry' # Masonry -

Masonry lays out contents of different sizes as blocks of the same width and variable height with configurable gaps.

+

Masonry lays out contents of varying dimensions as blocks of the same width and different height with configurable gaps.

-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}} @@ -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`. diff --git a/packages/mui-lab/src/Masonry/Masonry.js b/packages/mui-lab/src/Masonry/Masonry.js index dd01b859654ba2..e0fb0a3d0e30c0 100644 --- a/packages/mui-lab/src/Masonry/Masonry.js +++ b/packages/mui-lab/src/Masonry/Masonry.js @@ -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; } @@ -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); @@ -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]); diff --git a/packages/mui-lab/src/Masonry/Masonry.test.js b/packages/mui-lab/src/Masonry/Masonry.test.js index e516efaa4a1d02..661936f826b750 100644 --- a/packages/mui-lab/src/Masonry/Masonry.test.js +++ b/packages/mui-lab/src/Masonry/Masonry.test.js @@ -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('', () => { @@ -63,6 +64,37 @@ describe('', () => { }); }); + 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( + +
+ , + ); + 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();