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 <aakansha1216@gmail.com>
This commit is contained in:
David Luzar 2023-01-31 13:53:20 +01:00 committed by GitHub
parent 3a141ca77a
commit e6de1fe4a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 417 additions and 372 deletions

View file

@ -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 (
<MainMenu __fallback>
<MainMenu.DefaultItems.LoadScene />
<MainMenu.DefaultItems.SaveToActiveFile />
{/* FIXME we should to test for this inside the item itself */}
{UIOptions.canvasActions.export && <MainMenu.DefaultItems.Export />}
{/* FIXME we should to test for this inside the item itself */}
{UIOptions.canvasActions.saveAsImage && (
<MainMenu.DefaultItems.SaveAsImage />
)}
<MainMenu.DefaultItems.Help />
<MainMenu.DefaultItems.ClearCanvas />
<MainMenu.Separator />
<MainMenu.Group title="Excalidraw links">
<MainMenu.DefaultItems.Socials />
</MainMenu.Group>
<MainMenu.Separator />
<MainMenu.DefaultItems.ToggleTheme />
<MainMenu.DefaultItems.ChangeCanvasBackground />
</MainMenu>
);
};
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<UIChildrenComponents>(children, {
Menu: true,
FooterCenter: true,
WelcomeScreen: true,
});
const [WelcomeScreenComponents] = getReactChildren<UIWelcomeScreenComponents>(
renderWelcomeScreen
? (
childrenComponents?.WelcomeScreen ?? (
<WelcomeScreen>
<WelcomeScreen.Center />
<WelcomeScreen.Hints.MenuHint />
<WelcomeScreen.Hints.ToolbarHint />
<WelcomeScreen.Hints.HelpHint />
</WelcomeScreen>
)
)?.props?.children
: null,
);
const renderJSONExportDialog = () => {
if (!UIOptions.canvasActions.export) {
return null;
@ -197,37 +197,12 @@ const LayerUI = ({
);
};
const renderMenu = () => {
return (
childrenComponents.Menu || (
<MainMenu>
<MainMenu.DefaultItems.LoadScene />
<MainMenu.DefaultItems.SaveToActiveFile />
{/* FIXME we should to test for this inside the item itself */}
{UIOptions.canvasActions.export && <MainMenu.DefaultItems.Export />}
{/* FIXME we should to test for this inside the item itself */}
{UIOptions.canvasActions.saveAsImage && (
<MainMenu.DefaultItems.SaveAsImage />
)}
<MainMenu.DefaultItems.Help />
<MainMenu.DefaultItems.ClearCanvas />
<MainMenu.Separator />
<MainMenu.Group title="Excalidraw links">
<MainMenu.DefaultItems.Socials />
</MainMenu.Group>
<MainMenu.Separator />
<MainMenu.DefaultItems.ToggleTheme />
<MainMenu.DefaultItems.ChangeCanvasBackground />
</MainMenu>
)
);
};
const renderCanvasActions = () => (
<div style={{ position: "relative" }}>
{WelcomeScreenComponents.MenuHint}
{/* wrapping to Fragment stops React from occasionally complaining
about identical Keys */}
<>{renderMenu()}</>
<mainMenuTunnel.Out />
{renderWelcomeScreen && <welcomeScreenMenuHintTunnel.Out />}
</div>
);
@ -264,7 +239,6 @@ const LayerUI = ({
return (
<FixedSideContainer side="top">
{WelcomeScreenComponents.Center}
<div className="App-menu App-menu_top">
<Stack.Col
gap={6}
@ -279,7 +253,9 @@ const LayerUI = ({
<Section heading="shapes" className="shapes-section">
{(heading: React.ReactNode) => (
<div style={{ position: "relative" }}>
{WelcomeScreenComponents.ToolbarHint}
{renderWelcomeScreen && (
<welcomeScreenToolbarHintTunnel.Out />
)}
<Stack.Col gap={4} align="start">
<Stack.Row
gap={1}
@ -380,7 +356,16 @@ const LayerUI = ({
return (
<>
{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. */}
<DefaultMainMenu UIOptions={UIOptions} />
{/* ------------------------------------------------------------------ */}
{appState.isLoading && <LoadingMessage delay={250} />}
{appState.errorMessage && (
<ErrorDialog
@ -427,8 +412,6 @@ const LayerUI = ({
renderCustomStats={renderCustomStats}
renderSidebars={renderSidebars}
device={device}
renderMenu={renderMenu}
welcomeScreenCenter={WelcomeScreenComponents.Center}
/>
)}
@ -451,13 +434,13 @@ const LayerUI = ({
: {}
}
>
{renderWelcomeScreen && <welcomeScreenCenterTunnel.Out />}
{renderFixedSideContainer()}
<Footer
appState={appState}
actionManager={actionManager}
showExitZenModeBtn={showExitZenModeBtn}
footerCenter={childrenComponents.FooterCenter}
welcomeScreenHelp={WelcomeScreenComponents.HelpHint}
renderWelcomeScreen={renderWelcomeScreen}
/>
{appState.showStats && (
<Stats