Skip to content

Commit

Permalink
[Masonry] Check if container or child exists to prevent error (mui#29452
Browse files Browse the repository at this point in the history
)
  • Loading branch information
hbjORbj authored Nov 9, 2021
1 parent 5c79f3d commit f7594b8
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 55 deletions.
129 changes: 74 additions & 55 deletions packages/mui-lab/src/Masonry/Masonry.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,80 +182,99 @@ 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);
if (container.firstChild) {
resizeObserver.observe(container.firstChild);
}
}
return () => {
resizeObserver.disconnect();
};
return () => (resizeObserver ? resizeObserver.disconnect() : {});
}, [columns, spacing, children]);

const handleRef = useForkRef(ref, masonryRef);
Expand Down
19 changes: 19 additions & 0 deletions packages/mui-lab/src/Masonry/Masonry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,25 @@ describe('<Masonry />', () => {
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(<Masonry columns={3} spacing={1} />)).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(<Masonry columns={3} spacing={1} />)).toErrorDev(
'Warning: Failed prop type: The prop `children` is marked as required in `ForwardRef(Masonry)`, but its value is `undefined`.',
);
expect(() => render(<Masonry columns={3} spacing={1} />)).not.to.throw(new TypeError());
});
});

describe('style attribute:', () => {
Expand Down

0 comments on commit f7594b8

Please sign in to comment.