mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio): keep a remounted clip marked active when it stays at the playhead
The structural refresh now force-syncs data-active on freshly queried clip nodes instead of diffing. A clip that changes lanes on a reorder remounts as a new DOM node; if it stayed under the playhead the diff skipped it and it rendered as inactive.
This commit is contained in:
@@ -78,6 +78,26 @@ describe("updateTimelineActiveClipClasses", () => {
|
||||
expect(previous).toEqual(new Set());
|
||||
});
|
||||
|
||||
it("re-applies data-active to a fresh DOM node that stayed active across a re-render", () => {
|
||||
// A clip that moves lanes on a reorder remounts as a new element. It stays
|
||||
// in the previous active set, so the plain diff would skip it and leave the
|
||||
// new node without data-active. syncAll must force the attribute on.
|
||||
const container = document.createElement("div");
|
||||
appendClip(container, "hero", "0", "5");
|
||||
const previous = new Set<string>();
|
||||
updateTimelineActiveClipClasses(container, previous, 2);
|
||||
expect(previous).toEqual(new Set(["hero"]));
|
||||
|
||||
// Simulate a remount: replace the hero clip's DOM node (no data-active).
|
||||
container.replaceChildren();
|
||||
const heroReborn = appendClip(container, "hero", "0", "5");
|
||||
expect(heroReborn.hasAttribute("data-active")).toBe(false);
|
||||
|
||||
// Diff-only would skip it (still active → unchanged); syncAll re-applies.
|
||||
updateTimelineActiveClipClasses(container, previous, 2, true);
|
||||
expect(heroReborn.hasAttribute("data-active")).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores clips with invalid timing data", () => {
|
||||
const container = document.createElement("div");
|
||||
const missingId = appendClip(container, "", "0", "2");
|
||||
|
||||
@@ -65,13 +65,23 @@ function setsMatch(left: Set<string>, right: Set<string>): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyActiveClipDiff(records: ActiveClipRecord[], previous: Set<string>, time: number) {
|
||||
function applyActiveClipDiff(
|
||||
records: ActiveClipRecord[],
|
||||
previous: Set<string>,
|
||||
time: number,
|
||||
// Force every record's attribute to match its active state instead of only
|
||||
// touching clips whose active-state changed. Required whenever `records` were
|
||||
// freshly re-queried after a render: a clip that stayed active but got a new
|
||||
// DOM node (e.g. moved lanes on a reorder) would otherwise be skipped by the
|
||||
// diff and render without `data-active` despite still being at the playhead.
|
||||
syncAll = false,
|
||||
) {
|
||||
const next = getActiveClipIds(records, time);
|
||||
const changed = !setsMatch(previous, next);
|
||||
for (const record of records) {
|
||||
const wasActive = previous.has(record.id);
|
||||
const isActive = next.has(record.id);
|
||||
if (wasActive === isActive) continue;
|
||||
if (!syncAll && wasActive === isActive) continue;
|
||||
record.element.toggleAttribute("data-active", isActive);
|
||||
}
|
||||
previous.clear();
|
||||
@@ -83,8 +93,9 @@ export function updateTimelineActiveClipClasses(
|
||||
container: HTMLElement,
|
||||
previous: Set<string>,
|
||||
time: number,
|
||||
syncAll = false,
|
||||
) {
|
||||
applyActiveClipDiff(collectTimelineClipRecords(container), previous, time);
|
||||
applyActiveClipDiff(collectTimelineClipRecords(container), previous, time, syncAll);
|
||||
}
|
||||
|
||||
export function useTimelineActiveClips({
|
||||
@@ -107,7 +118,7 @@ export function useTimelineActiveClips({
|
||||
}
|
||||
recordsRef.current = collectTimelineClipRecords(scroll);
|
||||
recordsByIdRef.current = indexClipRecordsById(recordsRef.current);
|
||||
applyActiveClipDiff(recordsRef.current, previousActiveIdsRef.current, time);
|
||||
applyActiveClipDiff(recordsRef.current, previousActiveIdsRef.current, time, true);
|
||||
},
|
||||
[scrollRef],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user