feat(editor): sync export theme with ui theme (#10903)
This commit is contained in:
@@ -300,7 +300,8 @@ export const actionExportWithDarkMode = register<
|
|||||||
name: "exportWithDarkMode",
|
name: "exportWithDarkMode",
|
||||||
label: "imageExportDialog.label.darkMode",
|
label: "imageExportDialog.label.darkMode",
|
||||||
trackEvent: { category: "export", action: "toggleTheme" },
|
trackEvent: { category: "export", action: "toggleTheme" },
|
||||||
perform: (_elements, appState, value) => {
|
perform: (_elements, appState, value, app) => {
|
||||||
|
app.sessionExportThemeOverride = value ? THEME.DARK : THEME.LIGHT;
|
||||||
return {
|
return {
|
||||||
appState: { ...appState, exportWithDarkMode: value },
|
appState: { ...appState, exportWithDarkMode: value },
|
||||||
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
||||||
|
|||||||
@@ -595,6 +595,7 @@ const gesture: Gesture = {
|
|||||||
class App extends React.Component<AppProps, AppState> {
|
class App extends React.Component<AppProps, AppState> {
|
||||||
canvas: AppClassProperties["canvas"];
|
canvas: AppClassProperties["canvas"];
|
||||||
interactiveCanvas: AppClassProperties["interactiveCanvas"] = null;
|
interactiveCanvas: AppClassProperties["interactiveCanvas"] = null;
|
||||||
|
public sessionExportThemeOverride: AppState["theme"] | undefined;
|
||||||
rc: RoughCanvas;
|
rc: RoughCanvas;
|
||||||
unmounted: boolean = false;
|
unmounted: boolean = false;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@@ -710,6 +711,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state = {
|
this.state = {
|
||||||
...defaultAppState,
|
...defaultAppState,
|
||||||
theme,
|
theme,
|
||||||
|
exportWithDarkMode: theme === THEME.DARK,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
...this.getCanvasOffsets(),
|
...this.getCanvasOffsets(),
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
@@ -3241,6 +3243,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const elements = this.scene.getElementsIncludingDeleted();
|
const elements = this.scene.getElementsIncludingDeleted();
|
||||||
const elementsMap = this.scene.getElementsMapIncludingDeleted();
|
const elementsMap = this.scene.getElementsMapIncludingDeleted();
|
||||||
|
|
||||||
|
const shouldExportWithDarkMode =
|
||||||
|
(this.sessionExportThemeOverride ?? this.state.theme) === THEME.DARK;
|
||||||
|
|
||||||
|
if (this.state.exportWithDarkMode !== shouldExportWithDarkMode) {
|
||||||
|
this.setState({ exportWithDarkMode: shouldExportWithDarkMode });
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.state.showWelcomeScreen && !elements.length) {
|
if (!this.state.showWelcomeScreen && !elements.length) {
|
||||||
this.setState({ showWelcomeScreen: true });
|
this.setState({ showWelcomeScreen: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ type ImageExportModalProps = {
|
|||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
onExportImage: AppClassProperties["onExportImage"];
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
name: string;
|
name: string;
|
||||||
|
exportWithDarkMode: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImageExportModal = ({
|
const ImageExportModal = ({
|
||||||
@@ -68,6 +69,7 @@ const ImageExportModal = ({
|
|||||||
actionManager,
|
actionManager,
|
||||||
onExportImage,
|
onExportImage,
|
||||||
name,
|
name,
|
||||||
|
exportWithDarkMode,
|
||||||
}: ImageExportModalProps) => {
|
}: ImageExportModalProps) => {
|
||||||
const hasSelection = isSomeElementSelected(
|
const hasSelection = isSomeElementSelected(
|
||||||
elementsSnapshot,
|
elementsSnapshot,
|
||||||
@@ -79,15 +81,13 @@ const ImageExportModal = ({
|
|||||||
const [exportWithBackground, setExportWithBackground] = useState(
|
const [exportWithBackground, setExportWithBackground] = useState(
|
||||||
appStateSnapshot.exportBackground,
|
appStateSnapshot.exportBackground,
|
||||||
);
|
);
|
||||||
const [exportDarkMode, setExportDarkMode] = useState(
|
|
||||||
appStateSnapshot.exportWithDarkMode,
|
|
||||||
);
|
|
||||||
const [embedScene, setEmbedScene] = useState(
|
const [embedScene, setEmbedScene] = useState(
|
||||||
appStateSnapshot.exportEmbedScene,
|
appStateSnapshot.exportEmbedScene,
|
||||||
);
|
);
|
||||||
const [exportScale, setExportScale] = useState(appStateSnapshot.exportScale);
|
const [exportScale, setExportScale] = useState(appStateSnapshot.exportScale);
|
||||||
|
|
||||||
const previewRef = useRef<HTMLDivElement>(null);
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
|
const previewRenderRequestIdRef = useRef(0);
|
||||||
const [renderError, setRenderError] = useState<Error | null>(null);
|
const [renderError, setRenderError] = useState<Error | null>(null);
|
||||||
|
|
||||||
const { onCopy, copyStatus, resetCopyStatus } = useCopyStatus();
|
const { onCopy, copyStatus, resetCopyStatus } = useCopyStatus();
|
||||||
@@ -99,7 +99,7 @@ const ImageExportModal = ({
|
|||||||
}, [
|
}, [
|
||||||
projectName,
|
projectName,
|
||||||
exportWithBackground,
|
exportWithBackground,
|
||||||
exportDarkMode,
|
exportWithDarkMode,
|
||||||
exportScale,
|
exportScale,
|
||||||
embedScene,
|
embedScene,
|
||||||
resetCopyStatus,
|
resetCopyStatus,
|
||||||
@@ -122,13 +122,18 @@ const ImageExportModal = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestId = ++previewRenderRequestIdRef.current;
|
||||||
|
const isStaleRequest = () => {
|
||||||
|
return requestId !== previewRenderRequestIdRef.current;
|
||||||
|
};
|
||||||
|
|
||||||
exportToCanvas({
|
exportToCanvas({
|
||||||
elements: exportedElements,
|
elements: exportedElements,
|
||||||
appState: {
|
appState: {
|
||||||
...appStateSnapshot,
|
...appStateSnapshot,
|
||||||
name: projectName,
|
name: projectName,
|
||||||
exportBackground: exportWithBackground,
|
exportBackground: exportWithBackground,
|
||||||
exportWithDarkMode: exportDarkMode,
|
exportWithDarkMode,
|
||||||
exportScale,
|
exportScale,
|
||||||
exportEmbedScene: embedScene,
|
exportEmbedScene: embedScene,
|
||||||
},
|
},
|
||||||
@@ -137,25 +142,41 @@ const ImageExportModal = ({
|
|||||||
maxWidthOrHeight: Math.max(maxWidth, maxHeight),
|
maxWidthOrHeight: Math.max(maxWidth, maxHeight),
|
||||||
exportingFrame,
|
exportingFrame,
|
||||||
})
|
})
|
||||||
.then((canvas) => {
|
.then(async (canvas) => {
|
||||||
|
if (isStaleRequest()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If converting to blob fails, there's some problem that will likely
|
||||||
|
// prevent preview and export (e.g. canvas too big).
|
||||||
|
try {
|
||||||
|
await canvasToBlob(canvas);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
|
||||||
|
throw new Error(t("canvasError.canvasTooBig"));
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStaleRequest()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setRenderError(null);
|
setRenderError(null);
|
||||||
// if converting to blob fails, there's some problem that will
|
previewNode.replaceChildren(canvas);
|
||||||
// likely prevent preview and export (e.g. canvas too big)
|
|
||||||
return canvasToBlob(canvas)
|
|
||||||
.then(() => {
|
|
||||||
previewNode.replaceChildren(canvas);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
if (e.name === "CANVAS_POSSIBLY_TOO_BIG") {
|
|
||||||
throw new Error(t("canvasError.canvasTooBig"));
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
if (isStaleRequest()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setRenderError(error);
|
setRenderError(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
previewRenderRequestIdRef.current += 1;
|
||||||
|
};
|
||||||
}, [
|
}, [
|
||||||
appStateSnapshot,
|
appStateSnapshot,
|
||||||
files,
|
files,
|
||||||
@@ -163,7 +184,7 @@ const ImageExportModal = ({
|
|||||||
exportingFrame,
|
exportingFrame,
|
||||||
projectName,
|
projectName,
|
||||||
exportWithBackground,
|
exportWithBackground,
|
||||||
exportDarkMode,
|
exportWithDarkMode,
|
||||||
exportScale,
|
exportScale,
|
||||||
embedScene,
|
embedScene,
|
||||||
]);
|
]);
|
||||||
@@ -233,9 +254,8 @@ const ImageExportModal = ({
|
|||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
name="exportDarkModeSwitch"
|
name="exportDarkModeSwitch"
|
||||||
checked={exportDarkMode}
|
checked={exportWithDarkMode}
|
||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
setExportDarkMode(checked);
|
|
||||||
actionManager.executeAction(
|
actionManager.executeAction(
|
||||||
actionExportWithDarkMode,
|
actionExportWithDarkMode,
|
||||||
"ui",
|
"ui",
|
||||||
@@ -399,6 +419,7 @@ export const ImageExportDialog = ({
|
|||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
onExportImage={onExportImage}
|
onExportImage={onExportImage}
|
||||||
name={name}
|
name={name}
|
||||||
|
exportWithDarkMode={appState.exportWithDarkMode}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu items if passed from host 1`] = `
|
exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu items if passed from host 1`] = `
|
||||||
<div
|
<div
|
||||||
aria-labelledby="radix-:r7t:"
|
aria-labelledby="radix-:r85:"
|
||||||
aria-orientation="vertical"
|
aria-orientation="vertical"
|
||||||
class="dropdown-menu main-menu"
|
class="dropdown-menu main-menu"
|
||||||
data-align="start"
|
data-align="start"
|
||||||
@@ -12,7 +12,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
|||||||
data-state="open"
|
data-state="open"
|
||||||
data-testid="dropdown-menu"
|
data-testid="dropdown-menu"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
id="radix-:r7u:"
|
id="radix-:r86:"
|
||||||
role="menu"
|
role="menu"
|
||||||
style="outline: none; --radix-dropdown-menu-content-transform-origin: var(--radix-popper-transform-origin); --radix-dropdown-menu-content-available-width: var(--radix-popper-available-width); --radix-dropdown-menu-content-available-height: var(--radix-popper-available-height); --radix-dropdown-menu-trigger-width: var(--radix-popper-anchor-width); --radix-dropdown-menu-trigger-height: var(--radix-popper-anchor-height); animation: none;"
|
style="outline: none; --radix-dropdown-menu-content-transform-origin: var(--radix-popper-transform-origin); --radix-dropdown-menu-content-available-width: var(--radix-popper-available-width); --radix-dropdown-menu-content-available-height: var(--radix-popper-available-height); --radix-dropdown-menu-trigger-width: var(--radix-popper-anchor-width); --radix-dropdown-menu-trigger-height: var(--radix-popper-anchor-height); animation: none;"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
|||||||
@@ -6,8 +6,16 @@ import { THEME } from "@excalidraw/common";
|
|||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { Excalidraw, Footer, MainMenu } from "../index";
|
import { Excalidraw, Footer, MainMenu } from "../index";
|
||||||
|
import { actionExportWithDarkMode } from "../actions/actionExport";
|
||||||
|
|
||||||
import { fireEvent, GlobalTestState, toggleMenu, render } from "./test-utils";
|
import {
|
||||||
|
act,
|
||||||
|
fireEvent,
|
||||||
|
GlobalTestState,
|
||||||
|
toggleMenu,
|
||||||
|
render,
|
||||||
|
waitFor,
|
||||||
|
} from "./test-utils";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -304,6 +312,47 @@ describe("<Excalidraw/>", () => {
|
|||||||
const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
|
const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
|
||||||
expect(darkModeToggle).toBe(null);
|
expect(darkModeToggle).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should sync export theme with the UI theme when there is no session override", async () => {
|
||||||
|
await render(<Excalidraw theme={THEME.DARK} />);
|
||||||
|
|
||||||
|
expect(h.state.exportWithDarkMode).toBe(true);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
h.setState({ exportWithDarkMode: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.state.exportWithDarkMode).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep the export theme override for the current session", async () => {
|
||||||
|
await render(<Excalidraw theme={THEME.LIGHT} />);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
(h.app as any).actionManager.executeAction(
|
||||||
|
actionExportWithDarkMode,
|
||||||
|
"ui",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(h.app.sessionExportThemeOverride).toBe(THEME.DARK);
|
||||||
|
expect(h.state.exportWithDarkMode).toBe(true);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
h.setState({ theme: THEME.DARK });
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
h.setState({ theme: THEME.LIGHT });
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.state.exportWithDarkMode).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test name prop", () => {
|
describe("Test name prop", () => {
|
||||||
|
|||||||
@@ -719,6 +719,7 @@ export type AppProps = Merge<
|
|||||||
export type AppClassProperties = {
|
export type AppClassProperties = {
|
||||||
props: AppProps;
|
props: AppProps;
|
||||||
state: AppState;
|
state: AppState;
|
||||||
|
sessionExportThemeOverride: App["sessionExportThemeOverride"];
|
||||||
interactiveCanvas: HTMLCanvasElement | null;
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
/** static canvas */
|
/** static canvas */
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
|
|||||||
Reference in New Issue
Block a user