forked from facebook/react
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathContextMenu.js
More file actions
127 lines (107 loc) · 3.41 KB
/
ContextMenu.js
File metadata and controls
127 lines (107 loc) · 3.41 KB
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {useLayoutEffect} from 'react';
import {createPortal} from 'react-dom';
import ContextMenuItem from './ContextMenuItem';
import type {
ContextMenuItem as ContextMenuItemType,
ContextMenuPosition,
} from './types';
import styles from './ContextMenu.css';
function repositionToFit(element: HTMLElement, x: number, y: number) {
const ownerWindow = element.ownerDocument.defaultView;
if (y + element.offsetHeight >= ownerWindow.innerHeight) {
if (y - element.offsetHeight > 0) {
element.style.top = `${y - element.offsetHeight}px`;
} else {
element.style.top = '0px';
}
} else {
element.style.top = `${y}px`;
}
if (x + element.offsetWidth >= ownerWindow.innerWidth) {
if (x - element.offsetWidth > 0) {
element.style.left = `${x - element.offsetWidth}px`;
} else {
element.style.left = '0px';
}
} else {
element.style.left = `${x}px`;
}
}
type Props = {
anchorElementRef: {current: React.ElementRef<any> | null},
items: ContextMenuItemType[],
position: ContextMenuPosition,
hide: () => void,
};
export default function ContextMenu({
anchorElementRef,
position,
items,
hide,
}: Props): React.Node {
// This works on the assumption that ContextMenu component is only rendered when it should be shown
const anchor = anchorElementRef.current;
if (anchor == null) {
throw new Error(
'Attempted to open a context menu for an element, which is not mounted',
);
}
const ownerDocument = anchor.ownerDocument;
const portalContainer = ownerDocument.querySelector(
'[data-react-devtools-portal-root]',
);
const hideMenu = portalContainer == null || items.length === 0;
const menuRef = React.useRef<HTMLDivElement | null>(null);
useLayoutEffect(() => {
// Match the early-return condition below.
if (hideMenu) {
return;
}
const maybeMenu = menuRef.current;
if (maybeMenu === null) {
throw new Error(
"Can't access context menu element. This is a bug in React DevTools.",
);
}
const menu = (maybeMenu: HTMLDivElement);
function hideUnlessContains(event: Event) {
if (!menu.contains(((event.target: any): Node))) {
hide();
}
}
ownerDocument.addEventListener('mousedown', hideUnlessContains);
ownerDocument.addEventListener('touchstart', hideUnlessContains);
ownerDocument.addEventListener('keydown', hideUnlessContains);
const ownerWindow = ownerDocument.defaultView;
ownerWindow.addEventListener('resize', hide);
repositionToFit(menu, position.x, position.y);
return () => {
ownerDocument.removeEventListener('mousedown', hideUnlessContains);
ownerDocument.removeEventListener('touchstart', hideUnlessContains);
ownerDocument.removeEventListener('keydown', hideUnlessContains);
ownerWindow.removeEventListener('resize', hide);
};
}, [hideMenu]);
if (hideMenu) {
return null;
}
return createPortal(
<div className={styles.ContextMenu} ref={menuRef}>
{items.map(({onClick, content}, index) => (
<ContextMenuItem key={index} onClick={onClick} hide={hide}>
{content}
</ContextMenuItem>
))}
</div>,
portalContainer,
);
}