mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
feat(studio): a host can show the hover box and give the stage its starting size (#5116)
* test(studio): pin a host's hover box and a starting stage size * feat(studio): a host can show the hover box and give the stage its starting size * fix(studio): keep the comment share flat and let a measured size win over the hint
This commit is contained in:
@@ -61,7 +61,7 @@ export interface DomEditOverlayProps {
|
||||
hoverSelection: DomEditSelection | null;
|
||||
allowCanvasMovement?: boolean;
|
||||
allowBodyDrag?: boolean;
|
||||
/** "host": no hover, marquee, box re-select, or body drag if allowBodyDrag is false; Enter still opens text. */
|
||||
/** "host": no marquee, box re-select, or body drag if allowBodyDrag is false; Enter still opens text. */
|
||||
canvasInput?: "overlay" | "host";
|
||||
onTextEditingChange?: (editing: boolean) => void;
|
||||
/** A click on a single selection's box, in either mode; the event may be the pointerup. */
|
||||
@@ -500,7 +500,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
onLostPointerCapture={gestures.onLostPointerCapture}
|
||||
onContextMenu={hostInput ? undefined : handleContextMenu}
|
||||
>
|
||||
{!hostInput && hoverSelection && hoverRect && compRect.width > 0 && (
|
||||
{hoverSelection && hoverRect && compRect.width > 0 && (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
data-dom-edit-hover-box="true"
|
||||
|
||||
@@ -311,7 +311,7 @@ describe("DomEditOverlay with canvasInput host", () => {
|
||||
expect(fixture(HOST).overlay.className).toContain("pointer-events-none");
|
||||
});
|
||||
|
||||
it("draws no hover box", () => {
|
||||
it("draws the hover box for the selection the host hovers, and none without one", () => {
|
||||
layout.hover = RECT;
|
||||
const hovered = (props = {}) => {
|
||||
const { overlay } = fixture(props);
|
||||
@@ -321,7 +321,8 @@ describe("DomEditOverlay with canvasInput host", () => {
|
||||
};
|
||||
const selection = makeSelection("Hover", textElement("hover"));
|
||||
expect(hovered({ hoverSelection: selection })).not.toBeNull();
|
||||
expect(hovered({ ...HOST, hoverSelection: selection })).toBeNull();
|
||||
expect(hovered({ ...HOST, hoverSelection: selection })).not.toBeNull();
|
||||
expect(hovered({ ...HOST, hoverSelection: null })).toBeNull();
|
||||
});
|
||||
|
||||
it("starts no marquee and makes no selection from a press on empty canvas", () => {
|
||||
|
||||
@@ -12,6 +12,12 @@ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
const playerMounts: string[] = [];
|
||||
let livePlayerProps: { onReadyToShowChange?: (ready: boolean) => void } = {};
|
||||
|
||||
const measured = vi.hoisted(() => ({ size: null as { width: number; height: number } | null }));
|
||||
vi.mock("../../utils/previewCompositionSize", async (original) => ({
|
||||
...(await original<typeof import("../../utils/previewCompositionSize")>()),
|
||||
readPreviewCompositionSize: () => measured.size,
|
||||
}));
|
||||
|
||||
vi.mock("../../player", async () => {
|
||||
const React = await import("react");
|
||||
|
||||
@@ -81,7 +87,12 @@ function renderPreview(
|
||||
{
|
||||
box = { width: 800, height: 600 },
|
||||
fillBox,
|
||||
}: { box?: { width: number; height: number }; fillBox?: boolean } = {},
|
||||
compositionSizeHint,
|
||||
}: {
|
||||
box?: { width: number; height: number };
|
||||
fillBox?: boolean;
|
||||
compositionSizeHint?: { width: number; height: number };
|
||||
} = {},
|
||||
) {
|
||||
resizeCallbacks = [];
|
||||
const host = document.createElement("div");
|
||||
@@ -103,6 +114,7 @@ function renderPreview(
|
||||
setShadowIframeNode: () => {},
|
||||
resetPreviewSlots: () => {},
|
||||
fillBox,
|
||||
compositionSizeHint,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -378,6 +390,37 @@ describe("NLEPreview", () => {
|
||||
filled.cleanup();
|
||||
});
|
||||
|
||||
it("takes the host's composition size as the stage's shape until the preview measures its own", () => {
|
||||
const box = { width: 800, height: 600 };
|
||||
const plain = renderPreview(undefined, { box });
|
||||
expect([plain.stage.style.width, plain.stage.style.height]).toEqual(["784px", "441px"]);
|
||||
plain.cleanup();
|
||||
|
||||
const hinted = renderPreview(undefined, {
|
||||
box,
|
||||
compositionSizeHint: { width: 1276, height: 1078 },
|
||||
});
|
||||
const shape = parseFloat(hinted.stage.style.width) / parseFloat(hinted.stage.style.height);
|
||||
expect(shape).toBeCloseTo(1276 / 1078, 3);
|
||||
expect(hinted.stage.style.height).toBe("584px");
|
||||
hinted.cleanup();
|
||||
});
|
||||
|
||||
it("lets the size the preview measures win over the host's hint", () => {
|
||||
measured.size = { width: 1920, height: 1080 };
|
||||
try {
|
||||
const box = { width: 800, height: 600 };
|
||||
const hinted = renderPreview(undefined, {
|
||||
box,
|
||||
compositionSizeHint: { width: 1276, height: 1078 },
|
||||
});
|
||||
expect([hinted.stage.style.width, hinted.stage.style.height]).toEqual(["784px", "441px"]);
|
||||
hinted.cleanup();
|
||||
} finally {
|
||||
measured.size = null;
|
||||
}
|
||||
});
|
||||
|
||||
it("clips a shadow reload so its own loading overlay cannot paint over the live frame", () => {
|
||||
const view = renderPreview([
|
||||
{ gen: 0, role: "live" },
|
||||
|
||||
@@ -50,6 +50,7 @@ interface NLEPreviewProps extends Pick<
|
||||
onCompositionSizeChange?: (size: PreviewCompositionSize | null) => void;
|
||||
/** Draws the picture edge to edge in this box, without Studio's inset band. */
|
||||
fillBox?: boolean;
|
||||
compositionSizeHint?: PreviewCompositionSize;
|
||||
}
|
||||
|
||||
export function getPreviewPlayerKey({
|
||||
@@ -127,6 +128,7 @@ export const NLEPreview = memo(function NLEPreview({
|
||||
onStageRef,
|
||||
onCompositionSizeChange,
|
||||
fillBox,
|
||||
compositionSizeHint,
|
||||
}: NLEPreviewProps) {
|
||||
const activeKey = getPreviewPlayerKey({ projectId, directUrl });
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
@@ -151,6 +153,8 @@ export const NLEPreview = memo(function NLEPreview({
|
||||
const gutterPx = usePreviewGuidesStore((s) => (s.rulerVisible ? RULER_GUTTER_PX : 0));
|
||||
const insetPx = fillBox ? 0 : PREVIEW_STAGE_INSET_PX;
|
||||
const [stageSize, setStageSize] = useState(() => resolvePreviewStageSize(0, 0, null, portrait));
|
||||
const hintWidth = compositionSizeHint?.width;
|
||||
const hintHeight = compositionSizeHint?.height;
|
||||
|
||||
const zoomRef = useRef<PreviewZoomState>(DEFAULT_PREVIEW_ZOOM);
|
||||
const [settledZoom, setSettledZoom] = useState<PreviewZoomState>(DEFAULT_PREVIEW_ZOOM);
|
||||
@@ -183,7 +187,8 @@ export const NLEPreview = memo(function NLEPreview({
|
||||
resolvePreviewStageSize(
|
||||
rect.width,
|
||||
rect.height,
|
||||
compositionSize,
|
||||
compositionSize ??
|
||||
(hintWidth && hintHeight ? { width: hintWidth, height: hintHeight } : null),
|
||||
portrait,
|
||||
gutterPx,
|
||||
insetPx,
|
||||
@@ -195,7 +200,7 @@ export const NLEPreview = memo(function NLEPreview({
|
||||
const observer = new ResizeObserver(updateStageSize);
|
||||
observer.observe(viewport);
|
||||
return () => observer.disconnect();
|
||||
}, [compositionSize, portrait, gutterPx, insetPx]);
|
||||
}, [compositionSize, hintWidth, hintHeight, portrait, gutterPx, insetPx]);
|
||||
|
||||
const onCompositionSizeChangeRef = useRef(onCompositionSizeChange);
|
||||
onCompositionSizeChangeRef.current = onCompositionSizeChange;
|
||||
|
||||
Reference in New Issue
Block a user