|
8 | 8 | */
|
9 | 9 |
|
10 | 10 | import * as React from 'react';
|
11 |
| -import {useContext, useEffect, useLayoutEffect, useRef, useState} from 'react'; |
| 11 | +import {useLayoutEffect, createRef} from 'react'; |
12 | 12 | import {createPortal} from 'react-dom';
|
13 |
| -import {RegistryContext} from './Contexts'; |
14 | 13 |
|
15 |
| -import styles from './ContextMenu.css'; |
| 14 | +import ContextMenuItem from './ContextMenuItem'; |
| 15 | + |
| 16 | +import type { |
| 17 | + ContextMenuItem as ContextMenuItemType, |
| 18 | + ContextMenuPosition, |
| 19 | + ContextMenuRef, |
| 20 | +} from './types'; |
16 | 21 |
|
17 |
| -import type {RegistryContextType} from './Contexts'; |
| 22 | +import styles from './ContextMenu.css'; |
18 | 23 |
|
19 |
| -function repositionToFit(element: HTMLElement, pageX: number, pageY: number) { |
| 24 | +function repositionToFit(element: HTMLElement, x: number, y: number) { |
20 | 25 | const ownerWindow = element.ownerDocument.defaultView;
|
21 |
| - if (element !== null) { |
22 |
| - if (pageY + element.offsetHeight >= ownerWindow.innerHeight) { |
23 |
| - if (pageY - element.offsetHeight > 0) { |
24 |
| - element.style.top = `${pageY - element.offsetHeight}px`; |
25 |
| - } else { |
26 |
| - element.style.top = '0px'; |
27 |
| - } |
| 26 | + if (y + element.offsetHeight >= ownerWindow.innerHeight) { |
| 27 | + if (y - element.offsetHeight > 0) { |
| 28 | + element.style.top = `${y - element.offsetHeight}px`; |
28 | 29 | } else {
|
29 |
| - element.style.top = `${pageY}px`; |
| 30 | + element.style.top = '0px'; |
30 | 31 | }
|
| 32 | + } else { |
| 33 | + element.style.top = `${y}px`; |
| 34 | + } |
31 | 35 |
|
32 |
| - if (pageX + element.offsetWidth >= ownerWindow.innerWidth) { |
33 |
| - if (pageX - element.offsetWidth > 0) { |
34 |
| - element.style.left = `${pageX - element.offsetWidth}px`; |
35 |
| - } else { |
36 |
| - element.style.left = '0px'; |
37 |
| - } |
| 36 | + if (x + element.offsetWidth >= ownerWindow.innerWidth) { |
| 37 | + if (x - element.offsetWidth > 0) { |
| 38 | + element.style.left = `${x - element.offsetWidth}px`; |
38 | 39 | } else {
|
39 |
| - element.style.left = `${pageX}px`; |
| 40 | + element.style.left = '0px'; |
40 | 41 | }
|
| 42 | + } else { |
| 43 | + element.style.left = `${x}px`; |
41 | 44 | }
|
42 | 45 | }
|
43 | 46 |
|
44 |
| -const HIDDEN_STATE = { |
45 |
| - data: null, |
46 |
| - isVisible: false, |
47 |
| - pageX: 0, |
48 |
| - pageY: 0, |
49 |
| -}; |
50 |
| - |
51 | 47 | type Props = {
|
52 |
| - children: (data: Object) => React$Node, |
53 |
| - id: string, |
| 48 | + anchorElementRef: {current: React.ElementRef<any> | null}, |
| 49 | + items: ContextMenuItemType[], |
| 50 | + position: ContextMenuPosition, |
| 51 | + hide: () => void, |
| 52 | + ref?: ContextMenuRef, |
54 | 53 | };
|
55 | 54 |
|
56 |
| -export default function ContextMenu({children, id}: Props): React.Node { |
57 |
| - const {hideMenu, registerMenu} = |
58 |
| - useContext<RegistryContextType>(RegistryContext); |
59 |
| - |
60 |
| - const [state, setState] = useState(HIDDEN_STATE); |
| 55 | +export default function ContextMenu({ |
| 56 | + anchorElementRef, |
| 57 | + position, |
| 58 | + items, |
| 59 | + hide, |
| 60 | + ref = createRef(), |
| 61 | +}: Props): React.Node { |
| 62 | + // This works on the assumption that ContextMenu component is only rendered when it should be shown |
| 63 | + const anchor = anchorElementRef.current; |
| 64 | + |
| 65 | + if (anchor == null) { |
| 66 | + throw new Error( |
| 67 | + 'Attempted to open a context menu for an element, which is not mounted', |
| 68 | + ); |
| 69 | + } |
61 | 70 |
|
62 |
| - const bodyAccessorRef = useRef(null); |
63 |
| - const containerRef = useRef(null); |
64 |
| - const menuRef = useRef(null); |
| 71 | + const ownerDocument = anchor.ownerDocument; |
| 72 | + const portalContainer = ownerDocument.querySelector( |
| 73 | + '[data-react-devtools-portal-root]', |
| 74 | + ); |
65 | 75 |
|
66 |
| - useEffect(() => { |
67 |
| - const element = bodyAccessorRef.current; |
68 |
| - if (element !== null) { |
69 |
| - const ownerDocument = element.ownerDocument; |
70 |
| - containerRef.current = ownerDocument.querySelector( |
71 |
| - '[data-react-devtools-portal-root]', |
72 |
| - ); |
| 76 | + useLayoutEffect(() => { |
| 77 | + const menu = ((ref.current: any): HTMLElement); |
73 | 78 |
|
74 |
| - if (containerRef.current == null) { |
75 |
| - console.warn( |
76 |
| - 'DevTools tooltip root node not found; context menus will be disabled.', |
77 |
| - ); |
| 79 | + function hideUnlessContains(event: Event) { |
| 80 | + if (!menu.contains(((event.target: any): Node))) { |
| 81 | + hide(); |
78 | 82 | }
|
79 | 83 | }
|
80 |
| - }, []); |
81 | 84 |
|
82 |
| - useEffect(() => { |
83 |
| - const showMenuFn = ({ |
84 |
| - data, |
85 |
| - pageX, |
86 |
| - pageY, |
87 |
| - }: { |
88 |
| - data: any, |
89 |
| - pageX: number, |
90 |
| - pageY: number, |
91 |
| - }) => { |
92 |
| - setState({data, isVisible: true, pageX, pageY}); |
93 |
| - }; |
94 |
| - const hideMenuFn = () => setState(HIDDEN_STATE); |
95 |
| - return registerMenu(id, showMenuFn, hideMenuFn); |
96 |
| - }, [id]); |
| 85 | + ownerDocument.addEventListener('mousedown', hideUnlessContains); |
| 86 | + ownerDocument.addEventListener('touchstart', hideUnlessContains); |
| 87 | + ownerDocument.addEventListener('keydown', hideUnlessContains); |
97 | 88 |
|
98 |
| - useLayoutEffect(() => { |
99 |
| - if (!state.isVisible) { |
100 |
| - return; |
101 |
| - } |
| 89 | + const ownerWindow = ownerDocument.defaultView; |
| 90 | + ownerWindow.addEventListener('resize', hide); |
102 | 91 |
|
103 |
| - const menu = ((menuRef.current: any): HTMLElement); |
104 |
| - const container = containerRef.current; |
105 |
| - if (container !== null) { |
106 |
| - // $FlowFixMe[missing-local-annot] |
107 |
| - const hideUnlessContains = event => { |
108 |
| - if (!menu.contains(event.target)) { |
109 |
| - hideMenu(); |
110 |
| - } |
111 |
| - }; |
112 |
| - |
113 |
| - const ownerDocument = container.ownerDocument; |
114 |
| - ownerDocument.addEventListener('mousedown', hideUnlessContains); |
115 |
| - ownerDocument.addEventListener('touchstart', hideUnlessContains); |
116 |
| - ownerDocument.addEventListener('keydown', hideUnlessContains); |
117 |
| - |
118 |
| - const ownerWindow = ownerDocument.defaultView; |
119 |
| - ownerWindow.addEventListener('resize', hideMenu); |
120 |
| - |
121 |
| - repositionToFit(menu, state.pageX, state.pageY); |
122 |
| - |
123 |
| - return () => { |
124 |
| - ownerDocument.removeEventListener('mousedown', hideUnlessContains); |
125 |
| - ownerDocument.removeEventListener('touchstart', hideUnlessContains); |
126 |
| - ownerDocument.removeEventListener('keydown', hideUnlessContains); |
127 |
| - |
128 |
| - ownerWindow.removeEventListener('resize', hideMenu); |
129 |
| - }; |
130 |
| - } |
131 |
| - }, [state]); |
| 92 | + repositionToFit(menu, position.x, position.y); |
132 | 93 |
|
133 |
| - if (!state.isVisible) { |
134 |
| - return <div ref={bodyAccessorRef} />; |
135 |
| - } else { |
136 |
| - const container = containerRef.current; |
137 |
| - if (container !== null) { |
138 |
| - return createPortal( |
139 |
| - <div ref={menuRef} className={styles.ContextMenu}> |
140 |
| - {children(state.data)} |
141 |
| - </div>, |
142 |
| - container, |
143 |
| - ); |
144 |
| - } else { |
145 |
| - return null; |
146 |
| - } |
| 94 | + return () => { |
| 95 | + ownerDocument.removeEventListener('mousedown', hideUnlessContains); |
| 96 | + ownerDocument.removeEventListener('touchstart', hideUnlessContains); |
| 97 | + ownerDocument.removeEventListener('keydown', hideUnlessContains); |
| 98 | + |
| 99 | + ownerWindow.removeEventListener('resize', hide); |
| 100 | + }; |
| 101 | + }, []); |
| 102 | + |
| 103 | + if (portalContainer == null || items.length === 0) { |
| 104 | + return null; |
147 | 105 | }
|
| 106 | + |
| 107 | + return createPortal( |
| 108 | + <div className={styles.ContextMenu} ref={ref}> |
| 109 | + {items.map(({onClick, content}, index) => ( |
| 110 | + <ContextMenuItem key={index} onClick={onClick} hide={hide}> |
| 111 | + {content} |
| 112 | + </ContextMenuItem> |
| 113 | + ))} |
| 114 | + </div>, |
| 115 | + portalContainer, |
| 116 | + ); |
148 | 117 | }
|
0 commit comments