CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/components/dropdown-menu.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
8
NOTES:
9
10
MOBILE: Antd's Dropdown fully supports *nested* menus, with children.
11
This is great on a desktop, but is frequently completely unusable
12
on mobile, where the submenu appears off the screen, and is
13
hence completely unsable. Thus on mobile we basically flatten
14
then menu so it is still usable.
15
16
*/
17
import { IS_MOBILE } from "@cocalc/frontend/feature";
18
import { DownOutlined } from "@ant-design/icons";
19
import { Button, Dropdown, Menu } from "antd";
20
import type { DropdownProps, MenuProps } from "antd";
21
import { useMemo, useState } from "react";
22
23
export const STAY_OPEN_ON_CLICK = "stay-open-on-click";
24
25
// overlay={menu} is deprecated. Instead, use MenuItems as items={...}.
26
export type MenuItems = NonNullable<MenuProps["items"]>;
27
export type MenuItem = MenuItems[number];
28
29
/**
30
* NOTE: to work with this, make sure your list is typed as MenuItems. Then:
31
*
32
* const v: MenuItems = [
33
* { key: "a", label: "A", onClick: () => { action(key); } },
34
* ...
35
* { type: "divider" }, // for a divider
36
* ...
37
* ]
38
*/
39
40
interface Props {
41
items: MenuItems;
42
// show menu as a *Button* (disabled on touch devices -- https://github.com/sagemathinc/cocalc/issues/5113)
43
button?: boolean;
44
disabled?: boolean;
45
showDown?: boolean;
46
id?: string;
47
maxHeight?: string;
48
style?;
49
title?: JSX.Element | string;
50
size?;
51
mode?: "vertical" | "inline";
52
defaultOpen?: boolean;
53
}
54
55
export function DropdownMenu({
56
button,
57
disabled,
58
showDown,
59
id,
60
items: items0,
61
maxHeight,
62
style,
63
title,
64
size,
65
mode,
66
defaultOpen,
67
}: Props) {
68
const [open, setOpen] = useState<boolean>(!!defaultOpen);
69
const items = useMemo(() => {
70
return IS_MOBILE ? flatten(items0) : items0;
71
}, [items0]);
72
73
let body = (
74
<Button
75
style={style}
76
disabled={disabled}
77
id={id}
78
size={size}
79
type={button ? undefined : "text"}
80
>
81
{title ? (
82
<>
83
{title} {showDown && <DownOutlined />}
84
</>
85
) : (
86
// empty title implies to only show the downward caret sign
87
<DownOutlined />
88
)}
89
</Button>
90
);
91
92
if (disabled) {
93
return body;
94
}
95
96
const handleMenuClick: MenuProps["onClick"] = (e) => {
97
if (e.key?.includes(STAY_OPEN_ON_CLICK)) {
98
setOpen(true);
99
} else {
100
setOpen(false);
101
}
102
};
103
104
const handleOpenChange: DropdownProps["onOpenChange"] = (nextOpen, info) => {
105
if (info.source === "trigger" || nextOpen) {
106
setOpen(nextOpen);
107
}
108
};
109
110
return (
111
<Dropdown
112
destroyPopupOnHide
113
trigger={["click"]}
114
placement={"bottomLeft"}
115
menu={{
116
items,
117
style: {
118
maxHeight: maxHeight ?? "70vh",
119
overflow: "auto",
120
},
121
mode,
122
onClick: handleMenuClick,
123
}}
124
disabled={disabled}
125
onOpenChange={handleOpenChange}
126
open={open}
127
>
128
{body}
129
</Dropdown>
130
);
131
}
132
133
export function MenuItem(props) {
134
const M: any = Menu.Item;
135
return <M {...props}>{props.children}</M>;
136
}
137
138
export const MenuDivider = { type: "divider" } as const;
139
140
function flatten(items) {
141
const v: typeof items = [];
142
for (const item of items) {
143
if (item.children) {
144
const x = { ...item, disabled: true };
145
delete x.children;
146
v.push(x);
147
for (const i of flatten(item.children)) {
148
v.push({
149
...i,
150
label: <div style={{ marginLeft: "25px" }}>{i.label}</div>,
151
});
152
}
153
} else {
154
v.push(item);
155
}
156
}
157
return v;
158
}
159
160