Browse Source

fix: default light theme splash 🔧 (#5660)

Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
Abdullah Adeel 2 years ago
parent
commit
7eaf47c9d4

+ 0 - 1
src/actions/manager.tsx

@@ -137,7 +137,6 @@ export class ActionManager {
    */
   renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
     const canvasActions = this.app.props.UIOptions.canvasActions;
-
     if (
       this.actions[name] &&
       "PanelComponent" in this.actions[name] &&

+ 6 - 9
src/components/App.tsx

@@ -552,10 +552,6 @@ class App extends React.Component<AppProps, AppState> {
                     typeof this.props?.zenModeEnabled === "undefined" &&
                     this.state.zenModeEnabled
                   }
-                  showThemeBtn={
-                    typeof this.props?.theme === "undefined" &&
-                    this.props.UIOptions.canvasActions.theme
-                  }
                   libraryReturnUrl={this.props.libraryReturnUrl}
                   UIOptions={this.props.UIOptions}
                   focusContainer={this.focusContainer}
@@ -645,7 +641,8 @@ class App extends React.Component<AppProps, AppState> {
         let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
         let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
         let gridSize = actionResult?.appState?.gridSize || null;
-        let theme = actionResult?.appState?.theme || THEME.LIGHT;
+        const theme =
+          actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
         let name = actionResult?.appState?.name ?? this.state.name;
         if (typeof this.props.viewModeEnabled !== "undefined") {
           viewModeEnabled = this.props.viewModeEnabled;
@@ -659,10 +656,6 @@ class App extends React.Component<AppProps, AppState> {
           gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
         }
 
-        if (typeof this.props.theme !== "undefined") {
-          theme = this.props.theme;
-        }
-
         if (typeof this.props.name !== "undefined") {
           name = this.props.name;
         }
@@ -755,6 +748,9 @@ class App extends React.Component<AppProps, AppState> {
       );
     }
 
+    if (this.props.theme) {
+      this.setState({ theme: this.props.theme });
+    }
     if (!this.state.isLoading) {
       this.setState({ isLoading: true });
     }
@@ -784,6 +780,7 @@ class App extends React.Component<AppProps, AppState> {
     const scene = restore(initialData, null, null);
     scene.appState = {
       ...scene.appState,
+      theme: this.props.theme || scene.appState.theme,
       // we're falling back to current (pre-init) state when deciding
       // whether to open the library, to handle a case where we
       // update the state outside of initialData (e.g. when loading the app

+ 1 - 9
src/components/BackgroundPickerAndDarkModeToggle.tsx

@@ -1,20 +1,12 @@
-import React from "react";
 import { ActionManager } from "../actions/manager";
-import { AppState } from "../types";
 
 export const BackgroundPickerAndDarkModeToggle = ({
-  appState,
-  setAppState,
   actionManager,
-  showThemeBtn,
 }: {
   actionManager: ActionManager;
-  appState: AppState;
-  setAppState: React.Component<any, AppState>["setState"];
-  showThemeBtn: boolean;
 }) => (
   <div style={{ display: "flex" }}>
     {actionManager.renderAction("changeViewBackgroundColor")}
-    {showThemeBtn && actionManager.renderAction("toggleTheme")}
+    {actionManager.renderAction("toggleTheme")}
   </div>
 );

+ 3 - 1
src/components/InitializeApp.tsx

@@ -2,10 +2,12 @@ import React, { useEffect, useState } from "react";
 
 import { LoadingMessage } from "./LoadingMessage";
 import { defaultLang, Language, languages, setLanguage } from "../i18n";
+import { Theme } from "../element/types";
 
 interface Props {
   langCode: Language["code"];
   children: React.ReactElement;
+  theme?: Theme;
 }
 
 export const InitializeApp = (props: Props) => {
@@ -21,5 +23,5 @@ export const InitializeApp = (props: Props) => {
     updateLang();
   }, [props.langCode]);
 
-  return loading ? <LoadingMessage /> : props.children;
+  return loading ? <LoadingMessage theme={props.theme} /> : props.children;
 };

+ 1 - 9
src/components/LayerUI.tsx

@@ -53,7 +53,6 @@ interface LayerUIProps {
   onPenModeToggle: () => void;
   onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
   showExitZenModeBtn: boolean;
-  showThemeBtn: boolean;
   langCode: Language["code"];
   isCollaborating: boolean;
   renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
@@ -78,7 +77,6 @@ const LayerUI = ({
   onPenModeToggle,
   onInsertElements,
   showExitZenModeBtn,
-  showThemeBtn,
   isCollaborating,
   renderTopRightUI,
   renderCustomFooter,
@@ -209,12 +207,7 @@ const LayerUI = ({
               />
             )}
           </Stack.Row>
-          <BackgroundPickerAndDarkModeToggle
-            appState={appState}
-            actionManager={actionManager}
-            setAppState={setAppState}
-            showThemeBtn={showThemeBtn}
-          />
+          <BackgroundPickerAndDarkModeToggle actionManager={actionManager} />
           {appState.fileHandle && (
             <>{actionManager.renderAction("saveToActiveFile")}</>
           )}
@@ -424,7 +417,6 @@ const LayerUI = ({
           canvas={canvas}
           isCollaborating={isCollaborating}
           renderCustomFooter={renderCustomFooter}
-          showThemeBtn={showThemeBtn}
           onImageAction={onImageAction}
           renderTopRightUI={renderTopRightUI}
           renderCustomStats={renderCustomStats}

+ 12 - 2
src/components/LoadingMessage.tsx

@@ -1,8 +1,14 @@
 import { t } from "../i18n";
 import { useState, useEffect } from "react";
 import Spinner from "./Spinner";
+import clsx from "clsx";
+import { THEME } from "../constants";
+import { Theme } from "../element/types";
 
-export const LoadingMessage: React.FC<{ delay?: number }> = ({ delay }) => {
+export const LoadingMessage: React.FC<{ delay?: number; theme?: Theme }> = ({
+  delay,
+  theme,
+}) => {
   const [isWaiting, setIsWaiting] = useState(!!delay);
 
   useEffect(() => {
@@ -20,7 +26,11 @@ export const LoadingMessage: React.FC<{ delay?: number }> = ({ delay }) => {
   }
 
   return (
-    <div className="LoadingMessage">
+    <div
+      className={clsx("LoadingMessage", {
+        "LoadingMessage--dark": theme === THEME.DARK,
+      })}
+    >
       <div>
         <Spinner />
       </div>

+ 1 - 10
src/components/MobileMenu.tsx

@@ -38,7 +38,6 @@ type MobileMenuProps = {
     isMobile: boolean,
     appState: AppState,
   ) => JSX.Element | null;
-  showThemeBtn: boolean;
   onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
   renderTopRightUI?: (
     isMobile: boolean,
@@ -61,7 +60,6 @@ export const MobileMenu = ({
   canvas,
   isCollaborating,
   renderCustomFooter,
-  showThemeBtn,
   onImageAction,
   renderTopRightUI,
   renderCustomStats,
@@ -171,14 +169,7 @@ export const MobileMenu = ({
             onClick={onCollabButtonClick}
           />
         )}
-        {
-          <BackgroundPickerAndDarkModeToggle
-            actionManager={actionManager}
-            appState={appState}
-            setAppState={setAppState}
-            showThemeBtn={showThemeBtn}
-          />
-        }
+        {<BackgroundPickerAndDarkModeToggle actionManager={actionManager} />}
       </>
     );
   };

+ 1 - 1
src/constants.ts

@@ -149,7 +149,7 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
     export: { saveFileToDisk: true },
     loadScene: true,
     saveToActiveFile: true,
-    theme: true,
+    toggleTheme: null,
     saveAsImage: true,
   },
 };

+ 7 - 0
src/css/app.scss

@@ -1,3 +1,5 @@
+@import "open-color/open-color.scss";
+
 .visually-hidden {
   position: absolute !important;
   height: 1px;
@@ -30,3 +32,8 @@
     font-size: 0.8em;
   }
 }
+
+.LoadingMessage--dark {
+  background-color: $oc-black;
+  color: $oc-white;
+}

+ 1 - 0
src/excalidraw-app/app_constants.ts

@@ -34,6 +34,7 @@ export const STORAGE_KEYS = {
   LOCAL_STORAGE_APP_STATE: "excalidraw-state",
   LOCAL_STORAGE_COLLAB: "excalidraw-collab",
   LOCAL_STORAGE_LIBRARY: "excalidraw-library",
+  LOCAL_STORAGE_THEME: "excalidraw-theme",
   VERSION_DATA_STATE: "version-dataState",
   VERSION_FILES: "version-files",
 } as const;

+ 18 - 0
src/excalidraw-app/index.tsx

@@ -9,6 +9,7 @@ import {
   APP_NAME,
   COOKIES,
   EVENT,
+  THEME,
   TITLE_TIMEOUT,
   VERSION_TIMEOUT,
 } from "../constants";
@@ -17,6 +18,7 @@ import {
   ExcalidrawElement,
   FileId,
   NonDeletedExcalidrawElement,
+  Theme,
 } from "../element/types";
 import { useCallbackRefState } from "../hooks/useCallbackRefState";
 import { t } from "../i18n";
@@ -512,6 +514,18 @@ const ExcalidrawWrapper = () => {
     languageDetector.cacheUserLanguage(langCode);
   }, [langCode]);
 
+  const [theme, setTheme] = useState<Theme>(
+    () =>
+      localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_THEME) ||
+      // FIXME migration from old LS scheme. Can be removed later. #5660
+      importFromLocalStorage().appState?.theme ||
+      THEME.LIGHT,
+  );
+
+  useEffect(() => {
+    localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, theme);
+  }, [theme]);
+
   const onChange = (
     elements: readonly ExcalidrawElement[],
     appState: AppState,
@@ -521,6 +535,8 @@ const ExcalidrawWrapper = () => {
       collabAPI.syncElements(elements);
     }
 
+    setTheme(appState.theme);
+
     // this check is redundant, but since this is a hot path, it's best
     // not to evaludate the nested expression every time
     if (!LocalData.isSavePaused()) {
@@ -710,6 +726,7 @@ const ExcalidrawWrapper = () => {
         onPointerUpdate={collabAPI?.onPointerUpdate}
         UIOptions={{
           canvasActions: {
+            toggleTheme: true,
             export: {
               onExportToBackend,
               renderCustomUI: (elements, appState, files) => {
@@ -739,6 +756,7 @@ const ExcalidrawWrapper = () => {
         handleKeyboardGlobally={true}
         onLibraryChange={onLibraryChange}
         autoFocus={true}
+        theme={theme}
       />
       {excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
       {errorMessage && (

+ 2 - 0
src/packages/excalidraw/CHANGELOG.md

@@ -17,11 +17,13 @@ Please add the latest change on the top under the correct section.
 
 #### Features
 
+- Support [theme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#theme) to be semi-controlled [#5660](https://github.com/excalidraw/excalidraw/pull/5660).
 - Added support for storing [`customData`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#storing-custom-data-to-excalidraw-elements) on Excalidraw elements [#5592].
 - Added `exportPadding?: number;` to [exportToCanvas](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exporttocanvas) and [exportToBlob](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exporttoblob). The default value of the padding is 10.
 
 #### Breaking Changes
 
+- `props.UIOptions.canvasActions.theme` is now renamed to `props.UIOptions.canvasActions.toggleTheme` [#5660](https://github.com/excalidraw/excalidraw/pull/5660).
 - `setToastMessage` API is now renamed to `setToast` API and the function signature is also updated [#5427](https://github.com/excalidraw/excalidraw/pull/5427). You can also pass `duration` and `closable` attributes along with `message`.
 
 ## 0.12.0 (2022-07-07)

+ 4 - 2
src/packages/excalidraw/README.md

@@ -637,7 +637,9 @@ If supplied, this URL will be used when user tries to install a library from [li
 
 #### `theme`
 
-This prop controls Excalidraw's theme. When supplied, the value takes precedence over `intialData.appState.theme`, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app. You can use [`THEME`](#THEME-1) to specify the theme.
+This prop controls Excalidraw's theme. When supplied, the value takes precedence over `intialData.appState.theme`, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app unless `UIOptions.canvasActions.toggleTheme` is set to `true`, in which case the `theme` prop will control Excalidraw's default theme with ability to allow theme switching (you must take care of updating the `theme` prop when you detect a change to `appState.theme` from the [onChange](#onChange) callback).
+
+You can use [`THEME`](#THEME-1) to specify the theme.
 
 #### `name`
 
@@ -660,7 +662,7 @@ This prop can be used to customise UI of Excalidraw. Currently we support custom
 | `export` | false &#124; [exportOpts](#exportOpts) | <pre>{ saveFileToDisk: true }</pre> | This prop allows to customize the UI inside the export dialog. By default it shows the "saveFileToDisk". If this prop is `false` the export button will not be rendered. For more details visit [`exportOpts`](#exportOpts). |
 | `loadScene` | boolean | true | Implies whether to show `Load button` |
 | `saveToActiveFile` | boolean | true | Implies whether to show `Save button` to save to current file |
-| `theme` | boolean | true | Implies whether to show `Theme toggle` |
+| `toggleTheme` | boolean &#124; null | null | Implies whether to show `Theme toggle`. When defined as `boolean`, takes precedence over [`props.theme`](#theme) to show `Theme toggle` |
 | `saveAsImage` | boolean | true | Implies whether to show `Save as image button` |
 
 ##### `dockedSidebarBreakpoint`

+ 8 - 1
src/packages/excalidraw/index.tsx

@@ -56,6 +56,13 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
       DEFAULT_UI_OPTIONS.canvasActions.export.saveFileToDisk;
   }
 
+  if (
+    UIOptions.canvasActions.toggleTheme === null &&
+    typeof theme === "undefined"
+  ) {
+    UIOptions.canvasActions.toggleTheme = true;
+  }
+
   useEffect(() => {
     // Block pinch-zooming on iOS outside of the content area
     const handleTouchMove = (event: TouchEvent) => {
@@ -75,7 +82,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
   }, []);
 
   return (
-    <InitializeApp langCode={langCode}>
+    <InitializeApp langCode={langCode} theme={theme}>
       <Provider unstable_createStore={() => jotaiStore} scope={jotaiScope}>
         <App
           onChange={onChange}

+ 27 - 6
src/tests/packages/excalidraw.test.tsx

@@ -88,21 +88,42 @@ describe("<Excalidraw/>", () => {
   });
 
   describe("Test theme prop", () => {
-    it('should show the dark mode toggle when the theme prop is "undefined"', async () => {
+    it("should show the theme toggle by default", async () => {
       const { container } = await render(<Excalidraw />);
       expect(h.state.theme).toBe(THEME.LIGHT);
-
       const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
-
       expect(darkModeToggle).toBeTruthy();
     });
 
-    it('should not show the dark mode toggle when the theme prop is not "undefined"', async () => {
+    it("should not show theme toggle when the theme prop is defined", async () => {
       const { container } = await render(<Excalidraw theme="dark" />);
       expect(h.state.theme).toBe(THEME.DARK);
-
       expect(queryByTestId(container, "toggle-dark-mode")).toBe(null);
     });
+
+    it("should show theme mode toggle when `UIOptions.canvasActions.toggleTheme` is true", async () => {
+      const { container } = await render(
+        <Excalidraw
+          theme={THEME.DARK}
+          UIOptions={{ canvasActions: { toggleTheme: true } }}
+        />,
+      );
+      expect(h.state.theme).toBe(THEME.DARK);
+      const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
+      expect(darkModeToggle).toBeTruthy();
+    });
+
+    it("should not show theme toggle when `UIOptions.canvasActions.toggleTheme` is false", async () => {
+      const { container } = await render(
+        <Excalidraw
+          UIOptions={{ canvasActions: { toggleTheme: false } }}
+          theme={THEME.DARK}
+        />,
+      );
+      expect(h.state.theme).toBe(THEME.DARK);
+      const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
+      expect(darkModeToggle).toBeFalsy();
+    });
   });
 
   describe("Test name prop", () => {
@@ -214,7 +235,7 @@ describe("<Excalidraw/>", () => {
 
       it("should hide the theme toggle when theme is false", async () => {
         const { container } = await render(
-          <Excalidraw UIOptions={{ canvasActions: { theme: false } }} />,
+          <Excalidraw UIOptions={{ canvasActions: { toggleTheme: false } }} />,
         );
 
         expect(queryByTestId(container, "toggle-dark-mode")).toBeNull();

+ 5 - 1
src/types.ts

@@ -344,13 +344,17 @@ export type ExportOpts = {
   ) => JSX.Element;
 };
 
+// NOTE at the moment, if action name coressponds to canvasAction prop, its
+// truthiness value will determine whether the action is rendered or not
+// (see manager renderAction). We also override canvasAction values in
+// excalidraw package index.tsx.
 type CanvasActions = {
   changeViewBackgroundColor?: boolean;
   clearCanvas?: boolean;
   export?: false | ExportOpts;
   loadScene?: boolean;
   saveToActiveFile?: boolean;
-  theme?: boolean;
+  toggleTheme?: boolean | null;
   saveAsImage?: boolean;
 };