From f7594b8beb63e669518cb5794052f1242d2db9e5 Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Tue, 9 Nov 2021 10:06:27 +0000 Subject: [PATCH] [Masonry] Check if container or child exists to prevent error (#29452) --- packages/mui-lab/src/Masonry/Masonry.js | 129 +++++++++++-------- packages/mui-lab/src/Masonry/Masonry.test.js | 19 +++ 2 files changed, 93 insertions(+), 55 deletions(-) diff --git a/packages/mui-lab/src/Masonry/Masonry.js b/packages/mui-lab/src/Masonry/Masonry.js index 73bc1e166df3ca..a50ea2ef6e4ff4 100644 --- a/packages/mui-lab/src/Masonry/Masonry.js +++ b/packages/mui-lab/src/Masonry/Masonry.js @@ -182,70 +182,91 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { const classes = useUtilityClasses(ownerState); - React.useEffect(() => { - const handleResize = () => { - const parentWidth = masonryRef.current.clientWidth; - const childWidth = masonryRef.current.firstChild.clientWidth; - const firstChildComputedStyle = window.getComputedStyle(masonryRef.current.firstChild); - const firstChildMarginLeft = parseToNumber(firstChildComputedStyle.marginLeft); - const firstChildMarginRight = parseToNumber(firstChildComputedStyle.marginRight); - - if (parentWidth === 0 || childWidth === 0) { - return; - } + const handleResize = (elements) => { + if (!elements) { + 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; + } + + if (parentWidth === 0 || childWidth === 0 || !masonry || !masonryFirstChild) { + return; + } - const currentNumberOfColumns = Math.round( - parentWidth / (childWidth + firstChildMarginLeft + firstChildMarginRight), - ); + const firstChildComputedStyle = window.getComputedStyle(masonryFirstChild); + const firstChildMarginLeft = parseToNumber(firstChildComputedStyle.marginLeft); + const firstChildMarginRight = parseToNumber(firstChildComputedStyle.marginRight); - const columnHeights = new Array(currentNumberOfColumns).fill(0); - let skip = false; - masonryRef.current.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) { + const currentNumberOfColumns = Math.round( + parentWidth / (childWidth + 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) { 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) { - skip = true; - break; - } + break; } - 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 (!skip) { - setMaxColumnHeight(Math.max(...columnHeights)); - const numOfLineBreaks = currentNumberOfColumns > 0 ? currentNumberOfColumns - 1 : 0; - setNumberOfLineBreaks(numOfLineBreaks); + // 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) { + setMaxColumnHeight(Math.max(...columnHeights)); + const numOfLineBreaks = currentNumberOfColumns > 0 ? currentNumberOfColumns - 1 : 0; + setNumberOfLineBreaks(numOfLineBreaks); + } + }; + + const observer = React.useRef( + typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(handleResize), + ); + React.useEffect(() => { + const resizeObserver = observer.current; // IE and old browsers are not supported - if (typeof ResizeObserver === 'undefined') { + if (resizeObserver === undefined) { return undefined; } - const resizeObserver = new ResizeObserver(handleResize); const container = masonryRef.current; - if (container) { + 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); @@ -253,9 +274,7 @@ const Masonry = React.forwardRef(function Masonry(inProps, ref) { resizeObserver.observe(container.firstChild); } } - return () => { - resizeObserver.disconnect(); - }; + return () => (resizeObserver ? resizeObserver.disconnect() : {}); }, [columns, spacing, children]); 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 af7b5bbd266972..4686ae62874d05 100644 --- a/packages/mui-lab/src/Masonry/Masonry.test.js +++ b/packages/mui-lab/src/Masonry/Masonry.test.js @@ -52,6 +52,25 @@ describe('', () => { width: `calc(${(100 / columns).toFixed(2)}% - ${theme.spacing(spacing)})`, }); }); + + it('should throw console error when children are empty', function test() { + if (!/jsdom/.test(window.navigator.userAgent)) { + this.skip(); + } + expect(() => render()).toErrorDev( + 'Warning: Failed prop type: The prop `children` is marked as required in `ForwardRef(Masonry)`, but its value is `undefined`.', + ); + }); + + it('should not throw type error when children are empty', function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + this.skip(); + } + expect(() => render()).toErrorDev( + 'Warning: Failed prop type: The prop `children` is marked as required in `ForwardRef(Masonry)`, but its value is `undefined`.', + ); + expect(() => render()).not.to.throw(new TypeError()); + }); }); describe('style attribute:', () => {