feat: render footer as a component instead of render prop (#5970)
* feat: render footer as a component instead of render prop * Export FooterCenter as footer * remove useDevice export * revert some changes * remove * add spec * update specs * parse children into a dictionary * factor app footer components into a single file * Add docs * split app footer components Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
d2e371cdf0
commit
b704705ed8
@ -534,12 +534,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
const {
|
||||
onCollabButtonClick,
|
||||
renderTopRightUI,
|
||||
renderFooter,
|
||||
renderCustomStats,
|
||||
} = this.props;
|
||||
const { onCollabButtonClick, renderTopRightUI, renderCustomStats } =
|
||||
this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -583,7 +579,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
langCode={getLanguage().code}
|
||||
isCollaborating={this.props.isCollaborating}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomFooter={renderFooter}
|
||||
renderCustomStats={renderCustomStats}
|
||||
renderCustomSidebar={this.props.renderSidebar}
|
||||
showExitZenModeBtn={
|
||||
@ -601,7 +596,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.activeTool.type === "selection" &&
|
||||
!this.scene.getElementsIncludingDeleted().length
|
||||
}
|
||||
/>
|
||||
>
|
||||
{this.props.children}
|
||||
</LayerUI>
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
<div className="excalidraw-contextMenuContainer" />
|
||||
{selectedElement.length === 1 &&
|
||||
|
@ -8,8 +8,14 @@ import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { Language, t } from "../i18n";
|
||||
import { calculateScrollCenter } from "../scene";
|
||||
import { ExportType } from "../scene/types";
|
||||
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
|
||||
import { muteFSAbortError } from "../utils";
|
||||
import {
|
||||
AppProps,
|
||||
AppState,
|
||||
ExcalidrawProps,
|
||||
BinaryFiles,
|
||||
UIChildrenComponents,
|
||||
} from "../types";
|
||||
import { muteFSAbortError, ReactChildrenToObject } from "../utils";
|
||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||
import CollabButton from "./CollabButton";
|
||||
import { ErrorDialog } from "./ErrorDialog";
|
||||
@ -38,7 +44,7 @@ import { trackEvent } from "../analytics";
|
||||
import { isMenuOpenAtom, useDevice } from "../components/App";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||
import Footer from "./Footer";
|
||||
import Footer from "./footer/Footer";
|
||||
import {
|
||||
ExportImageIcon,
|
||||
HamburgerMenuIcon,
|
||||
@ -71,7 +77,6 @@ interface LayerUIProps {
|
||||
langCode: Language["code"];
|
||||
isCollaborating: boolean;
|
||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
@ -81,7 +86,9 @@ interface LayerUIProps {
|
||||
id: string;
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderWelcomeScreen: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const LayerUI = ({
|
||||
actionManager,
|
||||
appState,
|
||||
@ -96,7 +103,7 @@ const LayerUI = ({
|
||||
showExitZenModeBtn,
|
||||
isCollaborating,
|
||||
renderTopRightUI,
|
||||
renderCustomFooter,
|
||||
|
||||
renderCustomStats,
|
||||
renderCustomSidebar,
|
||||
libraryReturnUrl,
|
||||
@ -106,9 +113,13 @@ const LayerUI = ({
|
||||
id,
|
||||
onImageAction,
|
||||
renderWelcomeScreen,
|
||||
children,
|
||||
}: LayerUIProps) => {
|
||||
const device = useDevice();
|
||||
|
||||
const childrenComponents =
|
||||
ReactChildrenToObject<UIChildrenComponents>(children);
|
||||
|
||||
const renderJSONExportDialog = () => {
|
||||
if (!UIOptions.canvasActions.export) {
|
||||
return null;
|
||||
@ -481,7 +492,6 @@ const LayerUI = ({
|
||||
onPenModeToggle={onPenModeToggle}
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomStats={renderCustomStats}
|
||||
@ -514,9 +524,11 @@ const LayerUI = ({
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
appState={appState}
|
||||
actionManager={actionManager}
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
showExitZenModeBtn={showExitZenModeBtn}
|
||||
/>
|
||||
>
|
||||
{childrenComponents.FooterCenter}
|
||||
</Footer>
|
||||
|
||||
{appState.showStats && (
|
||||
<Stats
|
||||
appState={appState}
|
||||
@ -563,7 +575,6 @@ const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
|
||||
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
||||
|
||||
return (
|
||||
prev.renderCustomFooter === next.renderCustomFooter &&
|
||||
prev.renderTopRightUI === next.renderTopRightUI &&
|
||||
prev.renderCustomStats === next.renderCustomStats &&
|
||||
prev.renderCustomSidebar === next.renderCustomSidebar &&
|
||||
|
@ -36,10 +36,7 @@ type MobileMenuProps = {
|
||||
onPenModeToggle: () => void;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
isCollaborating: boolean;
|
||||
renderCustomFooter?: (
|
||||
isMobile: boolean,
|
||||
appState: AppState,
|
||||
) => JSX.Element | null;
|
||||
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderTopRightUI?: (
|
||||
isMobile: boolean,
|
||||
@ -63,7 +60,6 @@ export const MobileMenu = ({
|
||||
onPenModeToggle,
|
||||
canvas,
|
||||
isCollaborating,
|
||||
renderCustomFooter,
|
||||
onImageAction,
|
||||
renderTopRightUI,
|
||||
renderCustomStats,
|
||||
@ -253,7 +249,6 @@ export const MobileMenu = ({
|
||||
<div className="panelColumn">
|
||||
<Stack.Col gap={2}>
|
||||
{renderCanvasActions()}
|
||||
{renderCustomFooter?.(true, appState)}
|
||||
{appState.collaborators.size > 0 && (
|
||||
<fieldset>
|
||||
<legend>{t("labels.collaborators")}</legend>
|
||||
|
@ -2,7 +2,7 @@ import { useAtom } from "jotai";
|
||||
import { actionLoadScene, actionShortcuts } from "../actions";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||
import { COOKIES } from "../constants";
|
||||
import { isExcalidrawPlusSignedUser } from "../constants";
|
||||
import { collabDialogShownAtom } from "../excalidraw-app/collab/Collab";
|
||||
import { t } from "../i18n";
|
||||
import { AppState } from "../types";
|
||||
@ -15,10 +15,6 @@ import {
|
||||
} from "./icons";
|
||||
import "./WelcomeScreen.scss";
|
||||
|
||||
const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||
COOKIES.AUTH_STATE_COOKIE,
|
||||
);
|
||||
|
||||
const WelcomeScreenItem = ({
|
||||
label,
|
||||
shortcut,
|
||||
|
@ -1,35 +1,37 @@
|
||||
import clsx from "clsx";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { t } from "../i18n";
|
||||
import { AppState, ExcalidrawProps } from "../types";
|
||||
import { ActionManager } from "../../actions/manager";
|
||||
import { t } from "../../i18n";
|
||||
import { AppState } from "../../types";
|
||||
import {
|
||||
ExitZenModeAction,
|
||||
FinalizeAction,
|
||||
UndoRedoActions,
|
||||
ZoomActions,
|
||||
} from "./Actions";
|
||||
import { useDevice } from "./App";
|
||||
import { WelcomeScreenHelpArrow } from "./icons";
|
||||
import { Section } from "./Section";
|
||||
import Stack from "./Stack";
|
||||
import WelcomeScreenDecor from "./WelcomeScreenDecor";
|
||||
} from "../Actions";
|
||||
import { useDevice } from "../App";
|
||||
import { WelcomeScreenHelpArrow } from "../icons";
|
||||
import { Section } from "../Section";
|
||||
import Stack from "../Stack";
|
||||
import WelcomeScreenDecor from "../WelcomeScreenDecor";
|
||||
import FooterCenter from "./FooterCenter";
|
||||
|
||||
const Footer = ({
|
||||
appState,
|
||||
actionManager,
|
||||
renderCustomFooter,
|
||||
showExitZenModeBtn,
|
||||
renderWelcomeScreen,
|
||||
children,
|
||||
}: {
|
||||
appState: AppState;
|
||||
actionManager: ActionManager;
|
||||
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||
showExitZenModeBtn: boolean;
|
||||
renderWelcomeScreen: boolean;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const showFinalize =
|
||||
!appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
|
||||
|
||||
return (
|
||||
<footer
|
||||
role="contentinfo"
|
||||
@ -69,17 +71,7 @@ const Footer = ({
|
||||
</Section>
|
||||
</Stack.Col>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__footer-center zen-mode-transition",
|
||||
{
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{renderCustomFooter?.(false, appState)}
|
||||
</div>
|
||||
<FooterCenter>{children}</FooterCenter>
|
||||
<div
|
||||
className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", {
|
||||
"transition-right disable-pointerEvents": appState.zenModeEnabled,
|
||||
@ -107,3 +99,4 @@ const Footer = ({
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
Footer.displayName = "Footer";
|
19
src/components/footer/FooterCenter.tsx
Normal file
19
src/components/footer/FooterCenter.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import clsx from "clsx";
|
||||
import { useExcalidrawAppState } from "../App";
|
||||
|
||||
const FooterCenter = ({ children }: { children?: React.ReactNode }) => {
|
||||
const appState = useExcalidrawAppState();
|
||||
return (
|
||||
<div
|
||||
className={clsx("layer-ui__wrapper__footer-center zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterCenter;
|
||||
FooterCenter.displayName = "FooterCenter";
|
@ -243,3 +243,7 @@ export const COOKIES = {
|
||||
/** key containt id of precedeing elemnt id we use in reconciliation during
|
||||
* collaboration */
|
||||
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
||||
|
||||
export const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||
COOKIES.AUTH_STATE_COOKIE,
|
||||
);
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { t } from "../i18n";
|
||||
import { shield } from "./icons";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
import { shield } from "../../components/icons";
|
||||
import { Tooltip } from "../../components/Tooltip";
|
||||
import { t } from "../../i18n";
|
||||
|
||||
const EncryptedIcon = () => (
|
||||
export const EncryptedIcon = () => (
|
||||
<a
|
||||
className="encrypted-icon tooltip"
|
||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||
@ -15,5 +15,3 @@ const EncryptedIcon = () => (
|
||||
</Tooltip>
|
||||
</a>
|
||||
);
|
||||
|
||||
export default EncryptedIcon;
|
17
src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx
Normal file
17
src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { isExcalidrawPlusSignedUser } from "../../constants";
|
||||
|
||||
export const ExcalidrawPlusAppLink = () => {
|
||||
if (!isExcalidrawPlusSignedUser) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="plus-button"
|
||||
>
|
||||
Go to Excalidraw+
|
||||
</a>
|
||||
);
|
||||
};
|
@ -7,7 +7,6 @@ import { ErrorDialog } from "../components/ErrorDialog";
|
||||
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
||||
import {
|
||||
APP_NAME,
|
||||
COOKIES,
|
||||
EVENT,
|
||||
THEME,
|
||||
TITLE_TIMEOUT,
|
||||
@ -22,7 +21,7 @@ import {
|
||||
} from "../element/types";
|
||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||
import { t } from "../i18n";
|
||||
import { Excalidraw, defaultLang } from "../packages/excalidraw/index";
|
||||
import { Excalidraw, defaultLang, Footer } from "../packages/excalidraw/index";
|
||||
import {
|
||||
AppState,
|
||||
LibraryItems,
|
||||
@ -50,7 +49,6 @@ import Collab, {
|
||||
collabDialogShownAtom,
|
||||
isCollaboratingAtom,
|
||||
} from "./collab/Collab";
|
||||
import { LanguageList } from "./components/LanguageList";
|
||||
import {
|
||||
exportToBackend,
|
||||
getCollaborationLinkData,
|
||||
@ -79,15 +77,12 @@ import { atom, Provider, useAtom } from "jotai";
|
||||
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
|
||||
import { reconcileElements } from "./collab/reconciliation";
|
||||
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
||||
import EncryptedIcon from "../components/EncryptedIcon";
|
||||
import { EncryptedIcon } from "./components/EncryptedIcon";
|
||||
import { ExcalidrawPlusAppLink } from "./components/ExcalidrawPlusAppLink";
|
||||
|
||||
polyfill();
|
||||
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
||||
|
||||
const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||
COOKIES.AUTH_STATE_COOKIE,
|
||||
);
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.init({
|
||||
languageUtils: {},
|
||||
@ -577,41 +572,6 @@ const ExcalidrawWrapper = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderFooter = (isMobile: boolean) => {
|
||||
const renderLanguageList = () => <LanguageList />;
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: ".5rem", fontSize: "0.75rem" }}>
|
||||
{t("labels.language")}
|
||||
</div>
|
||||
<div style={{ padding: "0 0.625rem" }}>{renderLanguageList()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: ".5rem", alignItems: "center" }}>
|
||||
{isExcalidrawPlusSignedUser && (
|
||||
<a
|
||||
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="plus-button"
|
||||
>
|
||||
Go to Excalidraw+
|
||||
</a>
|
||||
)}
|
||||
<EncryptedIcon />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCustomStats = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
@ -672,7 +632,6 @@ const ExcalidrawWrapper = () => {
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderFooter={renderFooter}
|
||||
langCode={langCode}
|
||||
renderCustomStats={renderCustomStats}
|
||||
detectScroll={false}
|
||||
@ -680,7 +639,14 @@ const ExcalidrawWrapper = () => {
|
||||
onLibraryChange={onLibraryChange}
|
||||
autoFocus={true}
|
||||
theme={theme}
|
||||
/>
|
||||
>
|
||||
<Footer>
|
||||
<div style={{ display: "flex", gap: ".5rem", alignItems: "center" }}>
|
||||
<ExcalidrawPlusAppLink />
|
||||
<EncryptedIcon />
|
||||
</div>
|
||||
</Footer>
|
||||
</Excalidraw>
|
||||
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
||||
{errorMessage && (
|
||||
<ErrorDialog
|
||||
|
@ -13,6 +13,14 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Features
|
||||
|
||||
- Render Footer as a component instead of render prop [#5970](https://github.com/excalidraw/excalidraw/pull/5970). You can read more about its usage [here](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Footer)
|
||||
|
||||
#### BREAKING CHANGE
|
||||
|
||||
- With this change, the prop `renderFooter` is now removed.
|
||||
|
||||
### Excalidraw schema
|
||||
|
||||
- Merged `appState.currentItemStrokeSharpness` and `appState.currentItemLinearStrokeSharpness` into `appState.currentItemRoundness`. Renamed `changeSharpness` action to `changeRoundness`. Excalidraw element's `strokeSharpness` was changed to `roundness`. Check the PR for types and more details [#5553](https://github.com/excalidraw/excalidraw/pull/5553).
|
||||
|
@ -380,6 +380,31 @@ For a complete list of variables, check [theme.scss](https://github.com/excalidr
|
||||
|
||||
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx).
|
||||
|
||||
### Component API
|
||||
|
||||
#### Footer
|
||||
|
||||
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
|
||||
|
||||
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should a valid React Node.
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
import { Footer } from "@excalidraw/excalidraw";
|
||||
|
||||
const CustomFooter = () => <button> custom button</button>;
|
||||
const App = () => {
|
||||
return (
|
||||
<Excalidraw>
|
||||
<Footer>
|
||||
<CustomFooter />
|
||||
</Footer>
|
||||
</Excalidraw>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
@ -392,7 +417,6 @@ No, Excalidraw package doesn't come with collaboration built in, since the imple
|
||||
| [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. |
|
||||
| [`langCode`](#langCode) | string | `en` | Language code string |
|
||||
| [`renderTopRightUI`](#renderTopRightUI) | Function | | Function that renders custom UI in top right corner |
|
||||
| [`renderFooter `](#renderFooter) | Function | | Function that renders custom UI footer |
|
||||
| [`renderCustomStats`](#renderCustomStats) | Function | | Function that can be used to render custom stats on the stats dialog. |
|
||||
| [`renderSIdebar`](#renderSIdebar) | Function | | Render function that renders custom sidebar. |
|
||||
| [`viewModeEnabled`](#viewModeEnabled) | boolean | | This implies if the app is in view mode. |
|
||||
@ -613,14 +637,6 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
|
||||
|
||||
A function returning JSX to render custom UI in the top right corner of the app.
|
||||
|
||||
#### `renderFooter`
|
||||
|
||||
<pre>
|
||||
(isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>) => JSX | null
|
||||
</pre>
|
||||
|
||||
A function returning JSX to render custom UI footer. For example, you can use this to render a language picker that was previously being rendered by Excalidraw itself (for now, you'll need to implement your own language picker).
|
||||
|
||||
#### `renderCustomStats`
|
||||
|
||||
A function that can be used to render custom stats (returns JSX) in the nerd stats dialog. For example you can use this prop to render the size of the elements in the storage.
|
||||
|
@ -68,6 +68,7 @@ const {
|
||||
viewportCoordsToSceneCoords,
|
||||
restoreElements,
|
||||
Sidebar,
|
||||
Footer,
|
||||
} = window.ExcalidrawLib;
|
||||
|
||||
const COMMENT_SVG = (
|
||||
@ -160,49 +161,6 @@ export default function App() {
|
||||
fetchData();
|
||||
}, [excalidrawAPI]);
|
||||
|
||||
const renderFooter = () => {
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
<button
|
||||
className="custom-element"
|
||||
onClick={() => {
|
||||
excalidrawAPI?.setActiveTool({
|
||||
type: "custom",
|
||||
customType: "comment",
|
||||
});
|
||||
const url = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
||||
`<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="feather feather-message-circle"
|
||||
>
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||
</svg>`,
|
||||
)}`;
|
||||
excalidrawAPI?.setCursor(`url(${url}), auto`);
|
||||
}}
|
||||
>
|
||||
{COMMENT_SVG}
|
||||
</button>
|
||||
<button
|
||||
className="custom-footer"
|
||||
onClick={() => alert("This is dummy footer")}
|
||||
>
|
||||
{" "}
|
||||
custom footer{" "}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const loadSceneOrLibrary = async () => {
|
||||
const file = await fileOpen({ description: "Excalidraw or library file" });
|
||||
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
|
||||
@ -712,12 +670,49 @@ export default function App() {
|
||||
name="Custom name of drawing"
|
||||
UIOptions={{ canvasActions: { loadScene: false } }}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderFooter={renderFooter}
|
||||
onLinkOpen={onLinkOpen}
|
||||
onPointerDown={onPointerDown}
|
||||
onScrollChange={rerenderCommentIcons}
|
||||
renderSidebar={renderSidebar}
|
||||
/>
|
||||
>
|
||||
<Footer>
|
||||
<button
|
||||
className="custom-element"
|
||||
onClick={() => {
|
||||
excalidrawAPI?.setActiveTool({
|
||||
type: "custom",
|
||||
customType: "comment",
|
||||
});
|
||||
const url = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
||||
`<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="feather feather-message-circle"
|
||||
>
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||
</svg>`,
|
||||
)}`;
|
||||
excalidrawAPI?.setCursor(`url(${url}), auto`);
|
||||
}}
|
||||
>
|
||||
{COMMENT_SVG}
|
||||
</button>
|
||||
<button
|
||||
className="custom-footer"
|
||||
onClick={() => alert("This is dummy footer")}
|
||||
>
|
||||
{" "}
|
||||
custom footer{" "}
|
||||
</button>
|
||||
</Footer>
|
||||
</Excalidraw>
|
||||
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
|
||||
{comment && renderComment()}
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@ import { defaultLang } from "../../i18n";
|
||||
import { DEFAULT_UI_OPTIONS } from "../../constants";
|
||||
import { Provider } from "jotai";
|
||||
import { jotaiScope, jotaiStore } from "../../jotai";
|
||||
import Footer from "../../components/footer/FooterCenter";
|
||||
|
||||
const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
const {
|
||||
@ -20,7 +21,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
isCollaborating = false,
|
||||
onPointerUpdate,
|
||||
renderTopRightUI,
|
||||
renderFooter,
|
||||
renderSidebar,
|
||||
langCode = defaultLang.code,
|
||||
viewModeEnabled,
|
||||
@ -39,6 +39,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
onLinkOpen,
|
||||
onPointerDown,
|
||||
onScrollChange,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
const canvasActions = props.UIOptions?.canvasActions;
|
||||
@ -93,7 +94,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
isCollaborating={isCollaborating}
|
||||
onPointerUpdate={onPointerUpdate}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderFooter={renderFooter}
|
||||
langCode={langCode}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
@ -113,7 +113,9 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
onPointerDown={onPointerDown}
|
||||
onScrollChange={onScrollChange}
|
||||
renderSidebar={renderSidebar}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
</App>
|
||||
</Provider>
|
||||
</InitializeApp>
|
||||
);
|
||||
@ -236,3 +238,4 @@ export {
|
||||
} from "../../utils";
|
||||
|
||||
export { Sidebar } from "../../components/Sidebar/Sidebar";
|
||||
export { Footer };
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { fireEvent, GlobalTestState, render } from "../test-utils";
|
||||
import { Excalidraw } from "../../packages/excalidraw/index";
|
||||
import { Excalidraw, Footer } from "../../packages/excalidraw/index";
|
||||
import { queryByText, queryByTestId } from "@testing-library/react";
|
||||
import { GRID_SIZE, THEME } from "../../constants";
|
||||
import { t } from "../../i18n";
|
||||
@ -49,6 +49,31 @@ describe("<Excalidraw/>", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the footer only when Footer is passed as children", async () => {
|
||||
//Footer not passed hence it will not render the footer
|
||||
let { container } = await render(
|
||||
<Excalidraw>
|
||||
<div>This is a custom footer</div>
|
||||
</Excalidraw>,
|
||||
);
|
||||
expect(
|
||||
container.querySelector(".layer-ui__wrapper__footer-center"),
|
||||
).toBeEmptyDOMElement();
|
||||
|
||||
// Footer passed hence it will render the footer
|
||||
({ container } = await render(
|
||||
<Excalidraw>
|
||||
<Footer>
|
||||
<div>This is a custom footer</div>
|
||||
</Footer>
|
||||
</Excalidraw>,
|
||||
));
|
||||
expect(
|
||||
container.querySelector(".layer-ui__wrapper__footer-center")?.innerHTML,
|
||||
).toMatchInlineSnapshot(
|
||||
`"<div class=\\"layer-ui__wrapper__footer-center zen-mode-transition\\"><div>This is a custom footer</div></div>"`,
|
||||
);
|
||||
});
|
||||
describe("Test gridModeEnabled prop", () => {
|
||||
it('should show grid mode in context menu when gridModeEnabled is "undefined"', async () => {
|
||||
const { container } = await render(<Excalidraw />);
|
||||
|
@ -295,7 +295,6 @@ export interface ExcalidrawProps {
|
||||
isMobile: boolean,
|
||||
appState: AppState,
|
||||
) => JSX.Element | null;
|
||||
renderFooter?: (isMobile: boolean, appState: AppState) => JSX.Element | null;
|
||||
langCode?: Language["code"];
|
||||
viewModeEnabled?: boolean;
|
||||
zenModeEnabled?: boolean;
|
||||
@ -331,6 +330,7 @@ export interface ExcalidrawProps {
|
||||
* Render function that renders custom <Sidebar /> component.
|
||||
*/
|
||||
renderSidebar?: () => JSX.Element | null;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export type SceneData = {
|
||||
@ -507,3 +507,9 @@ export type Device = Readonly<{
|
||||
isTouchScreen: boolean;
|
||||
canDeviceFitSidebar: boolean;
|
||||
}>;
|
||||
|
||||
export type UIChildrenComponents = {
|
||||
[k in "FooterCenter"]?:
|
||||
| React.ReactPortal
|
||||
| React.ReactElement<unknown, string | React.JSXElementConstructor<any>>;
|
||||
};
|
||||
|
23
src/utils.ts
23
src/utils.ts
@ -15,6 +15,7 @@ import { AppState, DataURL, LastActiveToolBeforeEraser, Zoom } from "./types";
|
||||
import { unstable_batchedUpdates } from "react-dom";
|
||||
import { isDarwin } from "./keys";
|
||||
import { SHAPES } from "./shapes";
|
||||
import React from "react";
|
||||
|
||||
let mockDateTime: string | null = null;
|
||||
|
||||
@ -686,3 +687,25 @@ export const queryFocusableElements = (container: HTMLElement | null) => {
|
||||
)
|
||||
: [];
|
||||
};
|
||||
|
||||
export const ReactChildrenToObject = <
|
||||
T extends {
|
||||
[k in string]?:
|
||||
| React.ReactPortal
|
||||
| React.ReactElement<unknown, string | React.JSXElementConstructor<any>>;
|
||||
},
|
||||
>(
|
||||
children: React.ReactNode,
|
||||
) => {
|
||||
return React.Children.toArray(children).reduce((acc, child) => {
|
||||
if (
|
||||
React.isValidElement(child) &&
|
||||
typeof child.type !== "string" &&
|
||||
child?.type.name
|
||||
) {
|
||||
// @ts-ignore
|
||||
acc[child.type.name] = child;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Partial<T>);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user