mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 04:18:01 +00:00
fix(studio): open the path node menu on arc waypoints
Right-clicking a motionPath waypoint in the preview overlay opened Chrome's own context menu on top of the editor: the handler returned before preventDefault for every node that was not an x/y keyframe. Both node kinds now open Studio's menu. A waypoint has no percentage of its own, so Move to Playhead is hidden and Delete acts on the path index, matching the hover x badge; Delete is withheld entirely on a two-anchor arc, where the writer refuses the removal and the entry would silently do nothing.
This commit is contained in:
@@ -66,4 +66,25 @@ describe("KeyframeDiamondContextMenu", () => {
|
||||
expect(onDelete).toHaveBeenCalledWith("box", target);
|
||||
expect(onMoveToPlayhead).toHaveBeenCalledWith(element, target);
|
||||
});
|
||||
|
||||
// An arc waypoint on a two-anchor path cannot be dropped on its own, so the
|
||||
// caller withholds onDelete rather than offering an entry that does nothing.
|
||||
it("hides the single-node delete when no handler is given", () => {
|
||||
const host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() =>
|
||||
root.render(
|
||||
<KeyframeDiamondContextMenu state={state} onClose={() => {}} onDeleteAll={vi.fn()} />,
|
||||
),
|
||||
);
|
||||
|
||||
const labels = Array.from(document.body.querySelectorAll("button")).map(
|
||||
(button) => button.textContent,
|
||||
);
|
||||
expect(labels).toEqual(["Delete All Keyframes"]);
|
||||
|
||||
act(() => root.unmount());
|
||||
host.remove();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,10 @@ export interface KeyframeDiamondContextMenuState {
|
||||
interface KeyframeDiamondContextMenuProps {
|
||||
state: KeyframeDiamondContextMenuState;
|
||||
onClose: () => void;
|
||||
onDelete: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
|
||||
/** Omitted where this node cannot be deleted on its own (see the arc-waypoint
|
||||
* floor in removeMotionPathPointInScript): an entry that silently no-ops is
|
||||
* worse than no entry. */
|
||||
onDelete?: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
|
||||
onDeleteAll: (element: TimelineElement) => void;
|
||||
/** Retime the keyframe to the current playhead, preserving its value + ease. */
|
||||
onMoveToPlayhead?: (element: TimelineElement, keyframe: TimelineKeyframeTarget) => void;
|
||||
@@ -43,7 +46,9 @@ export const KeyframeDiamondContextMenu = memo(function KeyframeDiamondContextMe
|
||||
};
|
||||
|
||||
const menuWidth = 200;
|
||||
const menuHeight = onMoveToPlayhead ? 100 : 70;
|
||||
// Measured off the rendered rows, so the flip-up test below stays right as
|
||||
// optional entries drop out.
|
||||
const menuHeight = 10 + (1 + (onMoveToPlayhead ? 1 : 0) + (onDelete ? 1 : 0)) * 30;
|
||||
const overflowY = state.y + menuHeight - window.innerHeight;
|
||||
const adjustedX = state.x + menuWidth > window.innerWidth ? state.x - menuWidth : state.x;
|
||||
const adjustedY = overflowY > 0 ? state.y - overflowY - 8 : state.y;
|
||||
@@ -71,16 +76,18 @@ export const KeyframeDiamondContextMenu = memo(function KeyframeDiamondContextMe
|
||||
)}
|
||||
|
||||
{/* Delete */}
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 cursor-pointer text-left"
|
||||
onClick={() => {
|
||||
onDelete(state.elementId, keyframe);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Delete Keyframe
|
||||
</button>
|
||||
{onDelete && (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 cursor-pointer text-left"
|
||||
onClick={() => {
|
||||
onDelete(state.elementId, keyframe);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Delete Keyframe
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user