mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
feat(studio): add flat Overflow and Mask rows to the Style group
This commit is contained in:
@@ -334,3 +334,135 @@ describe("FlatStyleSection — blur sliders", () => {
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
|
||||
function getInsetSideInputOrNull(host: HTMLElement, label: "T" | "R" | "B" | "L") {
|
||||
const span = Array.from(host.querySelectorAll("span")).find((el) => el.textContent === label);
|
||||
return span?.parentElement?.querySelector<HTMLInputElement>("input") ?? null;
|
||||
}
|
||||
|
||||
function getInsetSideInput(host: HTMLElement, label: "T" | "R" | "B" | "L"): HTMLInputElement {
|
||||
const input = getInsetSideInputOrNull(host, label);
|
||||
if (!input) throw new Error(`expected an inset side input for "${label}"`);
|
||||
return input;
|
||||
}
|
||||
|
||||
async function commitInsetSideInput(
|
||||
host: HTMLElement,
|
||||
label: "T" | "R" | "B" | "L",
|
||||
nextValue: string,
|
||||
) {
|
||||
const input = getInsetSideInput(host, label);
|
||||
act(() => {
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
"value",
|
||||
)?.set;
|
||||
nativeInputValueSetter?.call(input, nextValue);
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
await act(async () => {
|
||||
input.dispatchEvent(new Event("focusout", { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
describe("FlatStyleSection — Overflow and Mask", () => {
|
||||
it("renders Overflow and Mask selects, and inset-side rows when the mask is an inset", () => {
|
||||
const { host, root } = renderSection({
|
||||
overflow: "hidden",
|
||||
"clip-path": "inset(8px round 4px)",
|
||||
});
|
||||
expect(host.textContent).toContain("Overflow");
|
||||
expect(host.textContent).toContain("Mask");
|
||||
expect(host.textContent).toContain("hidden");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("commits an overflow change through onSetStyle", () => {
|
||||
const onSetStyle = vi.fn();
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
act(() => {
|
||||
root.render(
|
||||
<FlatStyleSection
|
||||
projectId="proj-1"
|
||||
element={makeElement()}
|
||||
styles={{}}
|
||||
assets={[]}
|
||||
onSetStyle={onSetStyle}
|
||||
gsapBorderRadius={null}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
const overflowSelect = Array.from(host.querySelectorAll("select")).find((s) =>
|
||||
Array.from(s.options).some((o) => o.value === "scroll"),
|
||||
);
|
||||
if (!overflowSelect) throw new Error("expected the overflow select");
|
||||
act(() => {
|
||||
overflowSelect.value = "hidden";
|
||||
overflowSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
expect(onSetStyle).toHaveBeenCalledWith("overflow", "hidden");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("resets overflow back to visible via the reset button", () => {
|
||||
const { host, root, onSetStyle } = renderSection({ overflow: "scroll" });
|
||||
const { resetButton } = getFlatSelectRow(host, "Overflow");
|
||||
if (!resetButton) throw new Error("expected the overflow reset button");
|
||||
act(() => resetButton.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
||||
expect(onSetStyle).toHaveBeenCalledWith("overflow", "visible");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("commits a mask preset change through onSetStyle, building an inset() clip-path", () => {
|
||||
const { host, root, onSetStyle } = renderSection({});
|
||||
changeFlatSelectRow(host, "Mask", "inset");
|
||||
expect(onSetStyle).toHaveBeenCalledWith("clip-path", "inset(0 round 0px)");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("resets the mask back to none via the reset button", () => {
|
||||
const { host, root, onSetStyle } = renderSection({ "clip-path": "circle(50% at 50% 50%)" });
|
||||
const { resetButton } = getFlatSelectRow(host, "Mask");
|
||||
if (!resetButton) throw new Error("expected the mask reset button");
|
||||
act(() => resetButton.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
||||
expect(onSetStyle).toHaveBeenCalledWith("clip-path", "none");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("does not render inset-side rows when the mask is not an inset", () => {
|
||||
const { host, root } = renderSection({ "clip-path": "circle(50% at 50% 50%)" });
|
||||
expect(getInsetSideInputOrNull(host, "T")).toBeNull();
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("commits a T inset-side edit through onSetStyle, preserving the other sides and radius", async () => {
|
||||
const { host, root, onSetStyle } = renderSection({ "clip-path": "inset(8px round 4px)" });
|
||||
await commitInsetSideInput(host, "T", "10");
|
||||
expect(onSetStyle).toHaveBeenCalledWith("clip-path", "inset(10px 8px 8px 8px round 4px)");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("commits an L inset-side edit through onSetStyle", async () => {
|
||||
const { host, root, onSetStyle } = renderSection({ "clip-path": "inset(8px round 4px)" });
|
||||
await commitInsetSideInput(host, "L", "2");
|
||||
expect(onSetStyle).toHaveBeenCalledWith("clip-path", "inset(8px 8px 8px 2px round 4px)");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("commits an R inset-side edit through onSetStyle", async () => {
|
||||
const { host, root, onSetStyle } = renderSection({ "clip-path": "inset(8px round 4px)" });
|
||||
await commitInsetSideInput(host, "R", "3");
|
||||
expect(onSetStyle).toHaveBeenCalledWith("clip-path", "inset(8px 3px 8px 8px round 4px)");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("commits a B inset-side edit through onSetStyle", async () => {
|
||||
const { host, root, onSetStyle } = renderSection({ "clip-path": "inset(8px round 4px)" });
|
||||
await commitInsetSideInput(host, "B", "5");
|
||||
expect(onSetStyle).toHaveBeenCalledWith("clip-path", "inset(8px 8px 5px 8px round 4px)");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,18 +7,24 @@ import { BorderRadiusEditor } from "./BorderRadiusEditor";
|
||||
import { formatStrokeSummary, parseStrokeSummary } from "./propertyPanelFlatStyleHelpers";
|
||||
import {
|
||||
buildBoxShadowPresetValue,
|
||||
buildClipPathValue,
|
||||
buildInsetClipPathSides,
|
||||
buildStrokeStyleUpdates,
|
||||
buildStrokeWidthStyleUpdates,
|
||||
extractBackgroundImageUrl,
|
||||
formatNumericValue,
|
||||
formatPxMetricValue,
|
||||
getClipPathInsetPx,
|
||||
getCssFilterFunctionPx,
|
||||
inferBoxShadowPreset,
|
||||
inferClipPathPreset,
|
||||
normalizePanelPxValue,
|
||||
parseInsetClipPathSides,
|
||||
parseNumericValue,
|
||||
parsePxMetricValue,
|
||||
setCssFilterFunctionPx,
|
||||
type BoxShadowPreset,
|
||||
type ClipPathInsetSides,
|
||||
} from "./propertyPanelHelpers";
|
||||
import {
|
||||
FlatRow,
|
||||
@@ -26,6 +32,7 @@ import {
|
||||
FlatSelectRow,
|
||||
FlatSlider,
|
||||
} from "./propertyPanelFlatPrimitives";
|
||||
import { MetricField } from "./propertyPanelPrimitives";
|
||||
import { resolveValueTier } from "./propertyPanelValueTier";
|
||||
import { ColorField } from "./propertyPanelColor";
|
||||
import { GradientField, ImageFillField } from "./propertyPanelFill";
|
||||
@@ -373,6 +380,103 @@ function FlatBlurSliders({
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Flat Overflow + Mask rows (+ inset sides) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function FlatOverflowMaskRows({
|
||||
styles,
|
||||
disabled,
|
||||
onSetStyle,
|
||||
}: {
|
||||
styles: Record<string, string>;
|
||||
disabled: boolean;
|
||||
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
||||
}) {
|
||||
const radiusValue = parseNumericValue(styles["border-radius"]) ?? 0;
|
||||
const clipPathValue = styles["clip-path"] || "none";
|
||||
const clipPathPreset = inferClipPathPreset(clipPathValue);
|
||||
const parsedClipInsets = parseInsetClipPathSides(clipPathValue);
|
||||
const clipInsetValue = getClipPathInsetPx(clipPathValue);
|
||||
const clipInsetSides = parsedClipInsets ?? {
|
||||
top: clipInsetValue,
|
||||
right: clipInsetValue,
|
||||
bottom: clipInsetValue,
|
||||
left: clipInsetValue,
|
||||
radius: radiusValue,
|
||||
};
|
||||
const showClipInsetSides = clipPathPreset === "inset" || parsedClipInsets != null;
|
||||
|
||||
const commitClipInsetSide = (side: keyof ClipPathInsetSides, nextValue: string) => {
|
||||
const next = parsePxMetricValue(nextValue);
|
||||
if (next == null) return;
|
||||
const sides: ClipPathInsetSides = {
|
||||
top: clipInsetSides.top,
|
||||
right: clipInsetSides.right,
|
||||
bottom: clipInsetSides.bottom,
|
||||
left: clipInsetSides.left,
|
||||
};
|
||||
sides[side] = next;
|
||||
void onSetStyle("clip-path", buildInsetClipPathSides(sides, clipInsetSides.radius));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlatSelectRow
|
||||
label="Overflow"
|
||||
value={styles.overflow || "visible"}
|
||||
options={["visible", "hidden", "clip", "auto", "scroll"]}
|
||||
tier={resolveValueTier(styles.overflow, "visible")}
|
||||
disabled={disabled}
|
||||
onChange={(next) => void onSetStyle("overflow", next)}
|
||||
onReset={() => void onSetStyle("overflow", "visible")}
|
||||
/>
|
||||
<FlatSelectRow
|
||||
label="Mask"
|
||||
value={clipPathPreset === "custom" ? "none" : clipPathPreset}
|
||||
options={["none", "inset", "circle"]}
|
||||
tier={resolveValueTier(clipPathPreset === "none" ? undefined : clipPathPreset, "none")}
|
||||
disabled={disabled}
|
||||
onChange={(next) => {
|
||||
void onSetStyle(
|
||||
"clip-path",
|
||||
buildClipPathValue(next as "none" | "inset" | "circle", radiusValue, clipPathValue),
|
||||
);
|
||||
}}
|
||||
onReset={() => void onSetStyle("clip-path", "none")}
|
||||
/>
|
||||
{showClipInsetSides && (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<MetricField
|
||||
label="T"
|
||||
value={formatPxMetricValue(clipInsetSides.top)}
|
||||
disabled={disabled}
|
||||
onCommit={(next) => commitClipInsetSide("top", next)}
|
||||
/>
|
||||
<MetricField
|
||||
label="R"
|
||||
value={formatPxMetricValue(clipInsetSides.right)}
|
||||
disabled={disabled}
|
||||
onCommit={(next) => commitClipInsetSide("right", next)}
|
||||
/>
|
||||
<MetricField
|
||||
label="B"
|
||||
value={formatPxMetricValue(clipInsetSides.bottom)}
|
||||
disabled={disabled}
|
||||
onCommit={(next) => commitClipInsetSide("bottom", next)}
|
||||
/>
|
||||
<MetricField
|
||||
label="L"
|
||||
value={formatPxMetricValue(clipInsetSides.left)}
|
||||
disabled={disabled}
|
||||
onCommit={(next) => commitClipInsetSide("left", next)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FlatStyleSection({
|
||||
projectId,
|
||||
element,
|
||||
@@ -414,6 +518,11 @@ export function FlatStyleSection({
|
||||
onSetStyle={onSetStyle}
|
||||
/>
|
||||
<FlatBlurSliders styles={styles} disabled={styleEditingDisabled} onSetStyle={onSetStyle} />
|
||||
<FlatOverflowMaskRows
|
||||
styles={styles}
|
||||
disabled={styleEditingDisabled}
|
||||
onSetStyle={onSetStyle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user