Skip to content

Commit

Permalink
[lab][Masonry] Ability to sort elements from left to right (#39904)
Browse files Browse the repository at this point in the history
  • Loading branch information
Rishi556 authored Feb 12, 2024
1 parent 8eee615 commit 2d43ad1
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 57 deletions.
36 changes: 36 additions & 0 deletions docs/data/material/components/masonry/Sequential.js
Original file line number Diff line number Diff line change
@@ -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 (
<Box sx={{ width: 500, minHeight: 393 }}>
<Masonry
columns={4}
spacing={2}
defaultHeight={450}
defaultColumns={4}
defaultSpacing={1}
sequential
>
{heights.map((height, index) => (
<Item key={index} sx={{ height }}>
{index + 1}
</Item>
))}
</Masonry>
</Box>
);
}
36 changes: 36 additions & 0 deletions docs/data/material/components/masonry/Sequential.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box sx={{ width: 500, minHeight: 393 }}>
<Masonry
columns={4}
spacing={2}
defaultHeight={450}
defaultColumns={4}
defaultSpacing={1}
sequential
>
{heights.map((height, index) => (
<Item key={index} sx={{ height }}>
{index + 1}
</Item>
))}
</Masonry>
</Box>
);
}
14 changes: 14 additions & 0 deletions docs/data/material/components/masonry/Sequential.tsx.preview
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Masonry
columns={4}
spacing={2}
defaultHeight={450}
defaultColumns={4}
defaultSpacing={1}
sequential
>
{heights.map((height, index) => (
<Item key={index} sx={{ height }}>
{index + 1}
</Item>
))}
</Masonry>
7 changes: 7 additions & 0 deletions docs/data/material/components/masonry/masonry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/pages/material-ui/api/masonry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions docs/translations/api-docs/masonry/masonry.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"defaultSpacing": {
"description": "The default spacing of the component. Like <code>spacing</code>, it is a factor of the theme&#39;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&#39;s spacing."
},
Expand Down
5 changes: 5 additions & 0 deletions packages/mui-lab/src/Masonry/Masonry.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export interface MasonryOwnProps {
* @default 1
*/
spacing?: ResponsiveStyleValue<number | string>;
/**
* Allows using sequential order rather than adding to shortest column
* @default false
*/
sequential?: boolean;
/**
* Allows defining system overrides as well as additional CSS styles.
*/
Expand Down
133 changes: 76 additions & 57 deletions packages/mui-lab/src/Masonry/Masonry.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) {
component = 'div',
columns = 4,
spacing = 1,
sequential = false,
defaultColumns,
defaultHeight,
defaultSpacing,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions packages/mui-lab/src/Masonry/Masonry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,4 +369,35 @@ describe('<Masonry />', () => {
});
});
});

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(
<Masonry columns={2} spacing={1} sequential>
<div style={{ height: `20px` }} data-testid="child1" />
<div style={{ height: `10px` }} data-testid="child2" />
<div style={{ height: `10px` }} data-testid="child3" />
</Masonry>,
);
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`);
});
});
});

0 comments on commit 2d43ad1

Please sign in to comment.