From 7d063046bb51ccec7c1b140181afdcf49f9460f4 Mon Sep 17 00:00:00 2001 From: Rishi Panthee Date: Fri, 17 Nov 2023 12:52:38 -0600 Subject: [PATCH 1/7] add sequential masonry --- .../material/components/masonry/Sequential.js | 36 +++++++++++++++++++ .../components/masonry/Sequential.tsx | 36 +++++++++++++++++++ .../material/components/masonry/masonry.md | 7 ++++ docs/pages/material-ui/api/masonry.json | 1 + .../api-docs/masonry/masonry.json | 3 ++ packages/mui-lab/src/Masonry/Masonry.d.ts | 5 +++ packages/mui-lab/src/Masonry/Masonry.js | 26 +++++++++++--- 7 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 docs/data/material/components/masonry/Sequential.js create mode 100644 docs/data/material/components/masonry/Sequential.tsx 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/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 32d1e38045a963..0ab336ce016293 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 c48e32772f9170..8beb1397d6f63e 100644 --- a/packages/mui-lab/src/Masonry/Masonry.js +++ b/packages/mui-lab/src/Masonry/Masonry.js @@ -181,6 +181,7 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { component = 'div', columns = 4, spacing = 1, + sequential = false, defaultColumns, defaultHeight, defaultSpacing, @@ -235,6 +236,7 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { 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; @@ -259,11 +261,20 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { } } 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; + 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) { @@ -374,6 +385,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 From 4b70da5278f3df1d593a891bce7610592aa8e2a6 Mon Sep 17 00:00:00 2001 From: Rishi Panthee Date: Fri, 24 Nov 2023 08:20:40 -0600 Subject: [PATCH 2/7] Add tests and fix linter issues --- .../components/masonry/Sequential.tsx.preview | 14 ++ packages/mui-lab/src/Masonry/Masonry.js | 134 +++++++++--------- packages/mui-lab/src/Masonry/Masonry.test.js | 31 ++++ 3 files changed, 112 insertions(+), 67 deletions(-) create mode 100644 docs/data/material/components/masonry/Sequential.tsx.preview 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/packages/mui-lab/src/Masonry/Masonry.js b/packages/mui-lab/src/Masonry/Masonry.js index 8beb1397d6f63e..50b642fef620fe 100644 --- a/packages/mui-lab/src/Masonry/Masonry.js +++ b/packages/mui-lab/src/Masonry/Masonry.js @@ -212,83 +212,83 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { const classes = useUtilityClasses(ownerState); - const handleResize = (masonryChildren) => { - if (!masonryRef.current || !masonryChildren || masonryChildren.length === 0) { - return; - } + useEnhancedEffect(() => { + const handleResize = (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; - 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; - 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 (!skip) { - if (sequential) { - columnHeights[nextOrder - 1] += childHeight; - child.style.order = nextOrder; - nextOrder += 1; - if (nextOrder > currentNumberOfColumns) { - nextOrder = 1; + // 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; } - } 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) { - // 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); + }); + } + }; - useEnhancedEffect(() => { // IE and old browsers are not supported if (typeof ResizeObserver === 'undefined') { return undefined; @@ -315,7 +315,7 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { resizeObserver.disconnect(); } }; - }, [columns, spacing, children]); + }, [columns, spacing, children, sequential]); const handleRef = useForkRef(ref, masonryRef); diff --git a/packages/mui-lab/src/Masonry/Masonry.test.js b/packages/mui-lab/src/Masonry/Masonry.test.js index 7320e3aaaf49df..fa4475c9822dad 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', () => { + it('should place children in sequential order', function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + // only run on browser + this.skip(); + } + const firstChildHeight = 20; + const secondChildHeight = 10; + const thirdChildHeight = 10; + + const { getByTestId } = render( + +
+
+
+ , + ); + const masonry = getByTestId('sequential'); + + expect(window.getComputedStyle(masonry).height).to.equal( + `${firstChildHeight + thirdChildHeight}px`, + ); + + expect(window.getComputedStyle(masonry.children[0]).style).to.equal(`order: 1;`); + + expect(window.getComputedStyle(masonry.children[1]).style).to.equal(`order: 2;`); + + expect(window.getComputedStyle(masonry.children[2]).style).to.equal(`order: 1;`); + }); + }); }); From 788effb1d384fd473280c9a8459d0abcd839f3c0 Mon Sep 17 00:00:00 2001 From: Rishi Panthee Date: Fri, 24 Nov 2023 09:31:18 -0600 Subject: [PATCH 3/7] Update sequential tests --- packages/mui-lab/src/Masonry/Masonry.test.js | 25 ++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/mui-lab/src/Masonry/Masonry.test.js b/packages/mui-lab/src/Masonry/Masonry.test.js index fa4475c9822dad..b22e99921b2c33 100644 --- a/packages/mui-lab/src/Masonry/Masonry.test.js +++ b/packages/mui-lab/src/Masonry/Masonry.test.js @@ -381,23 +381,24 @@ describe('', () => { const thirdChildHeight = 10; const { getByTestId } = render( - -
-
-
+ +
+
+
, ); - const masonry = getByTestId('sequential'); - - expect(window.getComputedStyle(masonry).height).to.equal( - `${firstChildHeight + thirdChildHeight}px`, - ); - expect(window.getComputedStyle(masonry.children[0]).style).to.equal(`order: 1;`); + expect(getByTestId('child1')).toHaveComputedStyle({ + order: '1', + }); - expect(window.getComputedStyle(masonry.children[1]).style).to.equal(`order: 2;`); + expect(getByTestId('child2')).toHaveComputedStyle({ + order: '2', + }); - expect(window.getComputedStyle(masonry.children[2]).style).to.equal(`order: 1;`); + expect(getByTestId('child3')).toHaveComputedStyle({ + order: '1', + }); }); }); }); From e03cb445691985c2176aa3696250ce9237bbed77 Mon Sep 17 00:00:00 2001 From: Rishi Panthee Date: Sun, 17 Dec 2023 01:49:37 -0600 Subject: [PATCH 4/7] use height for testing --- packages/mui-lab/src/Masonry/Masonry.test.js | 25 +++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/mui-lab/src/Masonry/Masonry.test.js b/packages/mui-lab/src/Masonry/Masonry.test.js index b22e99921b2c33..67dc0928d10e13 100644 --- a/packages/mui-lab/src/Masonry/Masonry.test.js +++ b/packages/mui-lab/src/Masonry/Masonry.test.js @@ -376,29 +376,26 @@ describe('', () => { // only run on browser this.skip(); } + + const spacingProp = 1; const firstChildHeight = 20; const secondChildHeight = 10; const thirdChildHeight = 10; const { getByTestId } = render( - -
-
-
+ +
+
+
, ); - expect(getByTestId('child1')).toHaveComputedStyle({ - order: '1', - }); - - expect(getByTestId('child2')).toHaveComputedStyle({ - order: '2', - }); + const masonry = getByTestId('container'); + const topAndBottomMargin = parseToNumber(defaultTheme.spacing(spacingProp)) * 2; - expect(getByTestId('child3')).toHaveComputedStyle({ - order: '1', - }); + expect(window.getComputedStyle(masonry).height).to.equal( + `${firstChildHeight + thirdChildHeight + topAndBottomMargin}px`, + ); }); }); }); From 144fefb7b803b6c81b3b490dd68a57dc42083a61 Mon Sep 17 00:00:00 2001 From: Rishi Panthee Date: Wed, 3 Jan 2024 10:54:12 -0600 Subject: [PATCH 5/7] save --- packages/mui-lab/src/Masonry/Masonry.test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/mui-lab/src/Masonry/Masonry.test.js b/packages/mui-lab/src/Masonry/Masonry.test.js index 67dc0928d10e13..d2d5ab902b6bd6 100644 --- a/packages/mui-lab/src/Masonry/Masonry.test.js +++ b/packages/mui-lab/src/Masonry/Masonry.test.js @@ -377,13 +377,12 @@ describe('', () => { this.skip(); } - const spacingProp = 1; const firstChildHeight = 20; const secondChildHeight = 10; const thirdChildHeight = 10; const { getByTestId } = render( - +
@@ -391,10 +390,9 @@ describe('', () => { ); const masonry = getByTestId('container'); - const topAndBottomMargin = parseToNumber(defaultTheme.spacing(spacingProp)) * 2; expect(window.getComputedStyle(masonry).height).to.equal( - `${firstChildHeight + thirdChildHeight + topAndBottomMargin}px`, + `${firstChildHeight + thirdChildHeight}px`, ); }); }); From b16cea5ad8f27cd1b56118ea7a72ce500a12930a Mon Sep 17 00:00:00 2001 From: Rishi Panthee Date: Wed, 10 Jan 2024 09:07:08 -0600 Subject: [PATCH 6/7] Fix tests --- packages/mui-lab/src/Masonry/Masonry.test.js | 34 +++++++++++--------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/mui-lab/src/Masonry/Masonry.test.js b/packages/mui-lab/src/Masonry/Masonry.test.js index d2d5ab902b6bd6..a4b98bcdd70117 100644 --- a/packages/mui-lab/src/Masonry/Masonry.test.js +++ b/packages/mui-lab/src/Masonry/Masonry.test.js @@ -371,29 +371,33 @@ describe('', () => { }); describe('prop: sequential', () => { - it('should place children in sequential order', function test() { + 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 firstChildHeight = 20; - const secondChildHeight = 10; - const thirdChildHeight = 10; - const { getByTestId } = render( - -
-
-
+ +
+
+
, ); - - const masonry = getByTestId('container'); - - expect(window.getComputedStyle(masonry).height).to.equal( - `${firstChildHeight + thirdChildHeight}px`, - ); + 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`); }); }); }); From f449700393402cb25216405048574ca3bb86094b Mon Sep 17 00:00:00 2001 From: Rishi Panthee Date: Tue, 23 Jan 2024 13:47:48 -0600 Subject: [PATCH 7/7] move handleResize out of effect --- packages/mui-lab/src/Masonry/Masonry.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/mui-lab/src/Masonry/Masonry.js b/packages/mui-lab/src/Masonry/Masonry.js index 50b642fef620fe..daf0207701bf4f 100644 --- a/packages/mui-lab/src/Masonry/Masonry.js +++ b/packages/mui-lab/src/Masonry/Masonry.js @@ -212,8 +212,8 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { const classes = useUtilityClasses(ownerState); - useEnhancedEffect(() => { - const handleResize = (masonryChildren) => { + const handleResize = React.useCallback( + (masonryChildren) => { if (!masonryRef.current || !masonryChildren || masonryChildren.length === 0) { return; } @@ -287,8 +287,11 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { setNumberOfLineBreaks(currentNumberOfColumns > 0 ? currentNumberOfColumns - 1 : 0); }); } - }; + }, + [sequential], + ); + useEnhancedEffect(() => { // IE and old browsers are not supported if (typeof ResizeObserver === 'undefined') { return undefined; @@ -315,7 +318,7 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { resizeObserver.disconnect(); } }; - }, [columns, spacing, children, sequential]); + }, [columns, spacing, children, handleResize]); const handleRef = useForkRef(ref, masonryRef);