@@ -523,6 +560,16 @@ describe('the changed element exits (spec:AST-105 FR4)', () => {
expect(content.style.pointerEvents).toBe('');
});
+ it('guesses, then corrects, where a negative margin would collapse with its end margin', () => {
+ const {content, after, layout} = exitSetUp();
+ after.style.marginTop = '-4px';
+ const reads = vi.mocked(after.getBoundingClientRect);
+ reads.mockClear();
+ expect(holdLayoutExit(layout, content, vi.fn())).toBe(true);
+ expect(reads.mock.calls.length).toBeGreaterThan(2);
+ expect(content.style.getPropertyValue('margin-block-end')).toBe('-88px');
+ });
+
it('stops an exit when the content opens again', () => {
const {content, layout} = exitSetUp();
const done = vi.fn();
@@ -545,7 +592,8 @@ describe('the changed element exits (spec:AST-105 FR4)', () => {
});
it('lands at once where no end margin frees its space (a grid track)', () => {
- const {content, after, layout} = exitSetUp();
+ const {area, content, after, layout} = exitSetUp();
+ area.style.display = 'grid';
// The row after it ignores the margin: it moves only when the content
// is hidden.
vi.spyOn(after, 'getBoundingClientRect').mockImplementation(
@@ -567,7 +615,9 @@ describe('the changed element exits (spec:AST-105 FR4)', () => {
});
it("a refused hold keeps the element's own end margin and lets the content after it take its place at once", async () => {
- const {content, after, layout} = exitSetUp();
+ const {area, content, after, layout} = exitSetUp();
+ // A grid track, which no end margin frees.
+ area.style.display = 'grid';
content.style.setProperty('margin-block-end', '12px');
vi.spyOn(after, 'getBoundingClientRect').mockImplementation(
() =>
diff --git a/packages/core/src/motion/animatedLayout.ts b/packages/core/src/motion/animatedLayout.ts
index eb352ea370ec9..4d49e98787003 100644
--- a/packages/core/src/motion/animatedLayout.ts
+++ b/packages/core/src/motion/animatedLayout.ts
@@ -728,6 +728,8 @@ function flush(batch: Batch): void {
after?: DOMRect;
rawX?: number;
rawY?: number;
+ isMovedByApp?: boolean;
+ scale?: {x: number; y: number};
}[] = [];
for (const element of followers) {
const before = batch.before.get(element);
@@ -756,6 +758,9 @@ function flush(batch: Batch): void {
}
entries.push({element, before, carried, carriedMs});
}
+ // Every reading (places, transforms, scales) before any glide starts: a
+ // glide started between two readings would make the next one lay the page
+ // out again, once per element (on a long page, the whole cost).
const byElement = new Map
();
// An ancestor of the change, and a sibling before it, move by their layout
// change alone: when scroll anchoring re-scrolls the area, an outer
@@ -781,6 +786,13 @@ function flush(batch: Batch): void {
entry.rawY = isAncestor ? nearer(rawY, scrolledY) : rawY;
byElement.set(entry.element, entry);
}
+ for (const entry of entries) {
+ const after = entry.after as DOMRect;
+ if (intersects(entry.before, visible) || intersects(after, visible)) {
+ entry.isMovedByApp = movedByApp(entry.element);
+ entry.scale = scaleOf(entry.element, after);
+ }
+ }
for (const entry of entries) {
const {element, before, carried, carriedMs} = entry;
const after = entry.after as DOMRect;
@@ -814,14 +826,14 @@ function flush(batch: Batch): void {
});
continue;
}
- if (movedByApp(element)) {
+ if (entry.isMovedByApp === true) {
// The app moves it itself: it takes its final place at once
// (`spec:AST-105` Coverage).
continue;
}
// Where it shows now, against its new place, in its own px; the part the
// carried glide does not cover is the new move.
- const scale = scaleOf(element, after);
+ const scale = entry.scale ?? {x: 1, y: 1};
const now = carried(0);
const moveX = dx / scale.x - now.x;
const moveY = dy / scale.y - now.y;
@@ -1093,6 +1105,9 @@ function giveUpSpace(element: HTMLElement, area: Element): boolean {
? null
: nextInFlow(element.parentElement, area);
const checked = afterParent == null ? [next] : [next, afterParent];
+ // The element's scale, read in the layout the caller's visibility check
+ // already laid out.
+ const scale = scaleOf(element, element.getBoundingClientRect());
// Where the content after it goes once it is hidden, read with it taken
// out of flow (not `display: none`, which would restart the CSS animations
// inside it).
@@ -1106,14 +1121,14 @@ function giveUpSpace(element: HTMLElement, area: Element): boolean {
} else {
element.style.setProperty('position', position, positionPriority);
}
+ // The reading is in screen px, the margin in the element's own: under a
+ // scaled ancestor a distance is divided by the scale on this axis.
+ const perPx = isRow ? scale.x : scale.y;
// A first guess (its box), then one correction from the measured result:
// a gap, a collapsed margin or the element's own margins.
element.style.setProperty(property, `${-size}px`);
const guess = placeOf(next, rtl)[axis];
- // The reading is in screen px, the margin in the element's own: under a
- // scaled ancestor the correction is divided by the scale on this axis.
- const scale = scaleOf(element, element.getBoundingClientRect());
- const correction = (target[axis] - guess) / (isRow ? scale.x : scale.y);
+ const correction = (target[axis] - guess) / perPx;
element.style.setProperty(property, `${-size + correction}px`);
// Everything checked must land on both axes: a flex-row item that sets
// its row's height frees no block space with an inline margin.