Browse Source

feat: Add theme prop (#3228)

* support appearance when updating scene data

* works!

* whoops, missed a prop

* hide appearance button when prop is not set

* cleanup

* fix export + rename prop to theme

* rename to showThemeBtn, hide via react instead of css

* adapt to new state name

* add tests and css selector to target the dark mode toggle

* updated changelog and readme

* fix markdown rendering in readme

* pr feedback
Jeremy Press 4 years ago
parent
commit
84a1863233

+ 13 - 0
src/components/App.tsx

@@ -303,9 +303,11 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       viewModeEnabled = false,
       zenModeEnabled = false,
       gridModeEnabled = false,
+      theme = defaultAppState.theme,
     } = props;
     this.state = {
       ...defaultAppState,
+      theme,
       isLoading: true,
       width,
       height,
@@ -458,6 +460,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
           showExitZenModeBtn={
             typeof this.props?.zenModeEnabled === "undefined" && zenModeEnabled
           }
+          showThemeBtn={typeof this.props?.theme === "undefined"}
           libraryReturnUrl={this.props.libraryReturnUrl}
         />
         <div className="excalidraw-textEditorContainer" />
@@ -519,6 +522,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
         let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
         let gridSize = actionResult?.appState?.gridSize || null;
+        let theme = actionResult?.appState?.theme || "light";
 
         if (typeof this.props.viewModeEnabled !== "undefined") {
           viewModeEnabled = this.props.viewModeEnabled;
@@ -532,6 +536,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
           gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
         }
 
+        if (typeof this.props.theme !== "undefined") {
+          theme = this.props.theme;
+        }
+
         this.setState(
           (state) => {
             // using Object.assign instead of spread to fool TS 4.2.2+ into
@@ -547,6 +555,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
               viewModeEnabled,
               zenModeEnabled,
               gridSize,
+              theme,
             });
           },
           () => {
@@ -882,6 +891,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       this.setState({ zenModeEnabled: !!this.props.zenModeEnabled });
     }
 
