diff --git a/docs/data/material/components/masonry/Sequential.js b/docs/data/material/components/masonry/Sequential.js new file mode 100644 index 00000000000000..be3d4731df91b6 --- /dev/null +++ b/docs/data/material/components/masonry/Sequential.js @@ -0,0 +1,36 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { styled } from '@mui/material/styles'; +import Paper from '@mui/material/Paper'; +import Masonry from '@mui/lab/Masonry'; + +const heights = [150, 30, 90, 70, 110, 150, 130, 80, 50, 90, 100, 150, 30, 50, 80]; + +const Item = styled(Paper)(({ theme }) => ({ + backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + ...theme.typography.body2, + padding: theme.spacing(0.5), + textAlign: 'center', + color: theme.palette.text.secondary, +})); + +export default function Sequential() { + return ( + + + {heights.map((height, index) => ( + + {index + 1} + + ))} + + + ); +} diff --git a/docs/data/material/components/masonry/Sequential.tsx b/docs/data/material/components/masonry/Sequential.tsx new file mode 100644 index 00000000000000..be3d4731df91b6 --- /dev/null +++ b/docs/data/material/components/masonry/Sequential.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { styled } from '@mui/material/styles'; +import Paper from '@mui/material/Paper'; +import Masonry from '@mui/lab/Masonry'; + +const heights = [150, 30, 90, 70, 110, 150, 130, 80, 50, 90, 100, 150, 30, 50, 80]; + +const Item = styled(Paper)(({ theme }) => ({ + backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + ...theme.typography.body2, + padding: theme.spacing(0.5), + textAlign: 'center', + color: theme.palette.text.secondary, +})); + +export default function Sequential() { + return ( + + + {heights.map((height, index) => ( + + {index + 1} + + ))} + + + ); +} diff --git a/docs/data/material/components/masonry/Sequential.tsx.preview b/docs/data/material/components/masonry/Sequential.tsx.preview new file mode 100644 index 00000000000000..ed3ecb3589951c --- /dev/null +++ b/docs/data/material/components/masonry/Sequential.tsx.preview @@ -0,0 +1,14 @@ + + {heights.map((height, index) => ( + + {index + 1} + + ))} + \ No newline at end of file diff --git a/docs/data/material/components/masonry/masonry.md b/docs/data/material/components/masonry/masonry.md index 70d1077aeab4df..333c439874680f 100644 --- a/docs/data/material/components/masonry/masonry.md +++ b/docs/data/material/components/masonry/masonry.md @@ -56,6 +56,13 @@ It is important to note that the value provided to the `spacing` prop is multipl {{"demo": "ResponsiveSpacing.js", "bg": true}} +## Sequential + +This example demonstrates the use of the `sequential` to configure the sequential order. +With `sequential` enabled, items are added in order from left to right rather than adding to the shortest column. + +{{"demo": "Sequential.js", "bg": true}} + ## Server-side rendering This example demonstrates the use of the `defaultHeight`, `defaultColumns` and `defaultSpacing`, which are used to diff --git a/docs/pages/material-ui/api/masonry.json b/docs/pages/material-ui/api/masonry.json index 7ecdffc5c33a81..a2c25831dbbb74 100644 --- a/docs/pages/material-ui/api/masonry.json +++ b/docs/pages/material-ui/api/masonry.json @@ -13,6 +13,7 @@ "defaultColumns": { "type": { "name": "number" } }, "defaultHeight": { "type": { "name": "number" } }, "defaultSpacing": { "type": { "name": "number" } }, + "sequential": { "type": { "name": "bool" }, "default": "false" }, "spacing": { "type": { "name": "union", diff --git a/docs/translations/api-docs/masonry/masonry.json b/docs/translations/api-docs/masonry/masonry.json index 1792b21244c28b..e4de79a0f03d80 100644 --- a/docs/translations/api-docs/masonry/masonry.json +++ b/docs/translations/api-docs/masonry/masonry.json @@ -16,6 +16,9 @@ "defaultSpacing": { "description": "The default spacing of the component. Like spacing, it is a factor of the theme's spacing. This is provided for server-side rendering." }, + "sequential": { + "description": "Allows using sequential order rather than adding to shortest column" + }, "spacing": { "description": "Defines the space between children. It is a factor of the theme's spacing." }, diff --git a/packages/mui-lab/src/Masonry/Masonry.d.ts b/packages/mui-lab/src/Masonry/Masonry.d.ts index 7f338898e34051..530c4666296ca7 100644 --- a/packages/mui-lab/src/Masonry/Masonry.d.ts +++ b/packages/mui-lab/src/Masonry/Masonry.d.ts @@ -34,6 +34,11 @@ export interface MasonryOwnProps { * @default 1 */ spacing?: ResponsiveStyleValue; + /** + * Allows using sequential order rather than adding to shortest column + * @default false + */ + sequential?: boolean; /** * Allows defining system overrides as well as additional CSS styles. */ diff --git a/packages/mui-lab/src/Masonry/Masonry.js b/packages/mui-lab/src/Masonry/Masonry.js index c8797005e813c1..7d05bac97615e4 100644 --- a/packages/mui-lab/src/Masonry/Masonry.js +++ b/packages/mui-lab/src/Masonry/Masonry.js @@ -182,6 +182,7 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { component = 'div', columns = 4, spacing = 1, + sequential = false, defaultColumns, defaultHeight, defaultSpacing, @@ -212,71 +213,84 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { const classes = useUtilityClasses(ownerState); - const handleResize = (masonryChildren) => { - if (!masonryRef.current || !masonryChildren || masonryChildren.length === 0) { - return; - } + const handleResize = React.useCallback( + (masonryChildren) => { + if (!masonryRef.current || !masonryChildren || masonryChildren.length === 0) { + return; + } - const masonry = masonryRef.current; - const masonryFirstChild = masonryRef.current.firstChild; - const parentWidth = masonry.clientWidth; - const firstChildWidth = masonryFirstChild.clientWidth; + const masonry = masonryRef.current; + const masonryFirstChild = masonryRef.current.firstChild; + const parentWidth = masonry.clientWidth; + const firstChildWidth = masonryFirstChild.clientWidth; - if (parentWidth === 0 || firstChildWidth === 0) { - return; - } + if (parentWidth === 0 || firstChildWidth === 0) { + return; + } - const firstChildComputedStyle = window.getComputedStyle(masonryFirstChild); - const firstChildMarginLeft = parseToNumber(firstChildComputedStyle.marginLeft); - const firstChildMarginRight = parseToNumber(firstChildComputedStyle.marginRight); + const firstChildComputedStyle = window.getComputedStyle(masonryFirstChild); + const firstChildMarginLeft = parseToNumber(firstChildComputedStyle.marginLeft); + const firstChildMarginRight = parseToNumber(firstChildComputedStyle.marginRight); - const currentNumberOfColumns = Math.round( - parentWidth / (firstChildWidth + firstChildMarginLeft + firstChildMarginRight), - ); + const currentNumberOfColumns = Math.round( + parentWidth / (firstChildWidth + firstChildMarginLeft + firstChildMarginRight), + ); - const columnHeights = new Array(currentNumberOfColumns).fill(0); - let skip = false; - masonry.childNodes.forEach((child) => { - if (child.nodeType !== Node.ELEMENT_NODE || child.dataset.class === 'line-break' || skip) { - return; - } - const childComputedStyle = window.getComputedStyle(child); - const childMarginTop = parseToNumber(childComputedStyle.marginTop); - const childMarginBottom = parseToNumber(childComputedStyle.marginBottom); - // if any one of children isn't rendered yet, masonry's height shouldn't be computed yet - const childHeight = parseToNumber(childComputedStyle.height) - ? Math.ceil(parseToNumber(childComputedStyle.height)) + childMarginTop + childMarginBottom - : 0; - if (childHeight === 0) { - skip = true; - return; - } - // if there is a nested image that isn't rendered yet, masonry's height shouldn't be computed yet - for (let i = 0; i < child.childNodes.length; i += 1) { - const nestedChild = child.childNodes[i]; - if (nestedChild.tagName === 'IMG' && nestedChild.clientHeight === 0) { + const columnHeights = new Array(currentNumberOfColumns).fill(0); + let skip = false; + let nextOrder = 1; + masonry.childNodes.forEach((child) => { + if (child.nodeType !== Node.ELEMENT_NODE || child.dataset.class === 'line-break' || skip) { + return; + } + const childComputedStyle = window.getComputedStyle(child); + const childMarginTop = parseToNumber(childComputedStyle.marginTop); + const childMarginBottom = parseToNumber(childComputedStyle.marginBottom); + // if any one of children isn't rendered yet, masonry's height shouldn't be computed yet + const childHeight = parseToNumber(childComputedStyle.height) + ? Math.ceil(parseToNumber(childComputedStyle.height)) + childMarginTop + childMarginBottom + : 0; + if (childHeight === 0) { skip = true; - break; + return; } - } + // if there is a nested image that isn't rendered yet, masonry's height shouldn't be computed yet + for (let i = 0; i < child.childNodes.length; i += 1) { + const nestedChild = child.childNodes[i]; + if (nestedChild.tagName === 'IMG' && nestedChild.clientHeight === 0) { + skip = true; + break; + } + } + if (!skip) { + if (sequential) { + columnHeights[nextOrder - 1] += childHeight; + child.style.order = nextOrder; + nextOrder += 1; + if (nextOrder > currentNumberOfColumns) { + nextOrder = 1; + } + } else { + // find the current shortest column (where the current item will be placed) + const currentMinColumnIndex = columnHeights.indexOf(Math.min(...columnHeights)); + columnHeights[currentMinColumnIndex] += childHeight; + const order = currentMinColumnIndex + 1; + child.style.order = order; + } + } + }); if (!skip) { - // find the current shortest column (where the current item will be placed) - const currentMinColumnIndex = columnHeights.indexOf(Math.min(...columnHeights)); - columnHeights[currentMinColumnIndex] += childHeight; - const order = currentMinColumnIndex + 1; - child.style.order = order; + // In React 18, state updates in a ResizeObserver's callback are happening after the paint which causes flickering + // when doing some visual updates in it. Using flushSync ensures that the dom will be painted after the states updates happen + // Related issue - https://github.com/facebook/react/issues/24331 + ReactDOM.flushSync(() => { + setMaxColumnHeight(Math.max(...columnHeights)); + setNumberOfLineBreaks(currentNumberOfColumns > 0 ? currentNumberOfColumns - 1 : 0); + }); } - }); - if (!skip) { - // In React 18, state updates in a ResizeObserver's callback are happening after the paint which causes flickering - // when doing some visual updates in it. Using flushSync ensures that the dom will be painted after the states updates happen - // Related issue - https://github.com/facebook/react/issues/24331 - ReactDOM.flushSync(() => { - setMaxColumnHeight(Math.max(...columnHeights)); - setNumberOfLineBreaks(currentNumberOfColumns > 0 ? currentNumberOfColumns - 1 : 0); - }); - } - }; + }, + [sequential], + ); useEnhancedEffect(() => { // IE and old browsers are not supported @@ -305,7 +319,7 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { resizeObserver.disconnect(); } }; - }, [columns, spacing, children]); + }, [columns, spacing, children, handleResize]); const handleRef = useForkRef(ref, masonryRef); @@ -375,6 +389,11 @@ Masonry.propTypes /* remove-proptypes */ = { * The default spacing of the component. Like `spacing`, it is a factor of the theme's spacing. This is provided for server-side rendering. */ defaultSpacing: PropTypes.number, + /** + * Allows using sequential order rather than adding to shortest column + * @default false + */ + sequential: PropTypes.bool, /** * Defines the space between children. It is a factor of the theme's spacing. * @default 1 diff --git a/packages/mui-lab/src/Masonry/Masonry.test.js b/packages/mui-lab/src/Masonry/Masonry.test.js index 7320e3aaaf49df..a4b98bcdd70117 100644 --- a/packages/mui-lab/src/Masonry/Masonry.test.js +++ b/packages/mui-lab/src/Masonry/Masonry.test.js @@ -369,4 +369,35 @@ describe('', () => { }); }); }); + + describe('prop: sequential', () => { + const pause = (timeout) => + new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, timeout); + }); + + it('should place children in sequential order', async function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + // only run on browser + this.skip(); + } + + const { getByTestId } = render( + +
+
+
+ , + ); + await pause(400); // Masonry elements aren't ordered immediately, and so we need the pause to wait for them to be ordered + const child1 = getByTestId('child1'); + const child2 = getByTestId('child2'); + const child3 = getByTestId('child3'); + expect(window.getComputedStyle(child1).order).to.equal(`1`); + expect(window.getComputedStyle(child2).order).to.equal(`2`); + expect(window.getComputedStyle(child3).order).to.equal(`1`); + }); + }); });