fix: prevent infinite resize loop in media grid Masonry layout (#2413)

- Credit to @lieblinger.

---------

Co-authored-by: Yannik Lieblinger <yannik@lieblinger.de>
This commit is contained in:
Dermot Duffy
2026-06-30 17:44:49 -07:00
committed by dermotduffy
co-authored by Yannik Lieblinger
parent ab683df5e8
commit 3f5a35c7f6
2 changed files with 15 additions and 11 deletions
+6 -2
View File
@@ -286,8 +286,12 @@ export class MediaGridController {
// Reset the column CSS sizes first. // Reset the column CSS sizes first.
this._setColumnSizeStyles(); this._setColumnSizeStyles();
// Need to recreate the masonry layout since the column width will differ. // Update the column width on the existing Masonry instance rather than
this._createMasonry(); // destroying and recreating it. A destroy resets the container height to
// 0, which can cause an ancestor scrollbar to appear/disappear, changing
// the available width and triggering an infinite resize oscillation.
this._masonry?.option?.({ columnWidth: this._getColumnSize() });
this._throttledLayout();
} }
} }
@@ -491,7 +491,7 @@ describe('MediaGridController', () => {
expect(masonry.layout).toBeCalled(); expect(masonry.layout).toBeCalled();
}); });
it('should re-create masonry when host size changes', () => { it('should update masonry column width when host size changes', () => {
const children = createChildren(); const children = createChildren();
const parent = createParent({ children: children }); const parent = createParent({ children: children });
createController(parent); createController(parent);
@@ -508,18 +508,16 @@ describe('MediaGridController', () => {
// Clear mock state. // Clear mock state.
vi.mocked(Masonry).mockClear(); vi.mocked(Masonry).mockClear();
vi.mocked(masonry.layout)?.mockClear(); vi.mocked(masonry.layout)?.mockClear();
vi.mocked(masonry.option)?.mockClear();
// Resize the host. // Resize the host.
setElementWidth(parent, 3000); setElementWidth(parent, 3000);
triggerResizeObserver('host'); triggerResizeObserver('host');
// Masonry should be reconstructed, styles set and layout called. // Masonry should not be recreated, but column width should be updated
expect(Masonry).toBeCalledWith( // via option() and layout should be called.
parent, expect(Masonry).not.toBeCalled();
expect.objectContaining({ expect(masonry.option).toBeCalledWith({ columnWidth: 599 });
columnWidth: 599,
}),
);
expect( expect(
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'), parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
).toBe('599px'); ).toBe('599px');
@@ -528,10 +526,12 @@ describe('MediaGridController', () => {
// Clear mock state. // Clear mock state.
vi.mocked(Masonry).mockClear(); vi.mocked(Masonry).mockClear();
vi.mocked(masonry.layout)?.mockClear(); vi.mocked(masonry.layout)?.mockClear();
vi.mocked(masonry.option)?.mockClear();
// Triger with the same sizes. // Trigger with the same sizes.
triggerResizeObserver('host'); triggerResizeObserver('host');
expect(Masonry).not.toBeCalled(); expect(Masonry).not.toBeCalled();
expect(masonry.option).not.toBeCalled();
expect(masonry.layout).not.toBeCalled(); expect(masonry.layout).not.toBeCalled();
}); });