fix(studio): dragging an audio clip's edge trims it instead of moving it (#4645)

* fix(studio): dragging an audio clip's edge trims it instead of moving it

The clip picture layer (waveform, filmstrip, stills) paints above the trim
handles and swallowed the press, so an edge drag started a move. The
picture layer now takes no input.

* docs(studio): say clip content takes no pointer input
This commit is contained in:
Miguel Ángel
2026-09-28 08:21:00 +00:00
committed by GitHub
parent 2449ea05b1
commit 0744667293
3 changed files with 42 additions and 6 deletions
@@ -13,9 +13,9 @@ export interface TimelineClipRenderContext {
export interface TimelineProps extends TimelineDropCallbacks, TimelineEditOverrides {
/** Project-scoped reset boundary; soft source refreshes retain the same epoch. */
sessionEpoch?: number;
/** keepPlaying: true preserves the current play state across the seek. */
onSeek?: (time: number, options?: { keepPlaying?: boolean }) => void;
onDrillDown?: (element: TimelineElement) => void;
/** Picture only: takes no pointer input. Interactive content goes in renderClipOverlay. */
renderClipContent?: (
element: TimelineElement,
style: { clip: string; label: string },
@@ -1,7 +1,17 @@
// @vitest-environment happy-dom
import { act, createElement } from "react";
import { createRoot } from "react-dom/client";
import { describe, expect, it } from "vitest";
import { resolveClipRenderContext } from "./timelineClipChildren";
import { renderClipChildren, resolveClipRenderContext } from "./timelineClipChildren";
import { getTrackStyle } from "./timelineIcons";
import type { TimelineElement } from "../store/playerStore";
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", {
configurable: true,
value: true,
});
const clip: TimelineElement = {
id: "clip",
tag: "video",
@@ -29,3 +39,26 @@ describe("resolveClipRenderContext", () => {
});
});
});
describe("renderClipChildren", () => {
// happy-dom cannot hit-test, so this pins what decides the hit: the picture takes no input.
it("lets a press on a picture stacked above the trim handles fall through to them", () => {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
const picture = () =>
createElement("div", { "data-picture": "", style: { position: "absolute", zIndex: 10 } });
act(() => {
root.render(renderClipChildren(clip, getTrackStyle("video"), picture, undefined));
});
// happy-dom does not inherit pointer-events, so walk up to the layer that turns input off.
let node = host.querySelector<HTMLElement>("[data-picture]");
expect(node).not.toBeNull();
while (node && node !== host && node.style.pointerEvents !== "none") node = node.parentElement;
expect(node?.style.pointerEvents).toBe("none");
act(() => root.unmount());
host.remove();
});
});
@@ -57,10 +57,13 @@ export function renderClipChildren(
{renderClipOverlay?.(element)}
{!renderClipContent && <ClipLintDot element={element} />}
{renderClipContent && (
// borderRadius: inherit — the clip itself is overflow-visible (keyframe
// diamonds hang outside its bounds), so the thumbnail layer must clip
// itself to the clip's rounded corners or sharp corners poke out.
<div className="absolute inset-0 overflow-hidden" style={{ borderRadius: "inherit" }}>
// The picture can paint above the trim handles, so it takes no input and presses reach them.
// borderRadius: inherit clips it to the clip's rounded corners; the clip itself is
// overflow-visible because keyframe diamonds hang outside its bounds.
<div
className="absolute inset-0 overflow-hidden"
style={{ borderRadius: "inherit", pointerEvents: "none" }}
>
{renderClipContent(element, clipStyle, context)}
</div>
)}