mirror of
https://github.com/excalidraw/excalidraw.git
synced 2024-11-10 11:35:52 +01:00
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:
parent
1f295955d0
commit
84a1863233
@ -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,
|
||||||
|
@ -7,13 +7,16 @@ 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")}
|
||||||
|
{showThemeBtn && (
|
||||||
<div style={{ marginInlineStart: "0.25rem" }}>
|
<div style={{ marginInlineStart: "0.25rem" }}>
|
||||||
<DarkModeToggle
|
<DarkModeToggle
|
||||||
value={appState.theme}
|
value={appState.theme}
|
||||||
@ -22,5 +25,6 @@ export const BackgroundPickerAndDarkModeToggle = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
@ -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
|
||||||
|
@ -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`
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 = {
|
||||||
|
Loading…
Reference in New Issue
Block a user