Skip to content
Open
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
116 changes: 116 additions & 0 deletions packages/studio/src/components/editor/PropertyPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -615,3 +615,119 @@ describe("PropertyPanel — flat Layout currentPct basis (currentPct follow-up f
RENDER_TIMEOUT_MS,
);
});

// Media fixtures (Plan 4 Task 7): the three tag values resolveEditingSections
// turns `media` on for (video/audio/img). Each carries no text fields (so the
// Text group never renders) and sets `element` to a real media node so the
// FlatMediaSection reads a live media element. `as never` casts around the
// element-type mismatch with baseElement()'s HTMLDivElement.
function videoElement() {
return {
...baseElement(),
id: "s1-bg",
selector: "#s1-bg",
label: "S1 Background",
tagName: "video",
textFields: [],
element: document.createElement("video"),
};
}

function imageElement() {
return {
...baseElement(),
id: "s1-img",
selector: "#s1-img",
label: "S1 Image",
tagName: "img",
textFields: [],
element: document.createElement("img"),
};
}

function audioElement() {
return {
...baseElement(),
id: "s1-audio",
selector: "#s1-audio",
label: "S1 Audio",
tagName: "audio",
textFields: [],
element: document.createElement("audio"),
};
}

// All FlatGroup titles currently mounted (open row + every collapsed row).
function flatGroupTitles(host: HTMLElement): string[] {
const open = Array.from(
host.querySelectorAll('[data-flat-group-open="true"] .text-panel-text-0'),
).map((el) => el.textContent ?? "");
const collapsed = Array.from(
host.querySelectorAll('[data-flat-group-collapsed="true"] .text-panel-text-2'),
).map((el) => el.textContent ?? "");
return [...open, ...collapsed];
}

describe("PropertyPanel — Media group (Plan 4)", () => {
it(
"renders the flat Media group and not the legacy MediaSection, for a video element",
async () => {
const { host, root } = await renderPanel(true, videoElement() as never);
// A Media FlatGroup exists (open or collapsed).
expect(flatGroupTitles(host)).toContain("Media");
// The legacy MediaSection renders its rows inside a `Section` whose
// data-panel-section slug is the media title ("video"/"image"/"audio").
// On the flat path it's fully replaced, so none of those may appear.
expect(host.querySelector('[data-panel-section="video"]')).toBeNull();
expect(host.querySelector('[data-panel-section="image"]')).toBeNull();
expect(host.querySelector('[data-panel-section="audio"]')).toBeNull();
act(() => root.unmount());
},
RENDER_TIMEOUT_MS,
);

it(
"one-open accordion: opening Media closes whichever other group was open, and vice versa (5-way exclusivity)",
async () => {
// videoElement() has canEditStyles: true and no text fields, so Style is
// the default-open group; Layout and Media render collapsed alongside it.
const { host, root } = await renderPanel(true, videoElement() as never);
expect(openGroupText(host)).toContain("Style");

// Opening Media closes Style.
openFlatGroup(host, "Media");
const afterMedia = openGroupText(host);
expect(afterMedia).toContain("Media");
expect(afterMedia).not.toContain("Style");

// Reverse direction: opening Layout closes Media — same shared openGroupId.
openFlatGroup(host, "Layout");
const afterLayout = openGroupText(host);
expect(afterLayout).toContain("Layout");
expect(afterLayout).not.toContain("Media");
act(() => root.unmount());
},
RENDER_TIMEOUT_MS,
);

it(
"gates the Media group exactly like the legacy MediaSection: present for video/img/audio, absent for a plain div/text element",
async () => {
for (const fixture of [videoElement, imageElement, audioElement]) {
const { host, root } = await renderPanel(true, fixture() as never);
expect(flatGroupTitles(host)).toContain("Media");
act(() => root.unmount());
}

// baseElement() is a plain <div> with text — sections.media is false, so
// no Media group (flat or legacy) may render.
const { host, root } = await renderPanel(true);
expect(flatGroupTitles(host)).not.toContain("Media");
expect(host.querySelector('[data-panel-section="video"]')).toBeNull();
expect(host.querySelector('[data-panel-section="image"]')).toBeNull();
expect(host.querySelector('[data-panel-section="audio"]')).toBeNull();
act(() => root.unmount());
},
RENDER_TIMEOUT_MS,
);
});
41 changes: 28 additions & 13 deletions packages/studio/src/components/editor/PropertyPanelFlat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import { FlatTextSection } from "./propertyPanelFlatTextSection";
import { FlatStyleSection } from "./propertyPanelFlatStyleSections";
import { FlatLayoutSection } from "./propertyPanelFlatLayoutSection";
import { FlatMotionSection } from "./propertyPanelFlatMotionSection";
import { FlatMediaSection } from "./propertyPanelFlatMediaSection";
import { deriveElementTiming } from "./propertyPanelFlatTimingDerivation";
import { createGsapLivePreview } from "./gsapLivePreview";
import { formatTextFieldPreview, StyleSections } from "./propertyPanelSections";
import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability";
import { ColorGradingSection } from "./propertyPanelColorGradingSection";
import { MediaSection } from "./propertyPanelMediaSection";

type EditingSections = ReturnType<typeof resolveEditingSections>;

Expand All @@ -41,8 +41,8 @@ const EMPTY_GSAP_EFFECT_HANDLERS = {
* (same one-directional-import precedent as FlatTextSection). Rendered only
* when STUDIO_FLAT_INSPECTOR_ENABLED is on; owns the one-open/pin group state.
*
* The Text/Style/Layout/Motion groups share the one-open accordion. The legacy
* Media and Color-Grading sections render unchanged below the flat groups.
* The Text/Style/Layout/Motion/Media groups share the one-open accordion. The
* legacy Color-Grading section renders unchanged below the flat groups.
*/
// fallow-ignore-next-line complexity
export function PropertyPanelFlat({
Expand Down Expand Up @@ -215,7 +215,13 @@ export function PropertyPanelFlat({
// switching the selection re-mounts this component and re-derives the
// default instead of preserving stale state across unrelated elements.
const [openGroupId, setOpenGroupId] = useState<string>(() =>
isTextEditableSelection(element) ? "text" : showEditableSections ? "style" : "layout",
isTextEditableSelection(element)
? "text"
: showEditableSections
? "style"
: sections.media
? "media"
: "layout",
);
const [pinnedGroupIds, setPinnedGroupIds] = useState<string[]>([]);

Expand Down Expand Up @@ -427,15 +433,24 @@ export function PropertyPanelFlat({
/>
)}
{sections.media && (
<MediaSection
projectDir={projectDir}
element={element}
styles={styles}
onSetStyle={onSetStyle}
onSetAttribute={onSetAttribute}
onSetHtmlAttribute={onSetHtmlAttribute}
onRemoveBackground={onRemoveBackground}
/>
<FlatGroup
title="Media"
isOpen={openGroupId === "media" || pinnedGroupIds.includes("media")}
isPinned={pinnedGroupIds.includes("media")}
onToggleOpen={() => toggleOpen("media")}
onTogglePin={() => togglePin("media")}
summary={element.tagName}
>
<FlatMediaSection
projectDir={projectDir}
element={element}
styles={styles}
onSetStyle={onSetStyle}
onSetAttribute={onSetAttribute}
onSetHtmlAttribute={onSetHtmlAttribute}
onRemoveBackground={onRemoveBackground}
/>
</FlatGroup>
)}
{showEditableSections && (
<StyleSections
Expand Down
Loading
Loading