mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 16:42:27 +00:00
fix(studio): don't offer layout grouping for a selection of audio clips
Selecting two audio clips offered "Group selection", which is the layout grouper: it wraps the members in a positioned <div> at their bounding box, rebases each child's left/top against that origin, and adopts the topmost member's z-index. None of that means anything for audio. An <audio> clip has no box — offsetWidth/Height are 0 — so the wrapper came out `width: 0px; height: 0px` with inline left/top written onto elements that are never laid out, and the composition gained a <div> standing for nothing audible. Confirmed against `wrapElementsInHtml` directly: it matched and wrapped, producing exactly that. Refused in `handleGroupSelection`, which is where the G shortcut also lands — a hidden button cannot gate a keystroke. The panel withholds the button as well, so the refusal is not the first the author hears of it. The message names the alternative rather than only declining: audio's answer to "these clips belong together" is an <hf-audio-group> bus, which the timeline's own FX pointer already creates. A mixed selection is refused too — the wrapper would take the audio in with the rest. `isAudioDomElement` sits beside `isAudioTimelineElement` and delegates to it, so the selection layer and the timeline cannot drift into disagreeing about what counts as audio. Six tests across both entry points, each mutation-checked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a387850032
commit
a05f16c5e6
@@ -58,6 +58,8 @@ const capturedOnReorderShadow: { fn: ((targets: string[]) => void) | undefined }
|
||||
fn: undefined,
|
||||
};
|
||||
const domEditSelectionRef: { current: DomEditSelection | null } = { current: null };
|
||||
const domEditGroupSelectionsRef: { current: DomEditSelection[] } = { current: [] };
|
||||
const groupSelectionSpy = vi.fn();
|
||||
const gsapCommitMutation = Object.assign(vi.fn(), { batch: vi.fn() });
|
||||
|
||||
function createSessionParams(
|
||||
@@ -131,7 +133,7 @@ vi.mock("./useDomSelection", () => ({
|
||||
domEditHoverSelection: null,
|
||||
activeGroupElement: null,
|
||||
domEditSelectionRef,
|
||||
domEditGroupSelectionsRef: { current: [] },
|
||||
domEditGroupSelectionsRef,
|
||||
setActiveGroupElement: vi.fn(),
|
||||
applyDomSelection: vi.fn(),
|
||||
clearDomSelection: vi.fn(),
|
||||
@@ -190,7 +192,7 @@ vi.mock("./useGsapScriptCommits", () => ({
|
||||
}));
|
||||
vi.mock("./useGroupCommits", () => ({
|
||||
useGroupCommits: () => ({
|
||||
groupSelection: vi.fn(),
|
||||
groupSelection: (...args: unknown[]) => groupSelectionSpy(...args),
|
||||
ungroupSelection: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
@@ -407,3 +409,56 @@ describe("bulk segment ease commits", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── Grouping refuses audio ───────────────────────────────────────────────────
|
||||
//
|
||||
// A layout group is a positioned wrapper: it takes the members' bounding box,
|
||||
// rebases each child's left/top against it and adopts the topmost z-index. An
|
||||
// <audio> clip has no box — offsetWidth/Height are 0 — so this produced a 0x0
|
||||
// div with inline left/top on elements that are never laid out. Enforced here
|
||||
// rather than only by hiding the button, because the G shortcut routes through
|
||||
// the same handler and no hidden button can gate a keystroke.
|
||||
|
||||
describe("handleGroupSelection with audio in the selection", () => {
|
||||
const sel = (tag: string): DomEditSelection =>
|
||||
({
|
||||
id: tag,
|
||||
element: document.createElement(tag),
|
||||
sourceFile: "index.html",
|
||||
}) as unknown as DomEditSelection;
|
||||
|
||||
async function group(members: DomEditSelection[]) {
|
||||
const { useDomEditSession } = await import("./useDomEditSession");
|
||||
groupSelectionSpy.mockClear();
|
||||
domEditGroupSelectionsRef.current = members;
|
||||
const showToast = vi.fn();
|
||||
const captured: { fn?: () => void } = {};
|
||||
function Probe() {
|
||||
captured.fn = useDomEditSession(createSessionParams({ showToast })).handleGroupSelection;
|
||||
return null;
|
||||
}
|
||||
const root = createRoot(document.createElement("div"));
|
||||
act(() => root.render(<Probe />));
|
||||
act(() => captured.fn?.());
|
||||
act(() => root.unmount());
|
||||
domEditGroupSelectionsRef.current = [];
|
||||
return { showToast };
|
||||
}
|
||||
|
||||
it("refuses a selection of audio clips, and says where grouping audio lives", async () => {
|
||||
const { showToast } = await group([sel("audio"), sel("audio")]);
|
||||
expect(groupSelectionSpy).not.toHaveBeenCalled();
|
||||
expect(String(showToast.mock.calls[0]?.[0])).toContain("bus");
|
||||
});
|
||||
|
||||
it("refuses a mixed selection, since the wrapper would take the audio in too", async () => {
|
||||
const { showToast } = await group([sel("div"), sel("audio")]);
|
||||
expect(groupSelectionSpy).not.toHaveBeenCalled();
|
||||
expect(String(showToast.mock.calls[0]?.[0])).toContain("layout");
|
||||
});
|
||||
|
||||
it("still groups a selection of layout elements", async () => {
|
||||
await group([sel("div"), sel("span")]);
|
||||
expect(groupSelectionSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user