Skip to content

Commit ebc6e1c

Browse files
committed
Update ContextMenu.react.js
1 parent ab98f4c commit ebc6e1c

File tree

1 file changed

+78
-98
lines changed

1 file changed

+78
-98
lines changed

src/components/ContextMenu/ContextMenu.react.js

Lines changed: 78 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -5,115 +5,103 @@
55
* This source code is licensed under the license found in the LICENSE file in
66
* the root directory of this source tree.
77
*/
8+
89
import PropTypes from 'lib/PropTypes';
910
import React, { useState, useEffect, useRef } from 'react';
1011
import styles from 'components/ContextMenu/ContextMenu.scss';
1112

12-
const getPositionToFitVisibleScreen = (ref, offset = 0, mainItemCount = 0, subItemCount = 0) => {
13-
if (ref.current) {
14-
const elBox = ref.current.getBoundingClientRect();
15-
let y = 0;
16-
17-
const footerHeight = 50;
18-
const lowerLimit = window.innerHeight - footerHeight;
19-
const upperLimit = 0;
20-
21-
if (elBox.bottom > lowerLimit) {
22-
y = lowerLimit - elBox.bottom;
23-
} else if (elBox.top < upperLimit) {
24-
y = upperLimit - elBox.top;
25-
}
26-
27-
const projectedTop = elBox.top + y + offset;
28-
const projectedBottom = projectedTop + elBox.height;
13+
const getPositionToFitVisibleScreen = (
14+
ref,
15+
offset = 0,
16+
mainItemCount = 0,
17+
subItemCount = 0
18+
) => {
19+
if (!ref.current) return;
2920

30-
const shouldApplyOffset = mainItemCount === 0 || subItemCount > mainItemCount;
31-
if (shouldApplyOffset && projectedTop >= upperLimit && projectedBottom <= lowerLimit) {
32-
y += offset;
33-
}
21+
const elBox = ref.current.getBoundingClientRect();
22+
const menuHeight = elBox.height;
23+
const footerHeight = 50;
24+
const lowerLimit = window.innerHeight - footerHeight;
25+
const upperLimit = 0;
3426

35-
const prevEl = ref.current.previousSibling;
36-
if (prevEl) {
37-
const prevElBox = prevEl.getBoundingClientRect();
38-
const prevElStyle = window.getComputedStyle(prevEl);
39-
const rawTop = prevElStyle.top;
27+
const shouldApplyOffset = mainItemCount === 0 || subItemCount > mainItemCount;
28+
const prevEl = ref.current.previousSibling;
4029

41-
const parsedTop = parseInt(rawTop, 10);
42-
const prevElTop = Number.isFinite(parsedTop) ? parsedTop : prevElBox.top;
30+
if (prevEl) {
31+
const prevElBox = prevEl.getBoundingClientRect();
32+
const showOnRight = prevElBox.x + prevElBox.width + elBox.width < window.innerWidth;
4333

44-
if (!shouldApplyOffset) {
45-
y = prevElTop + offset;
46-
}
34+
let proposedTop = shouldApplyOffset
35+
? prevElBox.top + offset
36+
: prevElBox.top;
4737

48-
const showOnRight = prevElBox.x + prevElBox.width + elBox.width < window.innerWidth;
49-
return {
50-
x: showOnRight ? prevElBox.width : -elBox.width,
51-
y
52-
};
53-
}
38+
proposedTop = Math.max(upperLimit, Math.min(proposedTop, lowerLimit - menuHeight));
5439

55-
return { x: 0, y };
40+
return {
41+
x: showOnRight ? prevElBox.width : -elBox.width,
42+
y: proposedTop - elBox.top,
43+
};
5644
}
45+
46+
const proposedTop = elBox.top + offset;
47+
const clampedTop = Math.max(upperLimit, Math.min(proposedTop, lowerLimit - menuHeight));
48+
return {
49+
x: 0,
50+
y: clampedTop - elBox.top,
51+
};
5752
};
5853

5954
const MenuSection = ({ level, items, path, setPath, hide, parentItemCount = 0 }) => {
6055
const sectionRef = useRef(null);
61-
const [position, setPosition] = useState();
56+
const [position, setPosition] = useState(null);
57+
const hasPositioned = useRef(false);
6258

6359
useEffect(() => {
64-
const newPosition = getPositionToFitVisibleScreen(
65-
sectionRef,
66-
path[level] * 30,
67-
parentItemCount,
68-
items.length
69-
);
70-
newPosition && setPosition(newPosition);
71-
}, [sectionRef, path, level, items.length, parentItemCount]);
60+
if (!hasPositioned.current) {
61+
const newPosition = getPositionToFitVisibleScreen(
62+
sectionRef,
63+
path[level] * 30,
64+
parentItemCount,
65+
items.length
66+
);
67+
if (newPosition) {
68+
setPosition(newPosition);
69+
hasPositioned.current = true;
70+
}
71+
}
72+
}, []);
7273

7374
const style = position
7475
? {
75-
left: position.x,
76-
top: position.y,
77-
maxHeight: '80vh',
78-
overflowY: 'scroll',
79-
opacity: 1,
80-
}
76+
transform: `translate(${position.x}px, ${position.y}px)`,
77+
maxHeight: '80vh',
78+
overflowY: 'auto',
79+
opacity: 1,
80+
position: 'absolute',
81+
}
8182
: {};
8283

