From e6de1fe4a41947a7980c416e353f1e66ec5fc50b Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 31 Jan 2023 13:53:20 +0100 Subject: [PATCH] feat: rewrite public UI component rendering using tunnels (#6117) * feat: rewrite public UI component rendering using tunnels * factor out into components * comments * fix variable naming * fix not hiding welcomeScreen * factor out AppFooter and memoize components * remove `UIOptions.welcomeScreen` and render only from host app * factor out tunnels into own file * update changelog. Keep `UIOptions.welcomeScreen` as deprecated * update changelog * lint --------- Co-authored-by: Aakansha Doshi --- package.json | 1 + src/components/App.tsx | 2 - src/components/LayerUI.tsx | 119 ++++++++---------- src/components/MobileMenu.tsx | 22 ++-- src/components/footer/Footer.tsx | 17 +-- src/components/footer/FooterCenter.tsx | 19 +-- src/components/hoc/withInternalFallback.tsx | 50 ++++++++ src/components/main-menu/MainMenu.tsx | 119 ++++++++++-------- src/components/tunnels.ts | 8 ++ .../welcome-screen/WelcomeScreen.Center.tsx | 27 ++-- .../welcome-screen/WelcomeScreen.Hints.tsx | 39 +++--- .../welcome-screen/WelcomeScreen.tsx | 19 ++- src/constants.ts | 1 - src/excalidraw-app/components/AppFooter.tsx | 21 ++++ src/excalidraw-app/components/AppMainMenu.tsx | 40 ++++++ .../components/AppWelcomeScreen.tsx | 64 ++++++++++ src/excalidraw-app/index.tsx | 118 ++--------------- src/packages/excalidraw/CHANGELOG.md | 5 + src/packages/excalidraw/index.tsx | 1 - src/types.ts | 35 +----- src/utils.ts | 43 ------- yarn.lock | 19 +++ 22 files changed, 417 insertions(+), 372 deletions(-) create mode 100644 src/components/hoc/withInternalFallback.tsx create mode 100644 src/components/tunnels.ts create mode 100644 src/excalidraw-app/components/AppFooter.tsx create mode 100644 src/excalidraw-app/components/AppMainMenu.tsx create mode 100644 src/excalidraw-app/components/AppWelcomeScreen.tsx diff --git a/package.json b/package.json index 9f5c46ab..8d117ea9 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "roughjs": "4.5.2", "sass": "1.51.0", "socket.io-client": "2.3.1", + "tunnel-rat": "0.1.0", "typescript": "4.9.4", "workbox-background-sync": "^6.5.4", "workbox-broadcast-update": "^6.5.4", diff --git a/src/components/App.tsx b/src/components/App.tsx index 4ea2c3a8..7b2b5e40 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -589,7 +589,6 @@ class App extends React.Component { }) } langCode={getLanguage().code} - isCollaborating={this.props.isCollaborating} renderTopRightUI={renderTopRightUI} renderCustomStats={renderCustomStats} renderCustomSidebar={this.props.renderSidebar} @@ -605,7 +604,6 @@ class App extends React.Component { onImageAction={this.onImageAction} renderWelcomeScreen={ !this.state.isLoading && - this.props.UIOptions.welcomeScreen && this.state.showWelcomeScreen && this.state.activeTool.type === "selection" && !this.scene.getElementsIncludingDeleted().length diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 3defb294..380c01c1 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -8,15 +8,8 @@ import { NonDeletedExcalidrawElement } from "../element/types"; import { Language, t } from "../i18n"; import { calculateScrollCenter } from "../scene"; import { ExportType } from "../scene/types"; -import { - AppProps, - AppState, - ExcalidrawProps, - BinaryFiles, - UIChildrenComponents, - UIWelcomeScreenComponents, -} from "../types"; -import { isShallowEqual, muteFSAbortError, getReactChildren } from "../utils"; +import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types"; +import { isShallowEqual, muteFSAbortError } from "../utils"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import { ErrorDialog } from "./ErrorDialog"; import { ExportCB, ImageExportDialog } from "./ImageExportDialog"; @@ -45,7 +38,6 @@ import { useDevice } from "../components/App"; import { Stats } from "./Stats"; import { actionToggleStats } from "../actions/actionToggleStats"; import Footer from "./footer/Footer"; -import WelcomeScreen from "./welcome-screen/WelcomeScreen"; import { hostSidebarCountersAtom } from "./Sidebar/Sidebar"; import { jotaiScope } from "../jotai"; import { useAtom } from "jotai"; @@ -53,6 +45,12 @@ import MainMenu from "./main-menu/MainMenu"; import { ActiveConfirmDialog } from "./ActiveConfirmDialog"; import { HandButton } from "./HandButton"; import { isHandToolActive } from "../appState"; +import { + mainMenuTunnel, + welcomeScreenMenuHintTunnel, + welcomeScreenToolbarHintTunnel, + welcomeScreenCenterTunnel, +} from "./tunnels"; interface LayerUIProps { actionManager: ActionManager; @@ -67,7 +65,6 @@ interface LayerUIProps { onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void; showExitZenModeBtn: boolean; langCode: Language["code"]; - isCollaborating: boolean; renderTopRightUI?: ExcalidrawProps["renderTopRightUI"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderCustomSidebar?: ExcalidrawProps["renderSidebar"]; @@ -81,6 +78,32 @@ interface LayerUIProps { children?: React.ReactNode; } +const DefaultMainMenu: React.FC<{ + UIOptions: AppProps["UIOptions"]; +}> = ({ UIOptions }) => { + return ( + + + + {/* FIXME we should to test for this inside the item itself */} + {UIOptions.canvasActions.export && } + {/* FIXME we should to test for this inside the item itself */} + {UIOptions.canvasActions.saveAsImage && ( + + )} + + + + + + + + + + + ); +}; + const LayerUI = ({ actionManager, appState, @@ -93,7 +116,6 @@ const LayerUI = ({ onPenModeToggle, onInsertElements, showExitZenModeBtn, - isCollaborating, renderTopRightUI, renderCustomStats, renderCustomSidebar, @@ -108,28 +130,6 @@ const LayerUI = ({ }: LayerUIProps) => { const device = useDevice(); - const [childrenComponents, restChildren] = - getReactChildren(children, { - Menu: true, - FooterCenter: true, - WelcomeScreen: true, - }); - - const [WelcomeScreenComponents] = getReactChildren( - renderWelcomeScreen - ? ( - childrenComponents?.WelcomeScreen ?? ( - - - - - - - ) - )?.props?.children - : null, - ); - const renderJSONExportDialog = () => { if (!UIOptions.canvasActions.export) { return null; @@ -197,37 +197,12 @@ const LayerUI = ({ ); }; - const renderMenu = () => { - return ( - childrenComponents.Menu || ( - - - - {/* FIXME we should to test for this inside the item itself */} - {UIOptions.canvasActions.export && } - {/* FIXME we should to test for this inside the item itself */} - {UIOptions.canvasActions.saveAsImage && ( - - )} - - - - - - - - - - - ) - ); - }; const renderCanvasActions = () => (
- {WelcomeScreenComponents.MenuHint} {/* wrapping to Fragment stops React from occasionally complaining about identical Keys */} - <>{renderMenu()} + + {renderWelcomeScreen && }
); @@ -264,7 +239,6 @@ const LayerUI = ({ return ( - {WelcomeScreenComponents.Center}
{(heading: React.ReactNode) => (
- {WelcomeScreenComponents.ToolbarHint} + {renderWelcomeScreen && ( + + )} - {restChildren} + {/* ------------------------- tunneled UI ---------------------------- */} + {/* make sure we render host app components first so that we can detect + them first on initial render to optimize layout shift */} + {children} + {/* render component fallbacks. Can be rendered anywhere as they'll be + tunneled away. We only render tunneled components that actually + have defaults when host do not render anything. */} + + {/* ------------------------------------------------------------------ */} + {appState.isLoading && } {appState.errorMessage && ( )} @@ -451,13 +434,13 @@ const LayerUI = ({ : {} } > + {renderWelcomeScreen && } {renderFixedSideContainer()}
{appState.showStats && ( JSX.Element | null; device: Device; - renderMenu: () => React.ReactNode; - welcomeScreenCenter: UIWelcomeScreenComponents["Center"]; }; export const MobileMenu = ({ @@ -63,13 +57,11 @@ export const MobileMenu = ({ renderCustomStats, renderSidebars, device, - renderMenu, - welcomeScreenCenter, }: MobileMenuProps) => { const renderToolbar = () => { return ( - {welcomeScreenCenter} +
{(heading: React.ReactNode) => ( @@ -135,12 +127,16 @@ export const MobileMenu = ({ const renderAppToolbar = () => { if (appState.viewModeEnabled) { - return
{renderMenu()}
; + return ( +
+ +
+ ); } return (
- {renderMenu()} + {actionManager.renderAction("toggleEditMenu")} {actionManager.renderAction("undo")} {actionManager.renderAction("redo")} diff --git a/src/components/footer/Footer.tsx b/src/components/footer/Footer.tsx index 3cb1dd29..fd734e79 100644 --- a/src/components/footer/Footer.tsx +++ b/src/components/footer/Footer.tsx @@ -1,11 +1,7 @@ import clsx from "clsx"; import { actionShortcuts } from "../../actions"; import { ActionManager } from "../../actions/manager"; -import { - AppState, - UIChildrenComponents, - UIWelcomeScreenComponents, -} from "../../types"; +import { AppState } from "../../types"; import { ExitZenModeAction, FinalizeAction, @@ -16,19 +12,18 @@ import { useDevice } from "../App"; import { HelpButton } from "../HelpButton"; import { Section } from "../Section"; import Stack from "../Stack"; +import { footerCenterTunnel, welcomeScreenHelpHintTunnel } from "../tunnels"; const Footer = ({ appState, actionManager, showExitZenModeBtn, - footerCenter, - welcomeScreenHelp, + renderWelcomeScreen, }: { appState: AppState; actionManager: ActionManager; showExitZenModeBtn: boolean; - footerCenter: UIChildrenComponents["FooterCenter"]; - welcomeScreenHelp: UIWelcomeScreenComponents["HelpHint"]; + renderWelcomeScreen: boolean; }) => { const device = useDevice(); const showFinalize = @@ -73,14 +68,14 @@ const Footer = ({
- {footerCenter} +
- {welcomeScreenHelp} + {renderWelcomeScreen && } actionManager.executeAction(actionShortcuts)} /> diff --git a/src/components/footer/FooterCenter.tsx b/src/components/footer/FooterCenter.tsx index 9fb0acbd..1cd96b85 100644 --- a/src/components/footer/FooterCenter.tsx +++ b/src/components/footer/FooterCenter.tsx @@ -1,18 +1,21 @@ import clsx from "clsx"; import { useExcalidrawAppState } from "../App"; +import { footerCenterTunnel } from "../tunnels"; import "./FooterCenter.scss"; const FooterCenter = ({ children }: { children?: React.ReactNode }) => { const appState = useExcalidrawAppState(); return ( -
- {children} -
+ +
+ {children} +
+
); }; diff --git a/src/components/hoc/withInternalFallback.tsx b/src/components/hoc/withInternalFallback.tsx new file mode 100644 index 00000000..6c68b3c9 --- /dev/null +++ b/src/components/hoc/withInternalFallback.tsx @@ -0,0 +1,50 @@ +import { atom, useAtom } from "jotai"; +import React, { useLayoutEffect } from "react"; + +export const withInternalFallback = ( + componentName: string, + Component: React.FC

, +) => { + const counterAtom = atom(0); + // flag set on initial render to tell the fallback component to skip the + // render until mount counter are initialized. This is because the counter + // is initialized in an effect, and thus we could end rendering both + // components at the same time until counter is initialized. + let preferHost = false; + + const WrapperComponent: React.FC< + P & { + __fallback?: boolean; + } + > = (props) => { + const [counter, setCounter] = useAtom(counterAtom); + + useLayoutEffect(() => { + setCounter((counter) => counter + 1); + return () => { + setCounter((counter) => counter - 1); + }; + }, [setCounter]); + + if (!props.__fallback) { + preferHost = true; + } + + // ensure we don't render fallback and host components at the same time + if ( + // either before the counters are initialized + (!counter && props.__fallback && preferHost) || + // or after the counters are initialized, and both are rendered + // (this is the default when host renders as well) + (counter > 1 && props.__fallback) + ) { + return null; + } + + return ; + }; + + WrapperComponent.displayName = componentName; + + return WrapperComponent; +}; diff --git a/src/components/main-menu/MainMenu.tsx b/src/components/main-menu/MainMenu.tsx index 35b40635..cc0de1b5 100644 --- a/src/components/main-menu/MainMenu.tsx +++ b/src/components/main-menu/MainMenu.tsx @@ -11,62 +11,73 @@ import * as DefaultItems from "./DefaultItems"; import { UserList } from "../UserList"; import { t } from "../../i18n"; import { HamburgerMenuIcon } from "../icons"; +import { withInternalFallback } from "../hoc/withInternalFallback"; import { composeEventHandlers } from "../../utils"; +import { mainMenuTunnel } from "../tunnels"; -const MainMenu = ({ - children, - onSelect, -}: { - children?: React.ReactNode; - /** - * Called when any menu item is selected (clicked on). - */ - onSelect?: (event: Event) => void; -}) => { - const device = useDevice(); - const appState = useExcalidrawAppState(); - const setAppState = useExcalidrawSetAppState(); - const onClickOutside = device.isMobile - ? undefined - : () => setAppState({ openMenu: null }); +const MainMenu = Object.assign( + withInternalFallback( + "MainMenu", + ({ + children, + onSelect, + }: { + children?: React.ReactNode; + /** + * Called when any menu item is selected (clicked on). + */ + onSelect?: (event: Event) => void; + }) => { + const device = useDevice(); + const appState = useExcalidrawAppState(); + const setAppState = useExcalidrawSetAppState(); + const onClickOutside = device.isMobile + ? undefined + : () => setAppState({ openMenu: null }); - return ( - - { - setAppState({ - openMenu: appState.openMenu === "canvas" ? null : "canvas", - }); - }} - > - {HamburgerMenuIcon} - - { - setAppState({ openMenu: null }); - })} - > - {children} - {device.isMobile && appState.collaborators.size > 0 && ( -

- {t("labels.collaborators")} - -
- )} - - - ); -}; - -MainMenu.Trigger = DropdownMenu.Trigger; -MainMenu.Item = DropdownMenu.Item; -MainMenu.ItemLink = DropdownMenu.ItemLink; -MainMenu.ItemCustom = DropdownMenu.ItemCustom; -MainMenu.Group = DropdownMenu.Group; -MainMenu.Separator = DropdownMenu.Separator; -MainMenu.DefaultItems = DefaultItems; + return ( + + + { + setAppState({ + openMenu: appState.openMenu === "canvas" ? null : "canvas", + }); + }} + > + {HamburgerMenuIcon} + + { + setAppState({ openMenu: null }); + })} + > + {children} + {device.isMobile && appState.collaborators.size > 0 && ( +
+ {t("labels.collaborators")} + +
+ )} +
+
+
+ ); + }, + ), + { + Trigger: DropdownMenu.Trigger, + Item: DropdownMenu.Item, + ItemLink: DropdownMenu.ItemLink, + ItemCustom: DropdownMenu.ItemCustom, + Group: DropdownMenu.Group, + Separator: DropdownMenu.Separator, + DefaultItems, + }, +); export default MainMenu; - -MainMenu.displayName = "Menu"; diff --git a/src/components/tunnels.ts b/src/components/tunnels.ts new file mode 100644 index 00000000..646adac0 --- /dev/null +++ b/src/components/tunnels.ts @@ -0,0 +1,8 @@ +import tunnel from "tunnel-rat"; + +export const mainMenuTunnel = tunnel(); +export const welcomeScreenMenuHintTunnel = tunnel(); +export const welcomeScreenToolbarHintTunnel = tunnel(); +export const welcomeScreenHelpHintTunnel = tunnel(); +export const welcomeScreenCenterTunnel = tunnel(); +export const footerCenterTunnel = tunnel(); diff --git a/src/components/welcome-screen/WelcomeScreen.Center.tsx b/src/components/welcome-screen/WelcomeScreen.Center.tsx index c1eef7dc..169503a6 100644 --- a/src/components/welcome-screen/WelcomeScreen.Center.tsx +++ b/src/components/welcome-screen/WelcomeScreen.Center.tsx @@ -7,6 +7,7 @@ import { useExcalidrawAppState, } from "../App"; import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons"; +import { welcomeScreenCenterTunnel } from "../tunnels"; const WelcomeScreenMenuItemContent = ({ icon, @@ -89,18 +90,20 @@ WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink"; const Center = ({ children }: { children?: React.ReactNode }) => { return ( -
- {children || ( - <> - - {t("welcomeScreen.defaults.center_heading")} - - - - - - )} -
+ +
+ {children || ( + <> + + {t("welcomeScreen.defaults.center_heading")} + + + + + + )} +
+
); }; Center.displayName = "Center"; diff --git a/src/components/welcome-screen/WelcomeScreen.Hints.tsx b/src/components/welcome-screen/WelcomeScreen.Hints.tsx index d961a6c6..59d68cf9 100644 --- a/src/components/welcome-screen/WelcomeScreen.Hints.tsx +++ b/src/components/welcome-screen/WelcomeScreen.Hints.tsx @@ -4,37 +4,48 @@ import { WelcomeScreenMenuArrow, WelcomeScreenTopToolbarArrow, } from "../icons"; +import { + welcomeScreenMenuHintTunnel, + welcomeScreenToolbarHintTunnel, + welcomeScreenHelpHintTunnel, +} from "../tunnels"; const MenuHint = ({ children }: { children?: React.ReactNode }) => { return ( -
- {WelcomeScreenMenuArrow} -
- {children || t("welcomeScreen.defaults.menuHint")} + +
+ {WelcomeScreenMenuArrow} +
+ {children || t("welcomeScreen.defaults.menuHint")} +
-
+ ); }; MenuHint.displayName = "MenuHint"; const ToolbarHint = ({ children }: { children?: React.ReactNode }) => { return ( -
-
- {children || t("welcomeScreen.defaults.toolbarHint")} + +
+
+ {children || t("welcomeScreen.defaults.toolbarHint")} +
+ {WelcomeScreenTopToolbarArrow}
- {WelcomeScreenTopToolbarArrow} -
+ ); }; ToolbarHint.displayName = "ToolbarHint"; const HelpHint = ({ children }: { children?: React.ReactNode }) => { return ( -
-
{children || t("welcomeScreen.defaults.helpHint")}
- {WelcomeScreenHelpArrow} -
+ +
+
{children || t("welcomeScreen.defaults.helpHint")}
+ {WelcomeScreenHelpArrow} +
+
); }; HelpHint.displayName = "HelpHint"; diff --git a/src/components/welcome-screen/WelcomeScreen.tsx b/src/components/welcome-screen/WelcomeScreen.tsx index 9f8c7d73..1f38b1ca 100644 --- a/src/components/welcome-screen/WelcomeScreen.tsx +++ b/src/components/welcome-screen/WelcomeScreen.tsx @@ -3,12 +3,21 @@ import { MenuHint, ToolbarHint, HelpHint } from "./WelcomeScreen.Hints"; import "./WelcomeScreen.scss"; -const WelcomeScreen = (props: { children: React.ReactNode }) => { - // NOTE this component is used as a dummy wrapper to retrieve child props - // from, and will never be rendered to DOM directly. As such, we can't - // do anything here (use hooks and such) - return null; +const WelcomeScreen = (props: { children?: React.ReactNode }) => { + return ( + <> + {props.children || ( + <> +
+ + + + + )} + + ); }; + WelcomeScreen.displayName = "WelcomeScreen"; WelcomeScreen.Center = Center; diff --git a/src/constants.ts b/src/constants.ts index c302ce87..c3952b92 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -159,7 +159,6 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = { toggleTheme: null, saveAsImage: true, }, - welcomeScreen: true, }; // breakpoints diff --git a/src/excalidraw-app/components/AppFooter.tsx b/src/excalidraw-app/components/AppFooter.tsx new file mode 100644 index 00000000..7011a1c4 --- /dev/null +++ b/src/excalidraw-app/components/AppFooter.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { Footer } from "../../packages/excalidraw/index"; +import { EncryptedIcon } from "./EncryptedIcon"; +import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink"; + +export const AppFooter = React.memo(() => { + return ( +
+
+ + +
+
+ ); +}); diff --git a/src/excalidraw-app/components/AppMainMenu.tsx b/src/excalidraw-app/components/AppMainMenu.tsx new file mode 100644 index 00000000..b1c0771e --- /dev/null +++ b/src/excalidraw-app/components/AppMainMenu.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { PlusPromoIcon } from "../../components/icons"; +import { MainMenu } from "../../packages/excalidraw/index"; +import { LanguageList } from "./LanguageList"; + +export const AppMainMenu: React.FC<{ + setCollabDialogShown: (toggle: boolean) => any; + isCollaborating: boolean; +}> = React.memo((props) => { + return ( + + + + + + props.setCollabDialogShown(true)} + /> + + + + + + Excalidraw+ + + + + + + + + + + ); +}); diff --git a/src/excalidraw-app/components/AppWelcomeScreen.tsx b/src/excalidraw-app/components/AppWelcomeScreen.tsx new file mode 100644 index 00000000..9e760f73 --- /dev/null +++ b/src/excalidraw-app/components/AppWelcomeScreen.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { PlusPromoIcon } from "../../components/icons"; +import { t } from "../../i18n"; +import { WelcomeScreen } from "../../packages/excalidraw/index"; +import { isExcalidrawPlusSignedUser } from "../app_constants"; + +export const AppWelcomeScreen: React.FC<{ + setCollabDialogShown: (toggle: boolean) => any; +}> = React.memo((props) => { + let headingContent; + + if (isExcalidrawPlusSignedUser) { + headingContent = t("welcomeScreen.app.center_heading_plus") + .split(/(Excalidraw\+)/) + .map((bit, idx) => { + if (bit === "Excalidraw+") { + return ( + + Excalidraw+ + + ); + } + return bit; + }); + } else { + headingContent = t("welcomeScreen.app.center_heading"); + } + + return ( + + + {t("welcomeScreen.app.menuHint")} + + + + + + + {headingContent} + + + + + props.setCollabDialogShown(true)} + /> + {!isExcalidrawPlusSignedUser && ( + + Try Excalidraw Plus! + + )} + + + + ); +}); diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 8f2d8cbc..6d6c9fe7 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -1,6 +1,6 @@ import polyfill from "../polyfill"; import LanguageDetector from "i18next-browser-languagedetector"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { trackEvent } from "../analytics"; import { getDefaultAppState } from "../appState"; import { ErrorDialog } from "../components/ErrorDialog"; @@ -24,10 +24,7 @@ import { t } from "../i18n"; import { Excalidraw, defaultLang, - Footer, - MainMenu, LiveCollaborationTrigger, - WelcomeScreen, } from "../packages/excalidraw/index"; import { AppState, @@ -47,7 +44,6 @@ import { } from "../utils"; import { FIREBASE_STORAGE_PREFIXES, - isExcalidrawPlusSignedUser, STORAGE_KEYS, SYNC_BROWSER_TABS_TIMEOUT, } from "./app_constants"; @@ -85,10 +81,9 @@ 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 { ExcalidrawPlusAppLink } from "./components/ExcalidrawPlusAppLink"; -import { LanguageList } from "./components/LanguageList"; -import { PlusPromoIcon } from "../components/icons"; +import { AppMainMenu } from "./components/AppMainMenu"; +import { AppWelcomeScreen } from "./components/AppWelcomeScreen"; +import { AppFooter } from "./components/AppFooter"; polyfill(); @@ -604,96 +599,6 @@ const ExcalidrawWrapper = () => { localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems); }; - const renderMenu = () => { - return ( - - - - - - setCollabDialogShown(true)} - /> - - - - - - Excalidraw+ - - - - - - - - - - ); - }; - - const welcomeScreenJSX = useMemo(() => { - let headingContent; - - if (isExcalidrawPlusSignedUser) { - headingContent = t("welcomeScreen.app.center_heading_plus") - .split(/(Excalidraw\+)/) - .map((bit, idx) => { - if (bit === "Excalidraw+") { - return ( - - Excalidraw+ - - ); - } - return bit; - }); - } else { - headingContent = t("welcomeScreen.app.center_heading"); - } - - return ( - - - {t("welcomeScreen.app.menuHint")} - - - - - - - {headingContent} - - - - - setCollabDialogShown(true)} - /> - {!isExcalidrawPlusSignedUser && ( - - Try Excalidraw Plus! - - )} - - - - ); - }, [setCollabDialogShown]); - return (
{ ); }} > - {renderMenu()} - -
-
- - -
-
- {welcomeScreenJSX} + + + {excalidrawAPI && } {errorMessage && ( diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 8a32ddcd..167a9b21 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -15,8 +15,13 @@ Please add the latest change on the top under the correct section. ### Features +- Welcome screen no longer renders by default, and you need to render it yourself. `UIOptions.welcomeScreen` option is now deprecated. [#6117](https://github.com/excalidraw/excalidraw/pull/6117) - `MainMenu`, `MainMenu.Item`, and `MainMenu.ItemLink` components now all support `onSelect(event: Event): void` callback. If you call `event.preventDefault()`, it will prevent the menu from closing when an item is selected (clicked on). [#6152](https://github.com/excalidraw/excalidraw/pull/6152) +### Fixes + +- declare css variable for font in excalidraw so its available in host [#6160](https://github.com/excalidraw/excalidraw/pull/6160) + ## 0.14.1 (2023-01-16) ### Fixes diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index 52c3b489..1305f91f 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -53,7 +53,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { ...DEFAULT_UI_OPTIONS.canvasActions, ...canvasActions, }, - welcomeScreen: props.UIOptions?.welcomeScreen ?? true, }; if (canvasActions?.export) { diff --git a/src/types.ts b/src/types.ts index 49e09a76..486ec714 100644 --- a/src/types.ts +++ b/src/types.ts @@ -385,15 +385,16 @@ type CanvasActions = Partial<{ type UIOptions = Partial<{ dockedSidebarBreakpoint: number; - welcomeScreen: boolean; canvasActions: CanvasActions; + /** @deprecated does nothing. Will be removed in 0.15 */ + welcomeScreen?: boolean; }>; export type AppProps = Merge< ExcalidrawProps, { UIOptions: Merge< - MarkRequired, + UIOptions, { canvasActions: Required & { export: ExportOpts }; } @@ -523,33 +524,3 @@ export type Device = Readonly<{ isTouchScreen: boolean; canDeviceFitSidebar: boolean; }>; - -export type UIChildrenComponents = { - [k in "FooterCenter" | "Menu" | "WelcomeScreen"]?: React.ReactElement< - { children?: React.ReactNode }, - React.JSXElementConstructor - >; -}; - -export type UIWelcomeScreenComponents = { - [k in - | "Center" - | "MenuHint" - | "ToolbarHint" - | "HelpHint"]?: React.ReactElement< - { children?: React.ReactNode }, - React.JSXElementConstructor - >; -}; - -export type UIWelcomeScreenCenterComponents = { - [k in - | "Logo" - | "Heading" - | "Menu" - | "MenuItemLoadScene" - | "MenuItemHelp"]?: React.ReactElement< - { children?: React.ReactNode }, - React.JSXElementConstructor - >; -}; diff --git a/src/utils.ts b/src/utils.ts index ee8952da..60ad24f1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,7 +15,6 @@ import { FontFamilyValues, FontString } from "./element/types"; import { AppState, DataURL, LastActiveTool, Zoom } from "./types"; import { unstable_batchedUpdates } from "react-dom"; import { SHAPES } from "./shapes"; -import React from "react"; import { isEraserActive, isHandToolActive } from "./appState"; let mockDateTime: string | null = null; @@ -690,48 +689,6 @@ export const queryFocusableElements = (container: HTMLElement | null) => { : []; }; -/** - * Partitions React children into named components and the rest of children. - * - * Returns known children as a dictionary of react children keyed by their - * displayName, and the rest children as an array. - * - * NOTE all named react components are included in the dictionary, irrespective - * of the supplied type parameter. This means you may be throwing away - * children that you aren't expecting, but should nonetheless be rendered. - * To guard against this (provided you care about the rest children at all), - * supply a second parameter with an object with keys of the expected children. - */ -export const getReactChildren = < - KnownChildren extends { - [k in string]?: React.ReactNode; - }, ->( - children: React.ReactNode, - expectedComponents?: Record, -) => { - const restChildren: React.ReactNode[] = []; - - const knownChildren = React.Children.toArray(children).reduce( - (acc, child) => { - if ( - React.isValidElement(child) && - (!expectedComponents || - ((child.type as any).displayName as string) in expectedComponents) - ) { - // @ts-ignore - acc[child.type.displayName] = child; - } else { - restChildren.push(child); - } - return acc; - }, - {} as Partial, - ); - - return [knownChildren, restChildren] as const; -}; - export const isShallowEqual = >( objA: T, objB: T, diff --git a/yarn.lock b/yarn.lock index d06d613d..f14dc248 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10238,6 +10238,13 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" +tunnel-rat@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/tunnel-rat/-/tunnel-rat-0.1.0.tgz#62cfbaf1b24cabac9318fe45ef26d70dc40e86fe" + integrity sha512-/FKZLBXCoKhA7Wz+dsqitrItaLXYmT2bkZXod+1UuR4JqHtdb54yHvHhmMgLg+eyH1Od/CCnhA2VQQ2A/54Tcw== + dependencies: + zustand "^4.1.0" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -10406,6 +10413,11 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -11015,3 +11027,10 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@^4.1.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.2.tgz#bb121fcad84c5a569e94bd1a2695e1a93ba85d39" + integrity sha512-rd4haDmlwMTVWVqwvgy00ny8rtti/klRoZjFbL/MAcDnmD5qSw/RZc+Vddstdv90M5Lv6RPgWvm1Hivyn0QgJw== + dependencies: + use-sync-external-store "1.2.0"