mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(core): a clip gets its own inline display back when the runtime shows it (#4498)
* fix(core): a clip gets its own inline display back when the runtime shows it The visibility pass wrote a plain display:none over a hidden clip's inline display and removed the property when showing it, so an author's display:flex came back as block and an author's display:none !important showed the clip. The data-hidden path kept the value but not its priority. Both paths now save the author's value and priority once and put exactly that back. * fix(core): a plain inline display:none still shows at the clip's start A plain none on a hidden clip can be a runtime hide left behind (a Studio reveal restoring it), so it is not saved as the author's value; the clip shows at its start as before. Values with a priority, and every other value, are still put back. * fix(core): keep an author's plain display:none through a data-hidden toggle The leftover-hide rule applies to the timed hide only; a data-hidden hide saves the author's plain none as it is, so un-hiding keeps the clip hidden, as on main. * docs(core): say the leftover rule belongs to the timed hide
This commit is contained in:
@@ -1771,6 +1771,75 @@ describe("initSandboxRuntimeModular", () => {
|
||||
expect(hiddenClip.style.display).toBe("");
|
||||
});
|
||||
|
||||
describe("a clip the visibility pass hides gets the author's inline display back", () => {
|
||||
// A relative clip under a plain wrapper stays in flow (applyClipLayout only touches
|
||||
// root children), so the pass hides it with display:none rather than visibility.
|
||||
const mountClip = (display: string, priority = "", start = "2") => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
root.setAttribute("data-root", "true");
|
||||
root.setAttribute("data-start", "0");
|
||||
root.setAttribute("data-duration", "10");
|
||||
document.body.appendChild(root);
|
||||
const wrapper = document.createElement("div");
|
||||
root.appendChild(wrapper);
|
||||
const clip = document.createElement("div");
|
||||
clip.style.position = "relative";
|
||||
clip.style.setProperty("display", display, priority);
|
||||
clip.setAttribute("data-start", start);
|
||||
clip.setAttribute("data-duration", "4");
|
||||
wrapper.appendChild(clip);
|
||||
window.__timelines = { main: createMockTimeline(10) };
|
||||
initSandboxRuntimeModular();
|
||||
return clip;
|
||||
};
|
||||
|
||||
it("keeps the author's display:flex when a later clip appears", () => {
|
||||
const clip = mountClip("flex");
|
||||
window.__player?.seek(0);
|
||||
expect(clip.style.display).toBe("none");
|
||||
window.__player?.seek(3);
|
||||
expect(clip.style.display).toBe("flex");
|
||||
});
|
||||
|
||||
it("keeps an author's display:none !important once the clip's time comes", () => {
|
||||
const clip = mountClip("none", "important");
|
||||
window.__player?.seek(0);
|
||||
window.__player?.seek(3);
|
||||
expect(clip.style.getPropertyValue("display")).toBe("none");
|
||||
expect(clip.style.getPropertyPriority("display")).toBe("important");
|
||||
});
|
||||
|
||||
it("shows a clip whose inline display is a plain none at its start", () => {
|
||||
const clip = mountClip("none");
|
||||
window.__player?.seek(0);
|
||||
expect(clip.style.display).toBe("none");
|
||||
window.__player?.seek(3);
|
||||
expect(clip.style.display).toBe("");
|
||||
});
|
||||
|
||||
it("keeps an author's plain display:none through a data-hidden toggle", () => {
|
||||
const clip = mountClip("none", "", "0");
|
||||
window.__player?.seek(1);
|
||||
clip.setAttribute("data-hidden", "");
|
||||
window.__player?.seek(1);
|
||||
clip.removeAttribute("data-hidden");
|
||||
window.__player?.seek(1);
|
||||
expect(clip.style.display).toBe("none");
|
||||
});
|
||||
|
||||
it("keeps a data-hidden clip's own display, priority included, when the attribute goes", () => {
|
||||
const clip = mountClip("grid", "important");
|
||||
clip.setAttribute("data-hidden", "");
|
||||
window.__player?.seek(3);
|
||||
expect(clip.style.display).toBe("none");
|
||||
clip.removeAttribute("data-hidden");
|
||||
window.__player?.seek(3);
|
||||
expect(clip.style.getPropertyValue("display")).toBe("grid");
|
||||
expect(clip.style.getPropertyPriority("display")).toBe("important");
|
||||
});
|
||||
});
|
||||
|
||||
it("excludes a data-hidden audio clip from Web Audio scheduling", () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
|
||||
@@ -2390,15 +2390,28 @@ export function initSandboxRuntimeModular(): void {
|
||||
timedClipIsLeaf = new WeakMap<Element, boolean>();
|
||||
};
|
||||
|
||||
// Which elements carry a `display:none` the visibility pass itself applied, so
|
||||
// the un-hide branch can undo exactly that instead of re-deriving
|
||||
// `isTimedClipInFlow`. That derived answer can flip between the hide and the
|
||||
// show pass for the SAME element — `applyClipLayout` force-absolutizes a
|
||||
// root-level clip after an earlier pass already cached it as in-flow and hid
|
||||
// it — and the corrected, no-longer-in-flow reading then skips the removal,
|
||||
// stranding the clip hidden for the rest of the render.
|
||||
const timedClipDisplayNoneApplied = new WeakSet<HTMLElement>();
|
||||
const dataHiddenDisplayRestores = new WeakMap<HTMLElement, string>();
|
||||
// The author's inline display (value and priority) under each `display:none` the
|
||||
// visibility pass applied, so showing the element puts exactly that back. Keyed on
|
||||
// what was applied, not on `isTimedClipInFlow`: that answer can flip between the hide
|
||||
// and the show pass once `applyClipLayout` force-absolutizes the clip.
|
||||
const displayBeforeHide = new WeakMap<HTMLElement, { value: string; priority: string }>();
|
||||
const hideByDisplay = (el: HTMLElement, plainNoneMayBeLeftover: boolean) => {
|
||||
if (!displayBeforeHide.has(el)) {
|
||||
const value = el.style.getPropertyValue("display");
|
||||
const priority = el.style.getPropertyPriority("display");
|
||||
// On the timed hide, a plain none may be a hide left behind (a Studio reveal restoring ours).
|
||||
const isLeftoverHide = plainNoneMayBeLeftover && value === "none" && !priority;
|
||||
displayBeforeHide.set(el, isLeftoverHide ? { value: "", priority: "" } : { value, priority });
|
||||
}
|
||||
el.style.display = "none";
|
||||
};
|
||||
const restoreDisplay = (el: HTMLElement) => {
|
||||
const before = displayBeforeHide.get(el);
|
||||
if (!before) return;
|
||||
displayBeforeHide.delete(el);
|
||||
if (before.value) el.style.setProperty("display", before.value, before.priority);
|
||||
else el.style.removeProperty("display");
|
||||
};
|
||||
const dataHiddenDisplayNodes = new WeakSet<HTMLElement>();
|
||||
// A data-hidden toggle on (or affecting) an audio element must re-schedule
|
||||
// WebAudio playback so the hidden clip's source is dropped/restored mid-
|
||||
@@ -2455,12 +2468,11 @@ export function initSandboxRuntimeModular(): void {
|
||||
|
||||
if (rawNode.hasAttribute("data-hidden")) {
|
||||
if (!dataHiddenDisplayNodes.has(rawNode)) {
|
||||
dataHiddenDisplayRestores.set(rawNode, rawNode.style.getPropertyValue("display"));
|
||||
dataHiddenDisplayNodes.add(rawNode);
|
||||
if (nodeAffectsAudio(rawNode)) hiddenAudioDirty = true;
|
||||
groupMuteDirty = true;
|
||||
}
|
||||
rawNode.style.display = "none";
|
||||
hideByDisplay(rawNode, false);
|
||||
if (isVideoElement(rawNode) || isImageElement(rawNode)) {
|
||||
colorGradingRuntime?.setSourceVisibility(rawNode, false);
|
||||
}
|
||||
@@ -2468,13 +2480,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
}
|
||||
|
||||
if (dataHiddenDisplayNodes.has(rawNode)) {
|
||||
const previousDisplay = dataHiddenDisplayRestores.get(rawNode);
|
||||
if (previousDisplay) {
|
||||
rawNode.style.display = previousDisplay;
|
||||
} else {
|
||||
rawNode.style.removeProperty("display");
|
||||
}
|
||||
dataHiddenDisplayRestores.delete(rawNode);
|
||||
restoreDisplay(rawNode);
|
||||
dataHiddenDisplayNodes.delete(rawNode);
|
||||
if (nodeAffectsAudio(rawNode)) hiddenAudioDirty = true;
|
||||
groupMuteDirty = true;
|
||||
@@ -2519,13 +2525,9 @@ export function initSandboxRuntimeModular(): void {
|
||||
colorGradingRuntime?.setSourceVisibility(rawNode, isVisibleNow);
|
||||
}
|
||||
if (isVisibleNow) {
|
||||
if (timedClipDisplayNoneApplied.has(rawNode)) {
|
||||
rawNode.style.removeProperty("display");
|
||||
timedClipDisplayNoneApplied.delete(rawNode);
|
||||
}
|
||||
restoreDisplay(rawNode);
|
||||
} else if (isTimedClipInFlow(rawNode) && isTimedClipLeaf(rawNode)) {
|
||||
rawNode.style.display = "none";
|
||||
timedClipDisplayNoneApplied.add(rawNode);
|
||||
hideByDisplay(rawNode, true);
|
||||
}
|
||||
}
|
||||
if (decidedTimedClip && revealTimedClipsAfterFirstPass()) colorGradingRuntime?.refresh();
|
||||
|
||||
Reference in New Issue
Block a user