8384
return (
8485
<ul ref={sectionRef} className={styles.category} style={style}>
8586
{items.map((item, index) => {
86-
if (item.items) {
87-
return (
88-
<li
89-
key={`menu-section-${level}-${index}`}
90-
className={styles.item}
91-
onMouseEnter={() => {
92-
const newPath = path.slice(0, level + 1);
93-
newPath.push(index);
94-
setPath(newPath);
95-
}}
96-
>
97-
{item.text}
98-
</li>
99-
);
100-
}
87+
const handleHover = () => {
88+
const newPath = path.slice(0, level + 1);
89+
newPath.push(index);
90+
setPath(newPath);
91+
};
92+
10193
return (
10294
<li
10395
key={`menu-section-${level}-${index}`}
104-
className={styles.option}
96+
className={item.items ? styles.item : styles.option}
10597
style={item.disabled ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
10698
onClick={() => {
107-
if (item.disabled === true) {
108-
return;
99+
if (!item.disabled) {
100+
item.callback?.();
101+
hide();
109102
}
110-
item.callback && item.callback();
111-
hide();
112-
}}
113-
onMouseEnter={() => {
114-
const newPath = path.slice(0, level + 1);
115-
setPath(newPath);
116103
}}
104+
onMouseEnter={handleHover}
117105
>
118106
{item.text}
119107
{item.subtext && <span> - {item.subtext}</span>}
@@ -138,27 +126,24 @@ const ContextMenu = ({ x, y, items }) => {
138126
setPath([0]);
139127
};
140128

141-
function handleClickOutside(event) {
142-
if (menuRef.current && !menuRef.current.contains(event.target)) {
143-
hide();
144-
}
145-
}
146-
147129
useEffect(() => {
130+
const handleClickOutside = event => {
131+
if (menuRef.current && !menuRef.current.contains(event.target)) {
132+
hide();
133+
}
134+
};
148135
document.addEventListener('mousedown', handleClickOutside);
149136
return () => {
150137
document.removeEventListener('mousedown', handleClickOutside);
151138
};
152-
});
139+
}, []);
153140

154-
if (!visible) {
155-
return null;
156-
}
141+
if (!visible) return null;
157142

158143
const getItemsFromLevel = level => {
159144
let result = items;
160-
for (let index = 1; index <= level; index++) {
161-
result = result[path[index]].items;
145+
for (let i = 1; i <= level; i++) {
146+
result = result[path[i]]?.items || [];
162147
}
163148
return result;
164149
};
@@ -167,19 +152,16 @@ const ContextMenu = ({ x, y, items }) => {
167152
<div
168153
className={styles.menu}
169154
ref={menuRef}
170-
style={{
171-
left: x,
172-
top: y,
173-
}}
155+
style={{ left: x, top: y, position: 'absolute' }}
174156
>
175-
{path.map((position, level) => {
157+
{path.map((_, level) => {
176158
const itemsForLevel = getItemsFromLevel(level);
177159
const parentItemCount =
178160
level === 0 ? items.length : getItemsFromLevel(level - 1).length;
179161

180162
return (
181163
<MenuSection
182-
key={`section-${position}-${level}`}
164+
key={`section-${path[level]}-${level}`}
183165
path={path}
184166
setPath={setPath}
185167
level={level}
@@ -196,9 +178,7 @@ const ContextMenu = ({ x, y, items }) => {
196178
ContextMenu.propTypes = {
197179
x: PropTypes.number.isRequired.describe('X context menu position.'),
198180
y: PropTypes.number.isRequired.describe('Y context menu position.'),
199-
items: PropTypes.array.isRequired.describe(
200-
'Array with tree representation of context menu items.'
201-
),
181+
items: PropTypes.array.isRequired.describe('Array with tree representation of context menu items.'),
202182
};
203183

204184
export default ContextMenu;

0 commit comments

Comments
 (0)