@boostdev/design-system-components 2.4.1 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.cjs +65 -53
- package/dist/client.css +537 -537
- package/dist/client.js +65 -53
- package/dist/index.cjs +65 -53
- package/dist/index.css +537 -537
- package/dist/index.js +65 -53
- package/dist/web-components/index.js +14 -2
- package/package.json +1 -1
- package/src/components/layout/Grid/Grid.spec.tsx +31 -0
- package/src/components/layout/Grid/masonry.ts +29 -2
|
@@ -196,6 +196,37 @@ describe('applyMasonryLayout', () => {
|
|
|
196
196
|
expect(b.style.alignSelf).toBe('start');
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
+
it('skips re-writing row-spans when measured heights match the cached value', () => {
|
|
200
|
+
// ResizeObserver fires freely on `<img>` decode in Firefox even when the
|
|
201
|
+
// parent's reserved aspect-ratio box hasn't actually changed. Without this
|
|
202
|
+
// short-circuit, every fire triggers a `grid-auto-flow: dense` re-pack —
|
|
203
|
+
// visible as image overlap during fast scroll because Firefox's compositor
|
|
204
|
+
// hasn't caught up with the newly shuffled item positions.
|
|
205
|
+
const container = makeContainer(10);
|
|
206
|
+
const item = makeItem(50);
|
|
207
|
+
container.append(item);
|
|
208
|
+
applyMasonryLayout(container, [item]);
|
|
209
|
+
expect(item.style.gridRowEnd).toBe('span 60');
|
|
210
|
+
|
|
211
|
+
// Simulate something else writing to gridRowEnd — if the polyfill re-runs
|
|
212
|
+
// unnecessarily, this would be overwritten.
|
|
213
|
+
item.style.gridRowEnd = 'span 999';
|
|
214
|
+
applyMasonryLayout(container, [item]);
|
|
215
|
+
expect(item.style.gridRowEnd).toBe('span 999');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('re-writes row-spans when measured heights change', () => {
|
|
219
|
+
const container = makeContainer(10);
|
|
220
|
+
const item = makeItem(50);
|
|
221
|
+
container.append(item);
|
|
222
|
+
applyMasonryLayout(container, [item]);
|
|
223
|
+
expect(item.style.gridRowEnd).toBe('span 60');
|
|
224
|
+
|
|
225
|
+
Object.defineProperty(item, 'offsetHeight', { configurable: true, value: 80 });
|
|
226
|
+
applyMasonryLayout(container, [item]);
|
|
227
|
+
expect(item.style.gridRowEnd).toBe('span 90');
|
|
228
|
+
});
|
|
229
|
+
|
|
199
230
|
it('clearMasonryLayout undoes container and child styles', () => {
|
|
200
231
|
const container = makeContainer(10);
|
|
201
232
|
const item = makeItem(50);
|
|
@@ -95,6 +95,16 @@ export type MasonryOptions = {
|
|
|
95
95
|
// items pinned to their polyfill-assigned span.
|
|
96
96
|
const ORIGINAL_COL_ATTR = 'data-bds-masonry-original-column';
|
|
97
97
|
|
|
98
|
+
// Data attribute caching the last applied row-span. Used to short-circuit no-op
|
|
99
|
+
// re-runs: when every child's newly-measured span matches the cached value, the
|
|
100
|
+
// polyfill skips all writes. This prevents `grid-auto-flow: dense` from
|
|
101
|
+
// re-packing on spurious ResizeObserver fires (subpixel fluctuations, `<img>`
|
|
102
|
+
// decode in Firefox where offsetHeight briefly wobbles). Dense re-packing
|
|
103
|
+
// during fast scroll in Firefox is visible as images overlapping, because
|
|
104
|
+
// Firefox's compositor paints cached tiles from the pre-shuffle layout on top
|
|
105
|
+
// of the newly laid out items for a frame or two.
|
|
106
|
+
const LAST_SPAN_ATTR = 'data-bds-masonry-span';
|
|
107
|
+
|
|
98
108
|
export function applyMasonryLayout(
|
|
99
109
|
container: HTMLElement,
|
|
100
110
|
rawChildren: HTMLElement[],
|
|
@@ -180,11 +190,27 @@ export function applyMasonryLayout(
|
|
|
180
190
|
// O(N) forced-reflow cost of a read-then-write loop.
|
|
181
191
|
const heights = children.map((child) => child.offsetHeight);
|
|
182
192
|
|
|
193
|
+
// Compute spans; skip the entire write pass if every span matches the
|
|
194
|
+
// previously-applied value. ResizeObserver fires freely in Firefox during
|
|
195
|
+
// image decode and scroll — without this guard, each fire triggers a dense
|
|
196
|
+
// re-pack that's visible as overlap on fast scroll (the compositor hasn't
|
|
197
|
+
// caught up with the newly shuffled positions).
|
|
198
|
+
const spans = heights.map((h) => Math.max(1, Math.ceil(h + targetGap)));
|
|
199
|
+
let changed = false;
|
|
200
|
+
for (let i = 0; i < children.length; i++) {
|
|
201
|
+
const prev = children[i].getAttribute(LAST_SPAN_ATTR);
|
|
202
|
+
if (prev !== String(spans[i])) {
|
|
203
|
+
changed = true;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!changed) return;
|
|
208
|
+
|
|
183
209
|
// Pass 3 — batch all row-span writes. No reads in between, so the browser can
|
|
184
210
|
// coalesce style invalidation and do layout once for the whole batch.
|
|
185
211
|
for (let i = 0; i < children.length; i++) {
|
|
186
|
-
|
|
187
|
-
children[i].
|
|
212
|
+
children[i].style.gridRowEnd = `span ${spans[i]}`;
|
|
213
|
+
children[i].setAttribute(LAST_SPAN_ATTR, String(spans[i]));
|
|
188
214
|
}
|
|
189
215
|
}
|
|
190
216
|
|
|
@@ -230,6 +256,7 @@ export function clearMasonryLayout(container: HTMLElement, rawChildren: HTMLElem
|
|
|
230
256
|
child.style.gridRowEnd = '';
|
|
231
257
|
child.style.alignSelf = '';
|
|
232
258
|
child.style.overflowAnchor = '';
|
|
259
|
+
child.removeAttribute(LAST_SPAN_ATTR);
|
|
233
260
|
// Restore any `gridColumn` value the polyfill overwrote. Items the
|
|
234
261
|
// polyfill never touched have no ORIGINAL_COL_ATTR and are left alone.
|
|
235
262
|
restoreOriginalColumn(child);
|