-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
Copy pathcontextMenuSingleton.tsx
104 lines (93 loc) · 4.21 KB
/
contextMenuSingleton.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/*
* Copyright 2023 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from "react";
import * as ReactDOM from "react-dom";
import { Classes } from "../../common";
import type { DOMMountOptions } from "../../common/utils/mountOptions";
import { OverlaysProvider } from "../../context/overlays/overlaysProvider";
import { ContextMenuPopover, type ContextMenuPopoverProps } from "./contextMenuPopover";
/** DOM element which contains the context menu singleton instance for the imperative ContextMenu APIs. */
let contextMenuElement: HTMLElement | undefined;
/**
* Show a context menu at a particular offset from the top-left corner of the document.
* The menu will appear below-right of this point and will flip to below-left if there is not enough
* room onscreen. Additional props like `onClose`, `isDarkTheme`, etc. can be forwarded to the `<ContextMenuPopover>`.
*
* Context menus created with this API will automatically close when a user clicks outside the popover.
* You may force them to close by using `hideContextMenu()`.
*
* Note that this API relies on global state in the @blueprintjs/core package, and should be used with caution,
* especially if your build system allows multiple copies of Blueprint libraries to be bundled into an application at
* once.
*
* Alternative APIs to consider which do not have the limitations of global state:
* - `<ContextMenu>`
* - `<ContextMenuPopover>`
*
* @see https://blueprintjs.com/docs/#core/components/context-menu-popover.imperative-api
*/
export function showContextMenu(
props: Omit<ContextMenuPopoverProps, "isOpen">,
options: DOMMountOptions<ContextMenuPopoverProps> = {},
) {
const {
container = document.body,
domRenderer = ReactDOM.render,
domUnmounter = ReactDOM.unmountComponentAtNode,
} = options;
if (contextMenuElement === undefined) {
contextMenuElement = document.createElement("div");
contextMenuElement.classList.add(Classes.CONTEXT_MENU);
container.appendChild(contextMenuElement);
} else {
// N.B. It's important to unmount previous instances of the ContextMenuPopover rendered by this function.
// Otherwise, React will detect no change in props sent to the already-mounted component, and therefore
// do nothing after the first call to this function, leading to bugs like https://github.com/palantir/blueprint/issues/5949
domUnmounter(contextMenuElement);
}
domRenderer(
<OverlaysProvider>
<UncontrolledContextMenuPopover {...props} />
</OverlaysProvider>,
contextMenuElement,
);
}
/**
* Hide a context menu that was created using `showContextMenu()`.
*
* Note that this API relies on global state in the @blueprintjs/core package, and should be used with caution.
*
* @see https://blueprintjs.com/docs/#core/components/context-menu-popover.imperative-api
*/
export function hideContextMenu(options: DOMMountOptions<ContextMenuPopoverProps> = {}) {
const { domUnmounter = ReactDOM.unmountComponentAtNode } = options;
if (contextMenuElement !== undefined) {
domUnmounter(contextMenuElement);
contextMenuElement = undefined;
}
}
/**
* A simple wrapper around `ContextMenuPopover` which is open by default and uncontrolled.
* It closes when a user clicks outside the popover.
*/
function UncontrolledContextMenuPopover({ onClose, ...props }: Omit<ContextMenuPopoverProps, "isOpen">) {
const [isOpen, setIsOpen] = React.useState(true);
const handleClose = React.useCallback(() => {
setIsOpen(false);
onClose?.();
}, [onClose]);
return <ContextMenuPopover isOpen={isOpen} {...props} onClose={handleClose} />;
}