Skip to content

Commit 3fed292

Browse files
authored
fix: Improperly aligned unfolding sub-items in context menu in data browser (#2726)
1 parent 3eedbdb commit 3fed292

File tree

1 file changed

+90
-70
lines changed

1 file changed

+90
-70
lines changed

src/components/ContextMenu/ContextMenu.react.js

Lines changed: 90 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -5,80 +5,105 @@
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 => {
13-
if (ref.current) {
14-
const elBox = ref.current.getBoundingClientRect();
15-
const y = elBox.y + elBox.height < window.innerHeight ? 0 : 0 - elBox.y + 100;
16-
17-
// If there's a previous element show current next to it.
18-
// Try on right side first, then on left if there's no place.
19-
const prevEl = ref.current.previousSibling;
20-
if (prevEl) {
21-
const prevElBox = prevEl.getBoundingClientRect();
22-
const showOnRight = prevElBox.x + prevElBox.width + elBox.width < window.innerWidth;
23-
return {
24-
x: showOnRight ? prevElBox.width : -elBox.width,
25-
y,
26-
};
27-
}
13+
const getPositionToFitVisibleScreen = (
14+
ref,
15+
offset = 0,
16+
mainItemCount = 0,
17+
subItemCount = 0
18+
) => {
19+
if (!ref.current) {
20+
return;
21+
}
22+
23+
const elBox = ref.current.getBoundingClientRect();
24+
const menuHeight = elBox.height;
25+
const footerHeight = 50;
26+
const lowerLimit = window.innerHeight - footerHeight;
27+
const upperLimit = 0;
28+
29+
const shouldApplyOffset = mainItemCount === 0 || subItemCount > mainItemCount;
30+
const prevEl = ref.current.previousSibling;
31+
32+
if (prevEl) {
33+
const prevElBox = prevEl.getBoundingClientRect();
34+
const showOnRight = prevElBox.x + prevElBox.width + elBox.width < window.innerWidth;
2835

29-
return { x: 0, y };
36+
let proposedTop = shouldApplyOffset
37+
? prevElBox.top + offset
38+
: prevElBox.top;
39+
40+
proposedTop = Math.max(upperLimit, Math.min(proposedTop, lowerLimit - menuHeight));
41+
42+
return {
43+
x: showOnRight ? prevElBox.width : -elBox.width,
44+
y: proposedTop - elBox.top,
45+
};
3046
}
47+
48+
const proposedTop = elBox.top + offset;
49+
const clampedTop = Math.max(upperLimit, Math.min(proposedTop, lowerLimit - menuHeight));
50+
return {
51+
x: 0,
52+
y: clampedTop - elBox.top,
53+
};
3154
};
3255

33-
const MenuSection = ({ level, items, path, setPath, hide }) => {
56+
const MenuSection = ({ level, items, path, setPath, hide, parentItemCount = 0 }) => {
3457
const sectionRef = useRef(null);
35-
const [position, setPosition] = useState();
58+
const [position, setPosition] = useState(null);
59+
const hasPositioned = useRef(false);
3660

3761
useEffect(() => {
38-
const newPosition = getPositionToFitVisibleScreen(sectionRef);
39-
newPosition && setPosition(newPosition);
40-
}, [sectionRef]);
62+
if (!hasPositioned.current) {
63+
const newPosition = getPositionToFitVisibleScreen(
64+
sectionRef,
65+
path[level] * 30,
66+
parentItemCount,
67+
items.length
68+
);
69+
if (newPosition) {
70+
setPosition(newPosition);
71+
hasPositioned.current = true;
72+
}
73+
}
74+
}, []);
4175

4276
const style = position
4377
? {
44-
left: position.x,
45-
top: position.y + path[level] * 30,
78+
transform: `translate(${position.x}px, ${position.y}px)`,
4679
maxHeight: '80vh',
47-
overflowY: 'scroll',
80+
overflowY: 'auto',
4881
opacity: 1,
82+
position: 'absolute',
4983
}
5084
: {};
5185

5286
return (
5387
<ul ref={sectionRef} className={styles.category} style={style}>
5488
{items.map((item, index) => {
55-
if (item.items) {
56-
return (
57-
<li
58-
key={`menu-section-${level}-${index}`}
59-
className={styles.item}
60-
onMouseEnter={() => {
61-
const newPath = path.slice(0, level + 1);
62-
newPath.push(index);
63-
setPath(newPath);
64-
}}
65-
>
66-
{item.text}
67-
</li>
68-
);
69-
}
89+
const handleHover = () => {
90+
const newPath = path.slice(0, level + 1);
91+
newPath.push(index);
92+
setPath(newPath);
93+
};
94+
7095
return (
7196
<li
7297
key={`menu-section-${level}-${index}`}
73-
className={styles.option}
98+
className={item.items ? styles.item : styles.option}
7499
style={item.disabled ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
75100
onClick={() => {
76-
if (item.disabled === true) {
77-
return;
101+
if (!item.disabled) {
102+
item.callback?.();
103+
hide();
78104
}
79-
item.callback && item.callback();
80-
hide();
81105
}}
106+
onMouseEnter={handleHover}
82107
>
83108
{item.text}
84109
{item.subtext && <span> - {item.subtext}</span>}
@@ -92,6 +117,8 @@ const MenuSection = ({ level, items, path, setPath, hide }) => {
92117
const ContextMenu = ({ x, y, items }) => {
93118
const [path, setPath] = useState([0]);
94119
const [visible, setVisible] = useState(true);
120+
const menuRef = useRef(null);
121+
95122
useEffect(() => {
96123
setVisible(true);
97124
}, [items]);
@@ -101,33 +128,26 @@ const ContextMenu = ({ x, y, items }) => {
101128
setPath([0]);
102129
};
103130

104-
//#region Closing menu after clicking outside it
105-
106-
const menuRef = useRef(null);
107-
108-
function handleClickOutside(event) {
109-
if (menuRef.current && !menuRef.current.contains(event.target)) {
110-
hide();
111-
}
112-
}
113-
114131
useEffect(() => {
132+
const handleClickOutside = event => {
133+
if (menuRef.current && !menuRef.current.contains(event.target)) {
134+
hide();
135+
}
136+
};
115137
document.addEventListener('mousedown', handleClickOutside);
116138
return () => {
117139
document.removeEventListener('mousedown', handleClickOutside);
118140
};
119-
});
120-
121-
//#endregion
141+
}, []);
122142

123143
if (!visible) {
124144
return null;
125145
}
126146

127147
const getItemsFromLevel = level => {
128148
let result = items;
129-
for (let index = 1; index <= level; index++) {
130-
result = result[path[index]].items;
149+
for (let i = 1; i <= level; i++) {
150+
result = result[path[i]]?.items || [];
131151
}
132152
return result;
133153
};
@@ -136,20 +156,22 @@ const ContextMenu = ({ x, y, items }) => {
136156
<div
137157
className={styles.menu}
138158
ref={menuRef}
139-
style={{
140-
left: x,
141-
top: y,
142-
}}
159+
style={{ left: x, top: y, position: 'absolute' }}
143160
>
144-
{path.map((position, level) => {
161+
{path.map((_, level) => {
162+
const itemsForLevel = getItemsFromLevel(level);
163+
const parentItemCount =
164+
level === 0 ? items.length : getItemsFromLevel(level - 1).length;
165+
145166
return (
146167
<MenuSection
147-
key={`section-${position}-${level}`}
168+
key={`section-${path[level]}-${level}`}
148169
path={path}
149170
setPath={setPath}
150171
level={level}
151-
items={getItemsFromLevel(level)}
172+
items={itemsForLevel}
152173
hide={hide}
174+
parentItemCount={parentItemCount}
153175
/>
154176
);
155177
})}
@@ -160,9 +182,7 @@ const ContextMenu = ({ x, y, items }) => {
160182
ContextMenu.propTypes = {
161183
x: PropTypes.number.isRequired.describe('X context menu position.'),
162184
y: PropTypes.number.isRequired.describe('Y context menu position.'),
163-
items: PropTypes.array.isRequired.describe(
164-
'Array with tree representation of context menu items.'
165-
),
185+
items: PropTypes.array.isRequired.describe('Array with tree representation of context menu items.'),
166186
};
167187

168188
export default ContextMenu;

0 commit comments

Comments
 (0)