@boostdev/design-system-components 2.4.0 → 2.4.1
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 +70 -62
- package/dist/client.css +537 -537
- package/dist/client.js +70 -62
- package/dist/index.cjs +70 -62
- package/dist/index.css +537 -537
- package/dist/index.js +70 -62
- package/dist/web-components/index.js +2 -3
- package/package.json +1 -1
- package/src/components/layout/Grid/Grid.spec.tsx +23 -0
- package/src/components/layout/Grid/masonry.ts +50 -18
|
@@ -118,6 +118,29 @@ describe('applyMasonryLayout', () => {
|
|
|
118
118
|
expect(container.style.overflowAnchor).toBe('');
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
it('sets overflow-anchor: none on each child so the browser picks an anchor outside the grid', () => {
|
|
122
|
+
// `overflow-anchor` doesn't inherit — setting it only on the container leaves
|
|
123
|
+
// children anchor-eligible, so a child above the viewport being rewritten still
|
|
124
|
+
// drags the scroll position. Excluding each child forces the browser to anchor
|
|
125
|
+
// on content outside the grid.
|
|
126
|
+
const container = makeContainer(10);
|
|
127
|
+
const a = makeItem(100);
|
|
128
|
+
const b = makeItem(200);
|
|
129
|
+
container.append(a, b);
|
|
130
|
+
applyMasonryLayout(container, [a, b]);
|
|
131
|
+
expect(a.style.overflowAnchor).toBe('none');
|
|
132
|
+
expect(b.style.overflowAnchor).toBe('none');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('clearMasonryLayout restores overflow-anchor on children', () => {
|
|
136
|
+
const container = makeContainer(10);
|
|
137
|
+
const a = makeItem(100);
|
|
138
|
+
container.append(a);
|
|
139
|
+
applyMasonryLayout(container, [a]);
|
|
140
|
+
clearMasonryLayout(container, [a]);
|
|
141
|
+
expect(a.style.overflowAnchor).toBe('');
|
|
142
|
+
});
|
|
143
|
+
|
|
121
144
|
it('assigns grid-row-end: span ceil(height + targetGap) to each item', () => {
|
|
122
145
|
const container = makeContainer(10);
|
|
123
146
|
const a = makeItem(100);
|
|
@@ -113,24 +113,31 @@ export function applyMasonryLayout(
|
|
|
113
113
|
// `dense` + per-child `ResizeObserver` can fight the browser's scroll anchoring:
|
|
114
114
|
// a subpixel child resize (scrollbar toggling, late image decode, font swap) re-runs
|
|
115
115
|
// the pack, items above the viewport shift, and the browser yanks the viewport up
|
|
116
|
-
// to "follow" them.
|
|
117
|
-
//
|
|
118
|
-
//
|
|
116
|
+
// to "follow" them. `overflow-anchor: none` doesn't inherit, so setting it on the
|
|
117
|
+
// container alone doesn't exclude grid children from anchor selection — the browser
|
|
118
|
+
// will still pick one of the `<GridItem>`s as the anchor and drag the viewport when
|
|
119
|
+
// that item's row-span is rewritten. We set it on the container AND every child so
|
|
120
|
+
// the browser picks an anchor outside the grid entirely. Cleared in clearMasonryLayout.
|
|
119
121
|
container.style.overflowAnchor = 'none';
|
|
120
122
|
|
|
121
123
|
const children = rawChildren.filter(
|
|
122
124
|
(el) => el.nodeType === 1 && window.getComputedStyle(el).display !== 'none',
|
|
123
125
|
);
|
|
124
126
|
|
|
125
|
-
// Pass 1 —
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
127
|
+
// Pass 1 — force `align-self: start`: the default `stretch` would grow each item
|
|
128
|
+
// to fill the 1px track, making `offsetHeight` return 1 for every child. Also
|
|
129
|
+
// propagate `overflow-anchor: none` to each child (see container comment above).
|
|
130
|
+
//
|
|
131
|
+
// We intentionally do NOT clear `gridRow*` before measuring — with `align-self: start`,
|
|
132
|
+
// an item's `offsetHeight` is its natural content height regardless of the assigned
|
|
133
|
+
// span, so the stale span doesn't bias the measurement. Keeping the old span during
|
|
134
|
+
// the measurement phase means the container's block-size doesn't momentarily collapse
|
|
135
|
+
// (which is the other shape the scroll jump can take: grid height drops → document
|
|
136
|
+
// scrollHeight drops → browser clamps `scrollTop` → visible jump even with
|
|
137
|
+
// `overflow-anchor: none`).
|
|
129
138
|
for (const child of children) {
|
|
130
|
-
child.style.gridRow = '';
|
|
131
|
-
child.style.gridRowStart = '';
|
|
132
|
-
child.style.gridRowEnd = '';
|
|
133
139
|
child.style.alignSelf = 'start';
|
|
140
|
+
child.style.overflowAnchor = 'none';
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
// Preferred-column-span pass — optional. Assigns column spans to `data-column-span-auto`
|
|
@@ -222,6 +229,7 @@ export function clearMasonryLayout(container: HTMLElement, rawChildren: HTMLElem
|
|
|
222
229
|
child.style.gridRowStart = '';
|
|
223
230
|
child.style.gridRowEnd = '';
|
|
224
231
|
child.style.alignSelf = '';
|
|
232
|
+
child.style.overflowAnchor = '';
|
|
225
233
|
// Restore any `gridColumn` value the polyfill overwrote. Items the
|
|
226
234
|
// polyfill never touched have no ORIGINAL_COL_ATTR and are left alone.
|
|
227
235
|
restoreOriginalColumn(child);
|
|
@@ -251,31 +259,55 @@ export function useMasonry(
|
|
|
251
259
|
if (supportsNativeMasonry()) return;
|
|
252
260
|
|
|
253
261
|
let frame = 0;
|
|
254
|
-
const
|
|
262
|
+
const runNow = () => {
|
|
255
263
|
cancelAnimationFrame(frame);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
preferredColumnSpan,
|
|
259
|
-
});
|
|
264
|
+
applyMasonryLayout(container, Array.from(container.children) as HTMLElement[], {
|
|
265
|
+
preferredColumnSpan,
|
|
260
266
|
});
|
|
261
267
|
};
|
|
268
|
+
const schedule = () => {
|
|
269
|
+
cancelAnimationFrame(frame);
|
|
270
|
+
frame = requestAnimationFrame(runNow);
|
|
271
|
+
};
|
|
262
272
|
|
|
263
273
|
schedule();
|
|
264
274
|
|
|
275
|
+
// ResizeObserver fires during the "update the rendering" step, AFTER layout and
|
|
276
|
+
// BEFORE paint. Running the polyfill synchronously here means the paint that
|
|
277
|
+
// follows uses the freshly-computed row-spans — not the next frame's. If we
|
|
278
|
+
// defer to rAF instead, there's a visible frame where a grown item (e.g. a
|
|
279
|
+
// lazy-loaded image whose dimensions just resolved) overflows its stale cell
|
|
280
|
+
// and visually overlaps the next item in its column — easy to see on fast
|
|
281
|
+
// scroll through a long gallery.
|
|
265
282
|
const ro =
|
|
266
|
-
typeof ResizeObserver !== 'undefined' ? new ResizeObserver(
|
|
283
|
+
typeof ResizeObserver !== 'undefined' ? new ResizeObserver(runNow) : null;
|
|
284
|
+
const observeDescendants = (child: Element) => {
|
|
285
|
+
if (!ro) return;
|
|
286
|
+
ro.observe(child);
|
|
287
|
+
// Also observe media descendants directly. `loading="lazy"` images decode
|
|
288
|
+
// asynchronously; if the consumer hasn't reserved aspect ratio, the parent
|
|
289
|
+
// GridItem's size only changes once the image pushes it larger — by which
|
|
290
|
+
// point the next frame has painted with the stale cell. Observing the image
|
|
291
|
+
// itself means the polyfill re-runs in the same frame as the decode.
|
|
292
|
+
for (const media of Array.from(child.querySelectorAll('img, video'))) {
|
|
293
|
+
ro.observe(media);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
267
296
|
if (ro) {
|
|
268
297
|
// Observe each child's size, not the container — re-running when the container
|
|
269
298
|
// resizes causes a feedback loop (we set grid-auto-rows, which changes container
|
|
270
299
|
// block-size, which fires the observer). Child-size changes are what we actually
|
|
271
300
|
// need to respond to.
|
|
272
|
-
for (const child of Array.from(container.children))
|
|
301
|
+
for (const child of Array.from(container.children)) observeDescendants(child);
|
|
273
302
|
}
|
|
274
303
|
|
|
304
|
+
// MutationObserver fires as a microtask after DOM mutations — layout hasn't
|
|
305
|
+
// happened yet, so defer to rAF to let the browser lay out the new children
|
|
306
|
+
// before we measure them.
|
|
275
307
|
const mo =
|
|
276
308
|
typeof MutationObserver !== 'undefined'
|
|
277
309
|
? new MutationObserver(() => {
|
|
278
|
-
|
|
310
|
+
for (const child of Array.from(container.children)) observeDescendants(child);
|
|
279
311
|
schedule();
|
|
280
312
|
})
|
|
281
313
|
: null;
|