feat: new Menu Component API (#6034)

* feat: new Menu Component API

* allow valid children types

* introduce menu group to group items

* Add lang footer

* use display name

* displayName

* define types inside

* fix default menu

* add json export to menu

* fix

* simplify expression

* put open menu into own compo to optimize perf

So that we don't rerun `useOutsideClickHook` (and rebind event listeners
all the time)

* naming tweaks

* rename MenuComponents->MenuDefaultItems and export default items from Menu.Items

* import Menu.scss in Menu.tsx

* move menu scss to excal app

* Don't filter children inside menu group

* move E+ out of socials

* support style prop for MenuItem and MenuGroup

* Support header in menu group and add Excalidraw links header for default items in social section

* rename header to title

* fix padding for lang

* render menu in mobile

* review fixes

* tweaks

* Export collaborators and show in mobile menu

* revert .env

* lint :p

* again lint

* show correct actions in view mode for mobile

* Whitelist Collaborators Comp

* mobile styling

* padding

* don't show nerds when menu open in mobile

* lint :(

* hide shortcuts

* refactor userlist to support mobile and keep a wrapper comp for excal app

* use only UserList

* render only on mobile for default items

* remove unused hooks

* Show collab button in menu when onCollabButtonClick present and hide export when UIOptions.canvasActions.export is false

* fix tests

* lint

* inject userlist inside menu on mobile

* revert userlist

* move menu socials to default menu

* fix collab

* use meny in library

* Make Menu generic and create hamburgemenu for public excal menu and use menu in library as well

* use appState.openMenu for mobile

* fix tests

* styling fixes and support style and class name in menu content

* fix test

* rename MenuDefaultItems->DefaultItems

* move footer css to its own comp

* rename HamburgerMenu -> MainMenu

* rename menu -> dropdownMenu and update classes, onClick->onToggle

* close main menu when dialog closes

* by bye filtering

* update docs

* fix lint

* update example, docs for useDevice and footer in mobile, rename menu ->DropDownMenu everywhere

* spec

* remove isMenuOpenAtom and set openMenu as canvas for main menu, render decreases in specs :)

* [temp] remove cyclic depenedency to fix build

* hack- update appstate to sync lang change

* Add more specs

* wip: rewrite MainMenu footer

* fix margin

* fix snaps

* not needed as lang list no more imported

* simplify custom footer rendering

* Add DropdownMenuItemLink and DropdownMenuItemCustom and update API, docs

* fix `MainMenu.ItemCustom`

* naming

* use onSelect and base class for custom items

* fix lint

* fix snap

* use custom item for lang

* update docs

* fix

* properly use `MainMenu.ItemCustom` for `LanguageList`

* add margin top to custom items

* flex

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Aakansha Doshi 2023-01-05 22:04:23 +05:30 committed by GitHub
parent 08afb857c3
commit 8420aecb34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1876 additions and 1911 deletions

View file

