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:
Aakansha Doshi 2022-12-21 14:29:06 +05:30 committed by GitHub
parent d2e371cdf0
commit b704705ed8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 232 additions and 160 deletions

View File

@ -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 &&

View File

@ -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 &&

View File

@ -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>

View File

@ -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,

View File

@ -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";

View 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";

View File

@ -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,
);

View File

@ -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;

View 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>
);
};

View File

@ -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

View File

@ -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).

View File

@ -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.

View File

@ -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>

View File

@ -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 };

View File

@ -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 />);

View File

@ -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>>;
};

View File

@ -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>);
};