Merge remote-tracking branch 'origin/master' into danieljgeiger-mathjax

This commit is contained in:
Daniel J. Geiger 2023-01-24 19:27:05 -06:00
commit 5a3f4fd08f
93 changed files with 1308 additions and 567 deletions

View file

@ -223,9 +223,10 @@ export const ShapesSwitcher = ({
<>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
const label = t(`toolBar.${value}`);
const letter = key && (typeof key === "string" ? key : key[0]);
const letter =
key && capitalizeString(typeof key === "string" ? key : key[0]);
const shortcut = letter
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numericKey}`
? `${letter} ${t("helpDialog.or")} ${numericKey}`
: `${numericKey}`;
return (
<ToolButton
@ -236,7 +237,7 @@ export const ShapesSwitcher = ({
checked={activeTool.type === value}
name="editor-current-shape"
title={`${capitalizeString(label)}${shortcut}`}
keyBindingLabel={numericKey}
keyBindingLabel={numericKey || letter}
aria-label={capitalizeString(label)}
aria-keyshortcuts={shortcut}
data-testid={`toolbar-${value}`}

View file

@ -0,0 +1,35 @@
import { atom, useAtom } from "jotai";
import { actionClearCanvas } from "../actions";
import { t } from "../i18n";
import { useExcalidrawActionManager } from "./App";
import ConfirmDialog from "./ConfirmDialog";
export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
export const ActiveConfirmDialog = () => {
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
activeConfirmDialogAtom,
);
const actionManager = useExcalidrawActionManager();
if (!activeConfirmDialog) {
return null;
}
if (activeConfirmDialog === "clearCanvas") {
return (
<ConfirmDialog
onConfirm={() => {
actionManager.executeAction(actionClearCanvas);
setActiveConfirmDialog(null);
}}
onCancel={() => setActiveConfirmDialog(null)}
title={t("clearCanvasDialog.title")}
>
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
</ConfirmDialog>
);
}
return null;
};

View file

@ -41,7 +41,11 @@ import { ActionManager } from "../actions/manager";
import { getActions } from "../actions/register";
import { ActionResult } from "../actions/types";
import { trackEvent } from "../analytics";
import { getDefaultAppState, isEraserActive } from "../appState";
import {
getDefaultAppState,
isEraserActive,
isHandToolActive,
} from "../appState";
import { parseClipboard } from "../clipboard";
import {
APP_NAME,
@ -57,6 +61,7 @@ import {
EVENT,
GRID_SIZE,
IMAGE_RENDER_TIMEOUT,
isAndroid,
LINE_CONFIRM_THRESHOLD,
MAX_ALLOWED_FILE_BYTES,
MIME_TYPES,
@ -167,7 +172,6 @@ import {
shouldRotateWithDiscreteAngle,
isArrowKey,
KEYS,
isAndroid,
} from "../keys";
import { distance2d, getGridPoint, isPathALoop } from "../math";
import { renderScene } from "../renderer/renderScene";
@ -283,6 +287,7 @@ import {
import { shouldShowBoundingBox } from "../element/transformHandles";
import { Fonts } from "../scene/Fonts";
import { actionPaste } from "../actions/actionClipboard";
import { actionToggleHandTool } from "../actions/actionCanvas";
const deviceContextInitialValue = {
isSmScreen: false,
@ -622,6 +627,7 @@ class App extends React.Component<AppProps, AppState> {
elements={this.scene.getNonDeletedElements()}
onLockToggle={this.toggleLock}
onPenModeToggle={this.togglePenMode}
onHandToolToggle={this.onHandToolToggle}
onInsertElements={(elements) =>
this.addElementsFromPasteOrLibrary({
elements,
@ -1875,6 +1881,10 @@ class App extends React.Component<AppProps, AppState> {
});
};
onHandToolToggle = () => {
this.actionManager.executeAction(actionToggleHandTool);
};
scrollToContent = (
target:
| ExcalidrawElement
@ -2292,11 +2302,13 @@ class App extends React.Component<AppProps, AppState> {
private setActiveTool = (
tool:
| { type: typeof SHAPES[number]["value"] | "eraser" }
| { type: typeof SHAPES[number]["value"] | "eraser" | "hand" }
| { type: "custom"; customType: string },
) => {
const nextActiveTool = updateActiveTool(this.state, tool);
if (!isHoldingSpace) {
if (nextActiveTool.type === "hand") {
setCursor(this.canvas, CURSOR_TYPE.GRAB);
} else if (!isHoldingSpace) {
setCursorForShape(this.canvas, this.state);
}
if (isToolIcon(document.activeElement)) {
@ -2968,7 +2980,12 @@ class App extends React.Component<AppProps, AppState> {
null;
}
if (isHoldingSpace || isPanning || isDraggingScrollBar) {
if (
isHoldingSpace ||
isPanning ||
isDraggingScrollBar ||
isHandToolActive(this.state)
) {
return;
}
@ -3560,7 +3577,10 @@ class App extends React.Component<AppProps, AppState> {
);
} else if (this.state.activeTool.type === "custom") {
setCursor(this.canvas, CURSOR_TYPE.AUTO);
} else if (this.state.activeTool.type !== "eraser") {
} else if (
this.state.activeTool.type !== "eraser" &&
this.state.activeTool.type !== "hand"
) {
this.createGenericElementOnPointerDown(
this.state.activeTool.type,
pointerDownState,
@ -3671,6 +3691,7 @@ class App extends React.Component<AppProps, AppState> {
gesture.pointers.size <= 1 &&
(event.button === POINTER_BUTTON.WHEEL ||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
isHandToolActive(this.state) ||
this.state.viewModeEnabled)
) ||
isTextElement(this.state.editingElement)

View file

@ -96,6 +96,10 @@
width: 5rem;
height: 5rem;
margin: 0 0.2em;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 1rem;
background-color: var(--button-color);

View file

@ -0,0 +1,32 @@
import "./ToolIcon.scss";
import clsx from "clsx";
import { ToolButton } from "./ToolButton";
import { handIcon } from "./icons";
import { KEYS } from "../keys";
type LockIconProps = {
title?: string;
name?: string;
checked: boolean;
onChange?(): void;
isMobile?: boolean;
};
export const HandButton = (props: LockIconProps) => {
return (
<ToolButton
className={clsx("Shape", { fillable: false })}
type="radio"
icon={handIcon}
name="editor-current-shape"
checked={props.checked}
title={`${props.title} — H`}
keyBindingLabel={!props.isMobile ? KEYS.H.toLocaleUpperCase() : undefined}
aria-label={`${props.title} — H`}
aria-keyshortcuts={KEYS.H}
data-testid={`toolbar-hand`}
onChange={() => props.onChange?.()}
/>
);
};

View file

@ -1,10 +1,12 @@
import React from "react";
import { t } from "../i18n";
import { isDarwin, isWindows, KEYS } from "../keys";
import { KEYS } from "../keys";
import { Dialog } from "./Dialog";
import { getShortcutKey } from "../utils";
import "./HelpDialog.scss";
import { ExternalLinkIcon } from "./icons";
import { probablySupportsClipboardBlob } from "../clipboard";
import { isDarwin, isFirefox, isWindows } from "../constants";
const Header = () => (
<div className="HelpDialog__header">
@ -67,6 +69,10 @@ function* intersperse(as: JSX.Element[][], delim: string | null) {
}
}
const upperCaseSingleChars = (str: string) => {
return str.replace(/\b[a-z]\b/, (c) => c.toUpperCase());
};
const Shortcut = ({
label,
shortcuts,
@ -81,7 +87,9 @@ const Shortcut = ({
? [...shortcut.slice(0, -2).split("+"), "+"]
: shortcut.split("+");
return keys.map((key) => <ShortcutKey key={key}>{key}</ShortcutKey>);
return keys.map((key) => (
<ShortcutKey key={key}>{upperCaseSingleChars(key)}</ShortcutKey>
));
});
return (
@ -118,6 +126,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
className="HelpDialog__island--tools"
caption={t("helpDialog.tools")}
>
<Shortcut label={t("toolBar.hand")} shortcuts={[KEYS.H]} />
<Shortcut
label={t("toolBar.selection")}
shortcuts={[KEYS.V, KEYS["1"]]}
@ -304,10 +313,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.pasteAsPlaintext")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
/>
<Shortcut
label={t("labels.copyAsPng")}
shortcuts={[getShortcutKey("Shift+Alt+C")]}
/>
{/* firefox supports clipboard API under a flag, so we'll
show users what they can do in the error message */}
{(probablySupportsClipboardBlob || isFirefox) && (
<Shortcut
label={t("labels.copyAsPng")}
shortcuts={[getShortcutKey("Shift+Alt+C")]}
/>
)}
<Shortcut
label={t("labels.copyStyles")}
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}

View file

@ -12,7 +12,7 @@ import Stack from "./Stack";
import "./ExportDialog.scss";
import OpenColor from "open-color";
import { CheckboxItem } from "./CheckboxItem";
import { DEFAULT_EXPORT_PADDING } from "../constants";
import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
import { nativeFileSystemSupported } from "../data/filesystem";
import { ActionManager } from "../actions/manager";
@ -190,7 +190,9 @@ const ImageExportModal = ({
>
SVG
</ExportButton>
{probablySupportsClipboardBlob && (
{/* firefox supports clipboard API under a flag,
so let's throw and tell people what they can do */}
{(probablySupportsClipboardBlob || isFirefox) && (
<ExportButton
title={t("buttons.copyPngToClipboard")}
onClick={() => onExportToClipboard(exportedElements)}

View file

@ -50,6 +50,9 @@ import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
import { jotaiScope } from "../jotai";
import { useAtom } from "jotai";
import MainMenu from "./main-menu/MainMenu";
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
import { HandButton } from "./HandButton";
import { isHandToolActive } from "../appState";
interface LayerUIProps {
actionManager: ActionManager;
@ -59,6 +62,7 @@ interface LayerUIProps {
setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
onLockToggle: () => void;
onHandToolToggle: () => void;
onPenModeToggle: () => void;
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
showExitZenModeBtn: boolean;
@ -85,6 +89,7 @@ const LayerUI = ({
elements,
canvas,
onLockToggle,
onHandToolToggle,
onPenModeToggle,
onInsertElements,
showExitZenModeBtn,
@ -304,13 +309,20 @@ const LayerUI = ({
penDetected={appState.penDetected}
/>
<LockButton
zenModeEnabled={appState.zenModeEnabled}
checked={appState.activeTool.locked}
onChange={() => onLockToggle()}
onChange={onLockToggle}
title={t("toolBar.lock")}
/>
<div className="App-toolbar__divider"></div>
<HandButton
checked={isHandToolActive(appState)}
onChange={() => onHandToolToggle()}
title={t("toolBar.hand")}
isMobile
/>
<ShapesSwitcher
appState={appState}
canvas={canvas}
@ -322,9 +334,6 @@ const LayerUI = ({
});
}}
/>
{/* {actionManager.renderAction("eraser", {
// size: "small",
})} */}
</Stack.Row>
</Island>
</Stack.Row>
@ -386,6 +395,7 @@ const LayerUI = ({
}}
/>
)}
<ActiveConfirmDialog />
{renderImageExportDialog()}
{renderJSONExportDialog()}
{appState.pasteDialog.shown && (
@ -408,7 +418,8 @@ const LayerUI = ({
renderJSONExportDialog={renderJSONExportDialog}
renderImageExportDialog={renderImageExportDialog}
setAppState={setAppState}
onLockToggle={() => onLockToggle()}
onLockToggle={onLockToggle}
onHandToolToggle={onHandToolToggle}
onPenModeToggle={onPenModeToggle}
canvas={canvas}
onImageAction={onImageAction}

View file

@ -187,6 +187,7 @@ export const LibraryMenuHeader: React.FC<{
</DropdownMenu.Trigger>
<DropdownMenu.Content
onClickOutside={() => setIsLibraryMenuOpen(false)}
onSelect={() => setIsLibraryMenuOpen(false)}
className="library-menu"
>
{!itemsSelected && (

View file

@ -9,7 +9,6 @@ type LockIconProps = {
name?: string;
checked: boolean;
onChange?(): void;
zenModeEnabled?: boolean;
isMobile?: boolean;
};

View file

@ -22,6 +22,8 @@ import { LibraryButton } from "./LibraryButton";
import { PenModeButton } from "./PenModeButton";
import { Stats } from "./Stats";
import { actionToggleStats } from "../actions";
import { HandButton } from "./HandButton";
import { isHandToolActive } from "../appState";
type MobileMenuProps = {
appState: AppState;
@ -31,6 +33,7 @@ type MobileMenuProps = {
setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
onLockToggle: () => void;
onHandToolToggle: () => void;
onPenModeToggle: () => void;
canvas: HTMLCanvasElement | null;
@ -52,6 +55,7 @@ export const MobileMenu = ({
actionManager,
setAppState,
onLockToggle,
onHandToolToggle,
onPenModeToggle,
canvas,
onImageAction,
@ -88,6 +92,13 @@ export const MobileMenu = ({
</Island>
{renderTopRightUI && renderTopRightUI(true, appState)}
<div className="mobile-misc-tools-container">
{!appState.viewModeEnabled && (
<LibraryButton
appState={appState}
setAppState={setAppState}
isMobile
/>
)}
<PenModeButton
checked={appState.penMode}
onChange={onPenModeToggle}
@ -101,13 +112,12 @@ export const MobileMenu = ({
title={t("toolBar.lock")}
isMobile
/>
{!appState.viewModeEnabled && (
<LibraryButton
appState={appState}
setAppState={setAppState}
isMobile
/>
)}
<HandButton
checked={isHandToolActive(appState)}
onChange={() => onHandToolToggle()}
title={t("toolBar.hand")}
isMobile
/>
</div>
</Stack.Row>
</Stack.Col>

View file

@ -19,7 +19,7 @@ type ToolButtonBaseProps = {
name?: string;
id?: string;
size?: ToolButtonSize;
keyBindingLabel?: string;
keyBindingLabel?: string | null;
showAriaLabel?: boolean;
hidden?: boolean;
visible?: boolean;

View file

@ -4,16 +4,23 @@ import { Island } from "../Island";
import { useDevice } from "../App";
import clsx from "clsx";
import Stack from "../Stack";
import React from "react";
import { DropdownMenuContentPropsContext } from "./common";
const MenuContent = ({
children,
onClickOutside,
className = "",
onSelect,
style,
}: {
children?: React.ReactNode;
onClickOutside?: () => void;
className?: string;
/**
* Called when any menu item is selected (clicked on).
*/
onSelect?: (event: Event) => void;
style?: React.CSSProperties;
}) => {
const device = useDevice();
@ -24,28 +31,32 @@ const MenuContent = ({
const classNames = clsx(`dropdown-menu ${className}`, {
"dropdown-menu--mobile": device.isMobile,
}).trim();
return (
<div
ref={menuRef}
className={classNames}
style={style}
data-testid="dropdown-menu"
>
{/* the zIndex ensures this menu has higher stacking order,
<DropdownMenuContentPropsContext.Provider value={{ onSelect }}>
<div
ref={menuRef}
className={classNames}
style={style}
data-testid="dropdown-menu"
>
{/* the zIndex ensures this menu has higher stacking order,
see https://github.com/excalidraw/excalidraw/pull/1445 */}
{device.isMobile ? (
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
) : (
<Island
className="dropdown-menu-container"
padding={2}
style={{ zIndex: 1 }}
>
{children}
</Island>
)}
</div>
{device.isMobile ? (
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
) : (
<Island
className="dropdown-menu-container"
padding={2}
style={{ zIndex: 1 }}
>
{children}
</Island>
)}
</div>
</DropdownMenuContentPropsContext.Provider>
);
};
export default MenuContent;
MenuContent.displayName = "DropdownMenuContent";
export default MenuContent;

View file

@ -1,10 +1,10 @@
import React from "react";
import {
getDrodownMenuItemClassName,
useHandleDropdownMenuItemClick,
} from "./common";
import MenuItemContent from "./DropdownMenuItemContent";
export const getDrodownMenuItemClassName = (className = "") => {
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
};
const DropdownMenuItem = ({
icon,
onSelect,
@ -14,15 +14,17 @@ const DropdownMenuItem = ({
...rest
}: {
icon?: JSX.Element;
onSelect: () => void;
onSelect: (event: Event) => void;
children: React.ReactNode;
shortcut?: string;
className?: string;
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
return (
<button
{...rest}
onClick={onSelect}
onClick={handleClick}
type="button"
className={getDrodownMenuItemClassName(className)}
title={rest.title ?? rest["aria-label"]}

View file

@ -1,20 +1,28 @@
import MenuItemContent from "./DropdownMenuItemContent";
import React from "react";
import { getDrodownMenuItemClassName } from "./DropdownMenuItem";
import {
getDrodownMenuItemClassName,
useHandleDropdownMenuItemClick,
} from "./common";
const DropdownMenuItemLink = ({
icon,
shortcut,
href,
children,
onSelect,
className = "",
...rest
}: {
href: string;
icon?: JSX.Element;
children: React.ReactNode;
shortcut?: string;
className?: string;
href: string;
onSelect?: (event: Event) => void;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
return (
<a
{...rest}
@ -23,6 +31,7 @@ const DropdownMenuItemLink = ({
rel="noreferrer"
className={getDrodownMenuItemClassName(className)}
title={rest.title ?? rest["aria-label"]}
onClick={handleClick}
>
<MenuItemContent icon={icon} shortcut={shortcut}>
{children}

View file

@ -0,0 +1,31 @@
import React, { useContext } from "react";
import { EVENT } from "../../constants";
import { composeEventHandlers } from "../../utils";
export const DropdownMenuContentPropsContext = React.createContext<{
onSelect?: (event: Event) => void;
}>({});
export const getDrodownMenuItemClassName = (className = "") => {
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
};
export const useHandleDropdownMenuItemClick = (
origOnClick:
| React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>
| undefined,
onSelect: ((event: Event) => void) | undefined,
) => {
const DropdownMenuContentProps = useContext(DropdownMenuContentPropsContext);
return composeEventHandlers(origOnClick, (event) => {
const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {
bubbles: true,
cancelable: true,
});
onSelect?.(itemSelectEvent);
if (!itemSelectEvent.defaultPrevented) {
DropdownMenuContentProps.onSelect?.(itemSelectEvent);
}
});
};

View file

@ -1532,3 +1532,14 @@ export const publishIcon = createIcon(
export const eraser = createIcon(
<path d="M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z" />,
);
export const handIcon = createIcon(
<g strokeWidth={1.25}>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M8 13v-7.5a1.5 1.5 0 0 1 3 0v6.5"></path>
<path d="M11 5.5v-2a1.5 1.5 0 1 1 3 0v8.5"></path>
<path d="M14 5.5a1.5 1.5 0 0 1 3 0v6.5"></path>
<path d="M17 7.5a1.5 1.5 0 0 1 3 0v8.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47"></path>
</g>,
tablerIconProps,
);

View file

@ -28,9 +28,9 @@ import {
} from "../../actions";
import "./DefaultItems.scss";
import { useState } from "react";
import ConfirmDialog from "../ConfirmDialog";
import clsx from "clsx";
import { useSetAtom } from "jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
export const LoadScene = () => {
// FIXME Hack until we tie "t" to lang state
@ -122,41 +122,22 @@ export const ClearCanvas = () => {
// FIXME Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
const actionManager = useExcalidrawActionManager();
const [showDialog, setShowDialog] = useState(false);
const toggleDialog = () => setShowDialog(!showDialog);
if (!actionManager.isActionEnabled(actionClearCanvas)) {
return null;
}
return (
<>
<DropdownMenuItem
icon={TrashIcon}
onSelect={toggleDialog}
data-testid="clear-canvas-button"
aria-label={t("buttons.clearReset")}
>
{t("buttons.clearReset")}
</DropdownMenuItem>
{/* FIXME this should live outside MainMenu so it stays open
if menu is closed */}
{showDialog && (
<ConfirmDialog
onConfirm={() => {
actionManager.executeAction(actionClearCanvas);
toggleDialog();
}}
onCancel={toggleDialog}
title={t("clearCanvasDialog.title")}
>
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
</ConfirmDialog>
)}
</>
<DropdownMenuItem
icon={TrashIcon}
onSelect={() => setActiveConfirmDialog("clearCanvas")}
data-testid="clear-canvas-button"
aria-label={t("buttons.clearReset")}
>
{t("buttons.clearReset")}
</DropdownMenuItem>
);
};
ClearCanvas.displayName = "ClearCanvas";
@ -171,7 +152,9 @@ export const ToggleTheme = () => {
return (
<DropdownMenuItem
onSelect={() => {
onSelect={(event) => {
// do not close the menu when changing theme
event.preventDefault();
return actionManager.executeAction(actionToggleTheme);
}}
icon={appState.theme === "dark" ? SunIcon : MoonIcon}

View file

@ -11,14 +11,25 @@ import * as DefaultItems from "./DefaultItems";
import { UserList } from "../UserList";
import { t } from "../../i18n";
import { HamburgerMenuIcon } from "../icons";
import { composeEventHandlers } from "../../utils";
const MainMenu = ({ children }: { children?: React.ReactNode }) => {
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 });
return (
<DropdownMenu open={appState.openMenu === "canvas"}>
<DropdownMenu.Trigger
@ -30,7 +41,12 @@ const MainMenu = ({ children }: { children?: React.ReactNode }) => {
>
{HamburgerMenuIcon}
</DropdownMenu.Trigger>
<DropdownMenu.Content onClickOutside={onClickOutside}>
<DropdownMenu.Content
onClickOutside={onClickOutside}
onSelect={composeEventHandlers(onSelect, () => {
setAppState({ openMenu: null });
})}
>
{children}
{device.isMobile && appState.collaborators.size > 0 && (
<fieldset className="UserList-Wrapper">