From 858d1d4cce2d9377546101d1b4944478627ca7b5 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Wed, 18 Mar 2026 19:49:46 +1100 Subject: [PATCH] frame bg: make legacy scenes opt-in --- packages/element/src/newElement.ts | 5 ++- packages/element/src/renderElement.ts | 2 +- packages/element/src/types.ts | 1 + .../excalidraw/actions/actionProperties.tsx | 8 ++++- packages/excalidraw/data/restore.ts | 4 +++ packages/excalidraw/renderer/staticScene.ts | 3 ++ .../excalidraw/renderer/staticSvgScene.ts | 2 ++ .../excalidraw/tests/data/restore.test.ts | 33 +++++++++++++++++++ .../excalidraw/tests/scene/export.test.ts | 21 ++++++++++++ 9 files changed, 76 insertions(+), 3 deletions(-) diff --git a/packages/element/src/newElement.ts b/packages/element/src/newElement.ts index ec50a81ff2..9a6c45b292 100644 --- a/packages/element/src/newElement.ts +++ b/packages/element/src/newElement.ts @@ -10,6 +10,7 @@ import { getFontString, getUpdatedTimestamp, getLineHeight, + isTransparent, } from "@excalidraw/common"; import type { Radians } from "@excalidraw/math"; @@ -185,11 +186,13 @@ export const newFrameElement = ( name?: string; } & ElementConstructorOpts, ): NonDeleted => { + const frameBase = _newElementBase("frame", opts); const frameElement = newElementWith( { - ..._newElementBase("frame", opts), + ...frameBase, type: "frame", name: opts?.name || null, + backgroundEnabled: !isTransparent(frameBase.backgroundColor), }, {}, ); diff --git a/packages/element/src/renderElement.ts b/packages/element/src/renderElement.ts index 3def2f4e24..976a63ffca 100644 --- a/packages/element/src/renderElement.ts +++ b/packages/element/src/renderElement.ts @@ -787,7 +787,7 @@ export const renderFrameBackground = ( roundCorners?: boolean; }, ) => { - if (isTransparent(frame.backgroundColor)) { + if (!frame.backgroundEnabled || isTransparent(frame.backgroundColor)) { return; } diff --git a/packages/element/src/types.ts b/packages/element/src/types.ts index 8067342a20..d50729f476 100644 --- a/packages/element/src/types.ts +++ b/packages/element/src/types.ts @@ -163,6 +163,7 @@ export type InitializedExcalidrawImageElement = MarkNonNullable< export type ExcalidrawFrameElement = _ExcalidrawElementBase & { type: "frame"; name: string | null; + backgroundEnabled: boolean; }; export type ExcalidrawMagicFrameElement = _ExcalidrawElementBase & { diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index db3c5195a5..e88c3436f4 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -417,7 +417,13 @@ export const actionChangeBackgroundColor = register< }); } else { nextElements = changeProperty(elements, appState, (el) => { - return hasBackground(el.type) || isFrameElement(el) + if (isFrameElement(el)) { + return newElementWith(el, { + backgroundColor: value.currentItemBackgroundColor, + backgroundEnabled: !isTransparent(value.currentItemBackgroundColor), + }); + } + return hasBackground(el.type) ? newElementWith(el, { backgroundColor: value.currentItemBackgroundColor, }) diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index 5c81c657f3..25f2033f94 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -505,9 +505,13 @@ export const restoreElement = ( case "embeddable": return restoreElementWithProperties(element, {}); case "magicframe": + return restoreElementWithProperties(element, { + name: element.name ?? null, + }); case "frame": return restoreElementWithProperties(element, { name: element.name ?? null, + backgroundEnabled: (element as any).backgroundEnabled ?? false, }); // Don't use default case so as to catch a missing an element type case. diff --git a/packages/excalidraw/renderer/staticScene.ts b/packages/excalidraw/renderer/staticScene.ts index 70970010d9..462ddfdd8b 100644 --- a/packages/excalidraw/renderer/staticScene.ts +++ b/packages/excalidraw/renderer/staticScene.ts @@ -336,6 +336,9 @@ const _renderStaticScene = ({ if (!isFrameElement(frame) || renderedFrameBackgrounds.has(frame.id)) { return; } + if (!frame.backgroundEnabled) { + return; + } frameBackgroundByElementId.set(element.id, frame); renderedFrameBackgrounds.add(frame.id); diff --git a/packages/excalidraw/renderer/staticSvgScene.ts b/packages/excalidraw/renderer/staticSvgScene.ts index 646f693ac4..0798f19343 100644 --- a/packages/excalidraw/renderer/staticSvgScene.ts +++ b/packages/excalidraw/renderer/staticSvgScene.ts @@ -731,6 +731,7 @@ export const renderSceneToSvg = ( const renderFrameBackgroundNode = (frame: ExcalidrawFrameElement) => { if ( !frame || + !frame.backgroundEnabled || !frame.backgroundColor || isTransparent(frame.backgroundColor) ) { @@ -793,6 +794,7 @@ export const renderSceneToSvg = ( if ( !isFrameElement(frame) || renderedFrameBackgrounds.has(frame.id) || + !frame.backgroundEnabled || !frame.backgroundColor || isTransparent(frame.backgroundColor) ) { diff --git a/packages/excalidraw/tests/data/restore.test.ts b/packages/excalidraw/tests/data/restore.test.ts index 4b06601bb2..1ed8b5f059 100644 --- a/packages/excalidraw/tests/data/restore.test.ts +++ b/packages/excalidraw/tests/data/restore.test.ts @@ -11,6 +11,7 @@ import type { LocalPoint } from "@excalidraw/math"; import type { ExcalidrawArrowElement, ExcalidrawElement, + ExcalidrawFrameElement, ExcalidrawFreeDrawElement, ExcalidrawLinearElement, ExcalidrawTextElement, @@ -81,6 +82,38 @@ describe("restoreElements", () => { ).toEqual([expect.objectContaining({ isDeleted: true })]); }); + it("should disable frame background for legacy frames missing backgroundEnabled", () => { + const frame = API.createElement({ + type: "frame", + backgroundColor: "#ffc9c9", + }); + const legacyFrame = { ...frame } as any; + delete legacyFrame.backgroundEnabled; + + const restoredFrame = restore.restoreElements( + [legacyFrame], + null, + )[0] as ExcalidrawFrameElement; + + expect(restoredFrame.backgroundColor).toBe("#ffc9c9"); + expect(restoredFrame.backgroundEnabled).toBe(false); + }); + + it("should preserve frame backgroundEnabled when present", () => { + const frame = API.createElement({ + type: "frame", + backgroundColor: "#ffc9c9", + }); + frame.backgroundEnabled = true; + + const restoredFrame = restore.restoreElements( + [frame], + null, + )[0] as ExcalidrawFrameElement; + + expect(restoredFrame.backgroundEnabled).toBe(true); + }); + it("should restore text element correctly passing value for each attribute", () => { const textElement = API.createElement({ type: "text", diff --git a/packages/excalidraw/tests/scene/export.test.ts b/packages/excalidraw/tests/scene/export.test.ts index eb24e5da11..5585920ae5 100644 --- a/packages/excalidraw/tests/scene/export.test.ts +++ b/packages/excalidraw/tests/scene/export.test.ts @@ -364,6 +364,27 @@ describe("exporting frames", () => { expect(emptyBgSvg.querySelector('rect[fill=""]')).toBeNull(); }); + it("should not render SVG frame background when background is disabled", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + backgroundColor: "#ffc9c9", + }); + frame.backgroundEnabled = false; + + const svg = await exportToSvg({ + elements: [frame], + files: null, + exportPadding: 0, + exportingFrame: frame, + }); + + expect(svg.querySelector('rect[fill="#ffc9c9"]')).toBeNull(); + }); + it("should filter non-overlapping elements when exporting a frame", async () => { const frame = API.createElement({ type: "frame",