@ -0,0 +1,174 @@
import clsx from "clsx";
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { t } from "../../i18n";
import {
useExcalidrawAppState,
useExcalidrawSetAppState,
useExcalidrawActionManager,
} from "../App";
import { ExportIcon, ExportImageIcon, UsersIcon } from "../icons";
import { GithubIcon, DiscordIcon, TwitterIcon } from "../icons";
import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
import DropdownMenuItemLink from "../dropdownMenu/DropdownMenuItemLink";
export const LoadScene = () => {
const appState = useExcalidrawAppState();
const actionManager = useExcalidrawActionManager();
if (appState.viewModeEnabled) {
return null;
}
return actionManager.renderAction("loadScene");
};
LoadScene.displayName = "LoadScene";
export const SaveToActiveFile = () => {
const appState = useExcalidrawAppState();
const actionManager = useExcalidrawActionManager();
if (!appState.fileHandle) {
return null;
}
return actionManager.renderAction("saveToActiveFile");
};
SaveToActiveFile.displayName = "SaveToActiveFile";
export const SaveAsImage = () => {
const setAppState = useExcalidrawSetAppState();
// Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
return (
<DropdownMenuItem
icon={ExportImageIcon}
dataTestId="image-export-button"
onSelect={() => setAppState({ openDialog: "imageExport" })}
shortcut={getShortcutFromShortcutName("imageExport")}
ariaLabel={t("buttons.exportImage")}
>
{t("buttons.exportImage")}
</DropdownMenuItem>
);
};
SaveAsImage.displayName = "SaveAsImage";
export const Help = () => {
// Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
const actionManager = useExcalidrawActionManager();
return actionManager.renderAction("toggleShortcuts", undefined, true);
};
Help.displayName = "Help";
export const ClearCanvas = () => {
const appState = useExcalidrawAppState();
const actionManager = useExcalidrawActionManager();
if (appState.viewModeEnabled) {
return null;
}
return actionManager.renderAction("clearCanvas");
};
ClearCanvas.displayName = "ClearCanvas";
export const ToggleTheme = () => {
// Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
const actionManager = useExcalidrawActionManager();
return actionManager.renderAction("toggleTheme");
};
ToggleTheme.displayName = "ToggleTheme";
export const ChangeCanvasBackground = () => {
const appState = useExcalidrawAppState();
const actionManager = useExcalidrawActionManager();
if (appState.viewModeEnabled) {
return null;
}
return (
<div style={{ marginTop: "0.5rem" }}>
<div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}>
{t("labels.canvasBackground")}
</div>
<div style={{ padding: "0 0.625rem" }}>
{actionManager.renderAction("changeViewBackgroundColor")}
</div>
</div>
);
};
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
export const Export = () => {
// Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
const setAppState = useExcalidrawSetAppState();
return (
<DropdownMenuItem
icon={ExportIcon}
onSelect={() => {
setAppState({ openDialog: "jsonExport" });
}}
dataTestId="json-export-button"
ariaLabel={t("buttons.export")}
>
{t("buttons.export")}
</DropdownMenuItem>
);
};
Export.displayName = "Export";
export const Socials = () => (
<>
<DropdownMenuItemLink
icon={GithubIcon}
href="https://github.com/excalidraw/excalidraw"
ariaLabel="GitHub"
>
GitHub
</DropdownMenuItemLink>
<DropdownMenuItemLink
icon={DiscordIcon}
href="https://discord.gg/UexuTaE"
ariaLabel="Discord"
>
Discord
</DropdownMenuItemLink>
<DropdownMenuItemLink
icon={TwitterIcon}
href="https://twitter.com/excalidraw"
ariaLabel="Twitter"
>
Twitter
</DropdownMenuItemLink>
</>
);
Socials.displayName = "Socials";
export const LiveCollaboration = ({
onSelect,
isCollaborating,
}: {
onSelect: () => void;
isCollaborating: boolean;
}) => {
// Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
return (
<DropdownMenuItem
dataTestId="collab-button"
icon={UsersIcon}
className={clsx({
"active-collab": isCollaborating,
})}
onSelect={onSelect}
>
{t("labels.liveCollaboration")}
</DropdownMenuItem>
);
};
LiveCollaboration.displayName = "LiveCollaboration";

View file

@ -0,0 +1,56 @@
import React from "react";
import {
useDevice,
useExcalidrawAppState,
useExcalidrawSetAppState,
} from "../App";
import DropdownMenu from "../dropdownMenu/DropdownMenu";
import * as DefaultItems from "./DefaultItems";
import { UserList } from "../UserList";
import { t } from "../../i18n";
import { HamburgerMenuIcon } from "../icons";
const MainMenu = ({ children }: { children?: React.ReactNode }) => {
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
onToggle={() => {
setAppState({
openMenu: appState.openMenu === "canvas" ? null : "canvas",
});
}}
>
{HamburgerMenuIcon}
</DropdownMenu.Trigger>
<DropdownMenu.Content onClickOutside={onClickOutside}>
{children}
{device.isMobile && appState.collaborators.size > 0 && (
<fieldset className="UserList-Wrapper">
<legend>{t("labels.collaborators")}</legend>
<UserList mobile={true} collaborators={appState.collaborators} />
</fieldset>
)}
</DropdownMenu.Content>
</DropdownMenu>
);
};
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;
export default MainMenu;
MainMenu.displayName = "Menu";