mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 02:36:10 +00:00
fix(studio): target one element when adding a keyframe at the playhead
"Add keyframe at playhead" on an element with no id authored the bare class
buildStableSelector hands back, so one add on a `.group` wrote
`tl.to(".group", ...)`: a tween that animates all five siblings and that
resolveSelectorElementIds reads back as all five, collapsing their timeline
rows into one. It survived a reload, so the written file stayed un-editable.
writeTargetSelector is the write-side counterpart to selectorFromSelection
(which must keep returning the exact string findTweenAtTime compares against).
It resolves the element's own identity to a selector that addresses exactly
one element: `#id`, else `[data-hf-id="..."]`, else the selection's selector
when it is already unique, else a `:nth-child` path anchored on the nearest
identifiable ancestor (the selector + selectorIndex pair, resolved through the
DOM the index was counted in).
Applied to the two paths that author a NEW tween: the no-animation branch of
useEnableKeyframes and commitKeyframeAtTimeImpl. replace-with-keyframes still
writes the selection's own selector, since retargeting a tween the author
aimed at a whole group is a different decision from adding a keyframe.
This commit is contained in:
@@ -466,3 +466,47 @@ describe("useEnableKeyframes — flat tween transaction", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("useEnableKeyframes — new tween on a class-only element", () => {
|
||||
it("targets the selected sibling alone, not every element sharing its class", async () => {
|
||||
window.location.hash = "#/project/test-project";
|
||||
usePlayerStore.setState({ currentTime: 1 });
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => ({ ok: true, json: async () => ({ animations: [] }) })),
|
||||
);
|
||||
const scene = document.body.appendChild(document.createElement("div"));
|
||||
scene.id = "scene";
|
||||
scene.innerHTML = '<i class="group"></i>'.repeat(5);
|
||||
const groups = Array.from(scene.querySelectorAll<HTMLElement>(".group"));
|
||||
const commitMutation = vi.fn(async () => undefined);
|
||||
const enable = renderEnableKeyframes({
|
||||
// A bare `.group` here writes `tl.to(".group", …)`, which animates all five
|
||||
// siblings and reads back as all five — one add wiped the timeline.
|
||||
domEditSelection: {
|
||||
selector: ".group",
|
||||
selectorIndex: 3,
|
||||
sourceFile: "index.html",
|
||||
element: groups[3],
|
||||
dataAttributes: { start: "0", duration: "2" },
|
||||
} as unknown as DomEditSelection,
|
||||
selectedGsapAnimations: [],
|
||||
previewIframeRef: {
|
||||
current: {
|
||||
contentWindow: { gsap: { getProperty: () => 7 } },
|
||||
} as unknown as HTMLIFrameElement,
|
||||
},
|
||||
handleGsapAddAnimation: vi.fn(),
|
||||
handleGsapConvertToKeyframes: vi.fn(),
|
||||
handleGsapRemoveKeyframe: vi.fn(),
|
||||
commitMutation,
|
||||
});
|
||||
|
||||
await act(async () => enable());
|
||||
|
||||
const mutation = commitMutation.mock.calls[0]?.[0] as { targetSelector: string };
|
||||
expect(mutation?.targetSelector).toBeTruthy();
|
||||
expect(document.querySelectorAll(mutation.targetSelector)).toHaveLength(1);
|
||||
expect(document.querySelector(mutation.targetSelector)).toBe(groups[3]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user