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
This commit is contained in:
Jeremy Press 2021-03-15 11:33:46 -07:00 committed by GitHub
parent 1f295955d0
commit 84a1863233
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 62 additions and 10 deletions

View File

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

View File

@ -7,20 +7,24 @@ export const BackgroundPickerAndDarkModeToggle = ({
appState, appState,
setAppState, setAppState,
actionManager, actionManager,
showThemeBtn,
}: { }: {
actionManager: ActionManager; actionManager: ActionManager;
appState: AppState; appState: AppState;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
showThemeBtn: boolean;
}) => ( }) => (
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
{actionManager.renderAction("changeViewBackgroundColor")} {actionManager.renderAction("changeViewBackgroundColor")}
<div style={{ marginInlineStart: "0.25rem" }}> {showThemeBtn && (
<DarkModeToggle <div style={{ marginInlineStart: "0.25rem" }}>
value={appState.theme} <DarkModeToggle
onChange={(theme) => { value={appState.theme}
setAppState({ theme }); onChange={(theme) => {
}} setAppState({ theme });
/> }}
</div> />
</div>
)}
</div> </div>
); );

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ Please add the latest change on the top under the correct section.
### Features ### 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). - Support `libraryReturnUrl` prop to indicate what URL to install libraries to [#3227](https://github.com/excalidraw/excalidraw/pull/3227).
### Refactor ### Refactor

View File

@ -377,6 +377,7 @@ export default function IndexPage() {
| [`zenModeEnabled`](#zenModeEnabled) | boolean | | This implies if the zen mode is enabled | | [`zenModeEnabled`](#zenModeEnabled) | boolean | | This implies if the zen mode is enabled |
| [`gridModeEnabled`](#gridModeEnabled) | boolean | | This implies if the grid 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 | | [`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` #### `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`. 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 ### Extra API's
#### `getSceneVersion` #### `getSceneVersion`

View File

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

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { fireEvent, GlobalTestState, render } from "./test-utils"; import { fireEvent, GlobalTestState, render } from "./test-utils";
import Excalidraw from "../packages/excalidraw/index"; import Excalidraw from "../packages/excalidraw/index";
import { queryByText } from "@testing-library/react"; import { queryByText, queryByTestId } from "@testing-library/react";
import { GRID_SIZE } from "../constants"; import { GRID_SIZE } from "../constants";
const { h } = window; const { h } = window;
@ -86,4 +86,22 @@ describe("<Excalidraw/>", () => {
expect(h.state.gridSize).toBe(null); 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);
});
});
}); });

View File

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