fix(studio): cover legacy resize boxes

This commit is contained in:
Miguel Ángel
2026-08-07 00:31:27 +00:00
parent a693b12cca
commit 12e637fb25
4 changed files with 44 additions and 17 deletions
@@ -360,15 +360,21 @@ function writeStudioBoxSizeVars(
element: HTMLElement, element: HTMLElement,
size: { width: number; height: number }, size: { width: number; height: number },
): void { ): void {
// Keep the measurement on its own migration-safe guard. Elements drafted by
// an older Studio can already carry the box-size marker without these newer
// attributes; the next resize still reaches this function before its width
// and height are overwritten, so this is the last honest layout box to save.
// Offset sizes are layout values, so a running scale animation does not
// distort them.
if (!element.hasAttribute(STUDIO_ORIGINAL_BOX_WIDTH_ATTR)) {
element.setAttribute(STUDIO_ORIGINAL_BOX_WIDTH_ATTR, String(element.offsetWidth));
}
if (!element.hasAttribute(STUDIO_ORIGINAL_BOX_HEIGHT_ATTR)) {
element.setAttribute(STUDIO_ORIGINAL_BOX_HEIGHT_ATTR, String(element.offsetHeight));
}
if (!element.hasAttribute(STUDIO_BOX_SIZE_ATTR)) { if (!element.hasAttribute(STUDIO_BOX_SIZE_ATTR)) {
element.setAttribute(STUDIO_ORIGINAL_WIDTH_ATTR, element.style.getPropertyValue("width")); element.setAttribute(STUDIO_ORIGINAL_WIDTH_ATTR, element.style.getPropertyValue("width"));
element.setAttribute(STUDIO_ORIGINAL_HEIGHT_ATTR, element.style.getPropertyValue("height")); element.setAttribute(STUDIO_ORIGINAL_HEIGHT_ATTR, element.style.getPropertyValue("height"));
// Measured here and only here: this branch runs once, before the draft
// writes a width, so it is the last moment the element still has the box
// the user started with. Offset sizes are layout, so a scale animation
// running on the element does not distort them.
element.setAttribute(STUDIO_ORIGINAL_BOX_WIDTH_ATTR, String(element.offsetWidth));
element.setAttribute(STUDIO_ORIGINAL_BOX_HEIGHT_ATTR, String(element.offsetHeight));
element.setAttribute( element.setAttribute(
STUDIO_ORIGINAL_MIN_WIDTH_ATTR, STUDIO_ORIGINAL_MIN_WIDTH_ATTR,
element.style.getPropertyValue("min-width"), element.style.getPropertyValue("min-width"),
@@ -267,6 +267,20 @@ describe("buildBoxSizePatches / buildClearBoxSizePatches", () => {
expect(ops.every((op) => op.value === null)).toBe(true); expect(ops.every((op) => op.value === null)).toBe(true);
}); });
it("backfills the measured box on a legacy element that already has the resize marker", () => {
const e = div();
e.setAttribute(STUDIO_BOX_SIZE_ATTR, "true");
Object.defineProperties(e, {
offsetWidth: { configurable: true, value: 630 },
offsetHeight: { configurable: true, value: 252 },
});
applyStudioBoxSize(e, { width: 320, height: 128 });
expect(e.getAttribute(STUDIO_ORIGINAL_BOX_WIDTH_ATTR)).toBe("630");
expect(e.getAttribute(STUDIO_ORIGINAL_BOX_HEIGHT_ATTR)).toBe("252");
});
it("build/clear symmetry: clear addresses every {type,property} key that build emits", () => { it("build/clear symmetry: clear addresses every {type,property} key that build emits", () => {
const e = populatedBoxEl(); const e = populatedBoxEl();
assertClearCoversKeys(buildBoxSizePatches(e), buildClearBoxSizePatches(e)); assertClearCoversKeys(buildBoxSizePatches(e), buildClearBoxSizePatches(e));
@@ -77,6 +77,21 @@ interface ResizeCase {
positionWrite?: "static-set" | "keyframed-tween" | "none"; positionWrite?: "static-set" | "keyframed-tween" | "none";
} }
function createResizeElement(testCase: ResizeCase): HTMLElement {
const el = document.createElement("div");
el.id = "clip";
if (testCase.inlineSized) {
el.setAttribute("data-hf-studio-original-width", `${testCase.box.w}px`);
el.setAttribute("data-hf-studio-original-height", `${testCase.box.h}px`);
} else {
el.setAttribute("data-hf-studio-original-box-width", `${testCase.box.w}`);
el.setAttribute("data-hf-studio-original-box-height", `${testCase.box.h}`);
el.setAttribute("data-hf-studio-original-width", "");
el.setAttribute("data-hf-studio-original-height", "");
}
return el;
}
function scaleTween(longhand: boolean): GsapAnimation { function scaleTween(longhand: boolean): GsapAnimation {
const at = (v: number) => (longhand ? { scaleX: v, scaleY: v } : { scale: v }); const at = (v: number) => (longhand ? { scaleX: v, scaleY: v } : { scale: v });
return { return {
@@ -180,17 +195,7 @@ function persistedScale(calls: unknown[][]): { x: number; y: number } | null {
async function runCase(testCase: ResizeCase) { async function runCase(testCase: ResizeCase) {
const rotation = testCase.rotation ?? 0; const rotation = testCase.rotation ?? 0;
const el = document.createElement("div"); const el = createResizeElement(testCase);
el.id = "clip";
if (testCase.inlineSized) {
el.setAttribute("data-hf-studio-original-width", `${testCase.box.w}px`);
el.setAttribute("data-hf-studio-original-height", `${testCase.box.h}px`);
} else {
el.setAttribute("data-hf-studio-original-box-width", `${testCase.box.w}`);
el.setAttribute("data-hf-studio-original-box-height", `${testCase.box.h}`);
el.setAttribute("data-hf-studio-original-width", "");
el.setAttribute("data-hf-studio-original-height", "");
}
// What the gesture stamps at drag start, and the draft it leaves applied. // What the gesture stamps at drag start, and the draft it leaves applied.
el.setAttribute("data-hf-drag-gsap-base-x", `${testCase.base.x}`); el.setAttribute("data-hf-drag-gsap-base-x", `${testCase.base.x}`);
el.setAttribute("data-hf-drag-gsap-base-y", `${testCase.base.y}`); el.setAttribute("data-hf-drag-gsap-base-y", `${testCase.base.y}`);
@@ -293,6 +293,7 @@ it("non-uniform drag commits scaleX/scaleY longhands", async () => {
* left it rendering at 7532px, over three times where it was dropped, and the * left it rendering at 7532px, over three times where it was dropped, and the
* next drag compounded it. * next drag compounded it.
*/ */
// fallow-ignore-next-line code-duplication
it("scales from the element's real box, not a hardcoded fallback", async () => { it("scales from the element's real box, not a hardcoded fallback", async () => {
const el = document.createElement("div"); const el = document.createElement("div");
el.id = "clip"; el.id = "clip";
@@ -339,6 +340,7 @@ it("scales from the element's real box, not a hardcoded fallback", async () => {
* and the longhands won. The resize computed the right number, wrote it, and * and the longhands won. The resize computed the right number, wrote it, and
* the element snapped straight back to its old size on release. * the element snapped straight back to its old size on release.
*/ */
// fallow-ignore-next-line code-duplication
it("does not mix the scale shorthand into a tween that speaks longhands", async () => { it("does not mix the scale shorthand into a tween that speaks longhands", async () => {
const el = document.createElement("div"); const el = document.createElement("div");
el.id = "clip"; el.id = "clip";