Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/components/dropdown-menu.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*67NOTES:89MOBILE: Antd's Dropdown fully supports *nested* menus, with children.10This is great on a desktop, but is frequently completely unusable11on mobile, where the submenu appears off the screen, and is12hence completely unsable. Thus on mobile we basically flatten13then menu so it is still usable.1415*/16import { IS_MOBILE } from "@cocalc/frontend/feature";17import { DownOutlined } from "@ant-design/icons";18import { Button, Dropdown, Menu } from "antd";19import type { DropdownProps, MenuProps } from "antd";20import { useMemo, useState } from "react";2122export const STAY_OPEN_ON_CLICK = "stay-open-on-click";2324// overlay={menu} is deprecated. Instead, use MenuItems as items={...}.25export type MenuItems = NonNullable<MenuProps["items"]>;26export type MenuItem = MenuItems[number];2728/**29* NOTE: to work with this, make sure your list is typed as MenuItems. Then:30*31* const v: MenuItems = [32* { key: "a", label: "A", onClick: () => { action(key); } },33* ...34* { type: "divider" }, // for a divider35* ...36* ]37*/3839interface Props {40items: MenuItems;41// show menu as a *Button* (disabled on touch devices -- https://github.com/sagemathinc/cocalc/issues/5113)42button?: boolean;43disabled?: boolean;44showDown?: boolean;45id?: string;46maxHeight?: string;47style?;48title?: JSX.Element | string;49size?;50mode?: "vertical" | "inline";51defaultOpen?: boolean;52}5354export function DropdownMenu({55button,56disabled,57showDown,58id,59items: items0,60maxHeight,61style,62title,63size,64mode,65defaultOpen,66}: Props) {67const [open, setOpen] = useState<boolean>(!!defaultOpen);68const items = useMemo(() => {69return IS_MOBILE ? flatten(items0) : items0;70}, [items0]);7172let body = (73<Button74style={style}75disabled={disabled}76id={id}77size={size}78type={button ? undefined : "text"}79>80{title ? (81<>82{title} {showDown && <DownOutlined />}83</>84) : (85// empty title implies to only show the downward caret sign86<DownOutlined />87)}88</Button>89);9091if (disabled) {92return body;93}9495const handleMenuClick: MenuProps["onClick"] = (e) => {96if (e.key?.includes(STAY_OPEN_ON_CLICK)) {97setOpen(true);98} else {99setOpen(false);100}101};102103const handleOpenChange: DropdownProps["onOpenChange"] = (nextOpen, info) => {104if (info.source === "trigger" || nextOpen) {105setOpen(nextOpen);106}107};108109return (110<Dropdown111destroyPopupOnHide112trigger={["click"]}113placement={"bottomLeft"}114menu={{115items,116style: {117maxHeight: maxHeight ?? "70vh",118overflow: "auto",119},120mode,121onClick: handleMenuClick,122}}123disabled={disabled}124onOpenChange={handleOpenChange}125open={open}126>127{body}128</Dropdown>129);130}131132export function MenuItem(props) {133const M: any = Menu.Item;134return <M {...props}>{props.children}</M>;135}136137export const MenuDivider = { type: "divider" } as const;138139function flatten(items) {140const v: typeof items = [];141for (const item of items) {142if (item.children) {143const x = { ...item, disabled: true };144delete x.children;145v.push(x);146for (const i of flatten(item.children)) {147v.push({148...i,149label: <div style={{ marginLeft: "25px" }}>{i.label}</div>,150});151}152} else {153v.push(item);154}155}156return v;157}158159160