mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(studio): an element placed by a stylesheet translate stays put after a move or resize (#4734)
* fix(studio): an element placed by a stylesheet translate stays put after a drag or resize * fix(studio): an unset computed translate reads as nothing underneath * fix(studio): an element on the old offset channel keeps its folded translate masked too * test(studio): pin that a drag GSAP does not own leaves its translate unmasked * test(studio): the unmasked translate still carries the offset channel
This commit is contained in:
@@ -449,6 +449,62 @@ describe("GSAP-element drag — dot-a flies regressions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("a stylesheet translate after a committed GSAP drag", () => {
|
||||
function draggedTitle(css: string, oldOffsetChannel = false, withGsap = true) {
|
||||
const window = new Window();
|
||||
const style = window.document.createElement("style");
|
||||
style.textContent = css;
|
||||
window.document.head.append(style);
|
||||
const element = window.document.createElement("h1");
|
||||
element.id = "title";
|
||||
if (oldOffsetChannel) {
|
||||
element.setAttribute("data-hf-studio-path-offset", "true");
|
||||
element.style.setProperty(STUDIO_OFFSET_X_PROP, "30px");
|
||||
element.style.setProperty(STUDIO_OFFSET_Y_PROP, "0px");
|
||||
element.style.translate = `var(${STUDIO_OFFSET_X_PROP}, 0px) var(${STUDIO_OFFSET_Y_PROP}, 0px)`;
|
||||
}
|
||||
window.document.body.append(element);
|
||||
element.getBoundingClientRect = () => new window.DOMRect(10, 20, 100, 50);
|
||||
(window as unknown as { gsap: unknown }).gsap = withGsap && {
|
||||
set: (el: HTMLElement, vars: { x?: number; y?: number }) =>
|
||||
el.style.setProperty("transform", `translate(${vars.x ?? 0}px, ${vars.y ?? 0}px)`),
|
||||
getProperty: () => 0,
|
||||
};
|
||||
const result = createManualOffsetDragMember({
|
||||
key: "title",
|
||||
selection: { element } as never,
|
||||
element,
|
||||
rect: { left: 10, top: 20, width: 100, height: 50, editScaleX: 1, editScaleY: 1 },
|
||||
});
|
||||
if (!result.ok) throw new Error("member not created");
|
||||
applyManualOffsetDragCommit(result.member, 40, 30);
|
||||
if (withGsap) expect(element.style.getPropertyValue("translate")).toBe("none");
|
||||
endManualOffsetDragMembers([result.member]);
|
||||
return element;
|
||||
}
|
||||
|
||||
it("stays masked, so it is not applied on top of the committed transform", () => {
|
||||
expect(
|
||||
draggedTitle("#title { translate: 0 -200px; }").style.getPropertyValue("translate"),
|
||||
).toBe("none");
|
||||
});
|
||||
|
||||
it("stays masked for an element still on the old offset channel", () => {
|
||||
const element = draggedTitle("#title { translate: 0 -200px; }", true);
|
||||
expect(element.hasAttribute("data-hf-studio-path-offset")).toBe(false);
|
||||
expect(element.style.getPropertyValue("translate")).toBe("none");
|
||||
});
|
||||
|
||||
it("is not masked when GSAP does not own the position", () => {
|
||||
const element = draggedTitle("#title { translate: 0 -200px; }", true, false);
|
||||
expect(element.style.getPropertyValue("translate")).toContain(STUDIO_OFFSET_X_PROP);
|
||||
});
|
||||
|
||||
it("is not left behind when nothing sits under the draft's mask", () => {
|
||||
expect(draggedTitle("").style.getPropertyValue("translate")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resumeGsapTimelines", () => {
|
||||
it("unpauses exactly the timelines the drag start paused, then re-seeks the player", () => {
|
||||
const window = new Window();
|
||||
|
||||
@@ -525,6 +525,18 @@ export function restoreManualOffsetDragMembers(members: ManualOffsetDragMember[]
|
||||
}
|
||||
}
|
||||
|
||||
// Drop the draft's `translate: none` so the soft reload starts clean (#1673). When GSAP owns
|
||||
// the position it folded any translate into its transform, so one still showing applies twice.
|
||||
function settleDraftTranslate(element: HTMLElement, gsapOwnsPosition: boolean): void {
|
||||
if (element.style.getPropertyValue("translate") === "none") {
|
||||
element.style.removeProperty("translate");
|
||||
}
|
||||
if (!gsapOwnsPosition) return;
|
||||
const view = element.ownerDocument.defaultView;
|
||||
const underneath = view?.getComputedStyle(element).getPropertyValue("translate");
|
||||
if (underneath && underneath !== "none") element.style.setProperty("translate", "none");
|
||||
}
|
||||
|
||||
/** Teardown after a COMMITTED drag. */
|
||||
export function endManualOffsetDragMembers(members: ManualOffsetDragMember[]): void {
|
||||
for (const member of members) {
|
||||
@@ -533,23 +545,17 @@ export function endManualOffsetDragMembers(members: ManualOffsetDragMember[]): v
|
||||
member.element.removeAttribute("data-hf-drag-initial-offset-y");
|
||||
member.element.removeAttribute("data-hf-drag-gsap-base-x");
|
||||
member.element.removeAttribute("data-hf-drag-gsap-base-y");
|
||||
// Clear the draft's `translate: none` so the soft reload starts clean —
|
||||
// otherwise button-less pointermoves after the reload compute deltas
|
||||
// from a stale base and fling the element off-screen (#1673).
|
||||
// Do NOT clearProps:"transform" — that nukes the committed GSAP position
|
||||
// and causes a visual snap-back before the soft reload re-applies it.
|
||||
if (member.element.style.getPropertyValue("translate") === "none") {
|
||||
member.element.style.removeProperty("translate");
|
||||
}
|
||||
const gsapOwnsPosition = Boolean(getOffsetDragGsap(member.element));
|
||||
// Migration: when GSAP owns the position (the committed value lives in the
|
||||
// GSAP transform), the legacy `--hf-studio-offset` CSS channel is obsolete.
|
||||
// Clear it on the LIVE element — otherwise the leftover `translate:
|
||||
// var(--hf-studio-offset)` composes with the GSAP transform and the element
|
||||
// renders offset by the stale value until a full page reload (the source is
|
||||
// already stripped). clearStudioPathOffset leaves `transform` untouched.
|
||||
if (getOffsetDragGsap(member.element)) {
|
||||
clearStudioPathOffset(member.element);
|
||||
}
|
||||
if (gsapOwnsPosition) clearStudioPathOffset(member.element);
|
||||
settleDraftTranslate(member.element, gsapOwnsPosition);
|
||||
resumeGsapTimelines(member.element);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user