+    if (prevProps.theme !== this.props.theme && this.props.theme) {
+      this.setState({ theme: this.props.theme });
+    }
+
     if (prevProps.gridModeEnabled !== this.props.gridModeEnabled) {
       this.setState({
         gridSize: this.props.gridModeEnabled ? GRID_SIZE : null,

+ 12 - 8
src/components/BackgroundPickerAndDarkModeToggle.tsx

@@ -7,20 +7,24 @@ 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")}
-    <div style={{ marginInlineStart: "0.25rem" }}>
-      <DarkModeToggle
-        value={appState.theme}
-        onChange={(theme) => {
-          setAppState({ theme });
-        }}
-      />
-    </div>
+    {showThemeBtn && (
+      <div style={{ marginInlineStart: "0.25rem" }}>
+        <DarkModeToggle
+          value={appState.theme}
+          onChange={(theme) => {
+            setAppState({ theme });
+          }}
+        />
+      </div>
+    )}
   </div>
 );

+ 2 - 1
src/components/DarkModeToggle.tsx

@@ -20,7 +20,8 @@ export const DarkModeToggle = (props: {
 
   return (
     <label
-      className={`ToolIcon ToolIcon_type_floating ToolIcon_size_M`}
+      className="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
+      data-testid="toggle-dark-mode"
       title={title}
     >
       <input

+ 4 - 0
src/components/LayerUI.tsx

@@ -53,6 +53,7 @@ interface LayerUIProps {
   onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
   zenModeEnabled: boolean;
   showExitZenModeBtn: boolean;
+  showThemeBtn: boolean;
   toggleZenMode: () => void;
   langCode: Language["code"];
   isCollaborating: boolean;
@@ -325,6 +326,7 @@ const LayerUI = ({
   onInsertElements,
   zenModeEnabled,
   showExitZenModeBtn,
+  showThemeBtn,
   toggleZenMode,
   isCollaborating,
   onExportToBackend,
@@ -441,6 +443,7 @@ const LayerUI = ({
             actionManager={actionManager}
             appState={appState}
             setAppState={setAppState}
+            showThemeBtn={showThemeBtn}
           />
         </Stack.Col>
       </Island>
@@ -671,6 +674,7 @@ const LayerUI = ({
         isCollaborating={isCollaborating}
         renderCustomFooter={renderCustomFooter}
         viewModeEnabled={viewModeEnabled}
+        showThemeBtn={showThemeBtn}
       />
     </>
   ) : (

+ 3 - 0
src/components/MobileMenu.tsx

@@ -30,6 +30,7 @@ type MobileMenuProps = {
   isCollaborating: boolean;
   renderCustomFooter?: (isMobile: boolean) => JSX.Element;
   viewModeEnabled: boolean;
+  showThemeBtn: boolean;
 };
 
 export const MobileMenu = ({
@@ -45,6 +46,7 @@ export const MobileMenu = ({
   isCollaborating,
   renderCustomFooter,
   viewModeEnabled,
+  showThemeBtn,
 }: MobileMenuProps) => {
   const renderToolbar = () => {
     return (
@@ -130,6 +132,7 @@ export const MobileMenu = ({
             actionManager={actionManager}
             appState={appState}
             setAppState={setAppState}
+            showThemeBtn={showThemeBtn}
           />
         }
       </>

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

@@ -18,6 +18,7 @@ Please add the latest change on the top under the correct section.
 
 ### Features
 
+- Add a `theme` prop to indicate Excalidraw's theme. [#3228](https://github.com/excalidraw/excalidraw/pull/3228). When this prop is passed, the theme is fully controlled by host app.
 - Support `libraryReturnUrl` prop to indicate what URL to install libraries to [#3227](https://github.com/excalidraw/excalidraw/pull/3227).
 
 ### Refactor

+ 5 - 0
src/packages/excalidraw/README.md

@@ -377,6 +377,7 @@ export default function IndexPage() {
 | [`zenModeEnabled`](#zenModeEnabled) | boolean |  | This implies if the zen mode is enabled |
 | [`gridModeEnabled`](#gridModeEnabled) | boolean |  | This implies if the grid mode is enabled |
 | [`libraryReturnUrl`](#libraryReturnUrl) | string |  | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
+| [`theme`](#theme) | `light` or `dark` |  | The theme of the Excalidraw component |
 
 #### `width`
 
@@ -538,6 +539,10 @@ This prop indicates whether the shows the grid. When supplied, the value takes p
 
 If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com). Default to `window.location.origin`.
 
+### `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.
+
 ### Extra API's
 
 #### `getSceneVersion`

+ 2 - 0
src/packages/excalidraw/index.tsx

@@ -30,6 +30,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
     zenModeEnabled,
     gridModeEnabled,
     libraryReturnUrl,
+    theme,
   } = props;
 
   useEffect(() => {
@@ -71,6 +72,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
           zenModeEnabled={zenModeEnabled}
           gridModeEnabled={gridModeEnabled}
           libraryReturnUrl={libraryReturnUrl}
+          theme={theme}
         />
       </IsMobileProvider>
     </InitializeApp>

+ 19 - 1
src/tests/excalidrawPackage.test.tsx

@@ -1,7 +1,7 @@
 import React from "react";
 import { fireEvent, GlobalTestState, render } from "./test-utils";
 import Excalidraw from "../packages/excalidraw/index";
-import { queryByText } from "@testing-library/react";
+import { queryByText, queryByTestId } from "@testing-library/react";
 import { GRID_SIZE } from "../constants";
 
 const { h } = window;
@@ -86,4 +86,22 @@ describe("<Excalidraw/>", () => {
       expect(h.state.gridSize).toBe(null);
     });
   });
+
+  describe("Test theme prop", () => {
+    it('should show the dark mode toggle when the theme prop is "undefined"', async () => {
+      const { container } = await render(<Excalidraw />);
+      expect(h.state.theme).toBe("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 () => {
+      const { container } = await render(<Excalidraw theme="dark" />);
+      expect(h.state.theme).toBe("dark");
+
+      expect(queryByTestId(container, "toggle-dark-mode")).toBe(null);
+    });
+  });
 });

+ 1 - 0
src/types.ts

@@ -190,6 +190,7 @@ export interface ExcalidrawProps {
   zenModeEnabled?: boolean;
   gridModeEnabled?: boolean;
   libraryReturnUrl?: string;
+  theme?: "dark" | "light";
 }
 
 export type SceneData = {