Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/studio/src/player/components/TimelineFxButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,37 @@ describe("TimelineFxButton", () => {
expect(document.querySelector('[role="dialog"]')).toBeTruthy();
});

// The reported symptom was "the grouping button did nothing". The dialog WAS
// opening — it positioned at `anchorRect.bottom + 4` with no flip and no
// clamp, and this button lives in a track header at the bottom of the studio
// window, so it opened past the viewport edge. The test below it passed
// throughout: happy-dom reports an all-zero rect for an unlaid-out button,
// which lands the dialog at top:4 — on screen, and nothing like the app.
it("flips the group dialog above the anchor when there is no room below", () => {
const host = mount(<TimelineFxButton variant="group-pointer" onGroupClips={vi.fn()} />);
const fx = byTextButton(host, "FX");
// A track header near the bottom edge of the (1024x768) window.
fx!.getBoundingClientRect = () =>
({ left: 300, top: 760, right: 320, bottom: 776, width: 20, height: 16 }) as DOMRect;
act(() => fx?.click());
const dialog = document.querySelector('[role="dialog"]') as HTMLElement;
expect(dialog).toBeTruthy();
// Above the anchor, and fully inside the viewport: 644 + 112 === 756.
expect(dialog.style.top).toBe("644px");
});

it("keeps the group dialog inside the right edge of the window", () => {
const host = mount(<TimelineFxButton variant="group-pointer" onGroupClips={vi.fn()} />);
const fx = byTextButton(host, "FX");
// Hard against the right edge: 224px wide + a 12px margin has to fit.
fx!.getBoundingClientRect = () =>
({ left: 1010, top: 100, right: 1024, bottom: 116, width: 14, height: 16 }) as DOMRect;
act(() => fx?.click());
const dialog = document.querySelector('[role="dialog"]') as HTMLElement;
expect(dialog.style.left).toBe("788px");
expect(dialog.style.top).toBe("120px");
});

it("group-pointer variant offers Group instead of a popover", () => {
const onGroupClips = vi.fn();
const host = mount(<TimelineFxButton variant="group-pointer" onGroupClips={onGroupClips} />);
Expand Down
25 changes: 24 additions & 1 deletion packages/studio/src/player/components/TimelineFxButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ import {
} from "@hyperframes/core/audio-fx";
import type { HfAudioNameKind } from "@hyperframes/core/audio-carve";
import { TimelineFxPopover } from "../../components/editor/TimelineFxPopover.js";
import { resolveFloatingPanelPosition } from "../../components/editor/floatingPanel.js";

// Estimated, like FORMAT_PANEL_SIZE in RenderQueue: `w-56` is exact, and only
// the flip decision uses the height — the clamp keeps the dialog on screen
// either way.
const GROUP_DIALOG_SIZE = { width: 224, height: 112 };

/** Where the grouping dialog goes: flipped above the anchor when there is no
* room below, and clamped so neither edge leaves the viewport. */
function groupDialogPosition(anchorRect: DOMRect): { left: number; top: number } {
const { left, top } = resolveFloatingPanelPosition(
anchorRect,
{ width: window.innerWidth, height: window.innerHeight },
GROUP_DIALOG_SIZE,
{ offset: 4 },
);
return { left, top };
}

function parseFxChainOrEmpty(raw: string | undefined): HfAudioFxChain {
if (!raw) return { version: 1, nodes: [] };
Expand Down Expand Up @@ -80,7 +98,12 @@ export function TimelineFxButton(props: TimelineFxButtonProps) {
role="dialog"
aria-label="Group these clips to add effects"
className="z-[200] w-56 rounded-md border border-white/10 bg-[#1b1b1f] p-2.5 text-[11px] text-white/75 shadow-xl"
style={{ position: "fixed", left: anchorRect.left, top: anchorRect.bottom + 4 }}
// Was `top: anchorRect.bottom + 4` with no flip and no clamp. This
// button lives in a track header at the BOTTOM of the studio
// window, so the dialog opened past the viewport edge and the
// click read as doing nothing at all. Same helper the other body
// portals position with.
style={{ position: "fixed", ...groupDialogPosition(anchorRect) }}
onPointerDown={(event) => event.stopPropagation()}
>
<p>Group these clips to add effects to all of them.</p>
Expand Down
Loading