-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix: Improperly aligned unfolding sub-items in context menu in data browser #2726
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: alpha
Are you sure you want to change the base?
Changes from all commits
53f4b8c
10b2a5c
18d5c97
7e920cc
a66522f
848db0f
830d089
e220b16
3a901fe
f572a28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -9,40 +9,68 @@ import PropTypes from 'lib/PropTypes'; | |||||||||||||||||||||||||||||||||||||||
import React, { useState, useEffect, useRef } from 'react'; | ||||||||||||||||||||||||||||||||||||||||
import styles from 'components/ContextMenu/ContextMenu.scss'; | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
const getPositionToFitVisibleScreen = ref => { | ||||||||||||||||||||||||||||||||||||||||
const getPositionToFitVisibleScreen = (ref, offset = 0, mainItemCount = 0, subItemCount = 0) => { | ||||||||||||||||||||||||||||||||||||||||
if (ref.current) { | ||||||||||||||||||||||||||||||||||||||||
const elBox = ref.current.getBoundingClientRect(); | ||||||||||||||||||||||||||||||||||||||||
const y = elBox.y + elBox.height < window.innerHeight ? 0 : 0 - elBox.y + 100; | ||||||||||||||||||||||||||||||||||||||||
let y = 0; | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
const footerHeight = 50; | ||||||||||||||||||||||||||||||||||||||||
const lowerLimit = window.innerHeight - footerHeight; | ||||||||||||||||||||||||||||||||||||||||
const upperLimit = 0; | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
if (elBox.bottom > lowerLimit) { | ||||||||||||||||||||||||||||||||||||||||
y = lowerLimit - elBox.bottom; | ||||||||||||||||||||||||||||||||||||||||
} else if (elBox.top < upperLimit) { | ||||||||||||||||||||||||||||||||||||||||
y = upperLimit - elBox.top; | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
const projectedTop = elBox.top + y + offset; | ||||||||||||||||||||||||||||||||||||||||
const projectedBottom = projectedTop + elBox.height; | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
const shouldApplyOffset = subItemCount > mainItemCount; | ||||||||||||||||||||||||||||||||||||||||
if (shouldApplyOffset && projectedTop >= upperLimit && projectedBottom <= lowerLimit) { | ||||||||||||||||||||||||||||||||||||||||
y += offset; | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
// If there's a previous element show current next to it. | ||||||||||||||||||||||||||||||||||||||||
// Try on right side first, then on left if there's no place. | ||||||||||||||||||||||||||||||||||||||||
const prevEl = ref.current.previousSibling; | ||||||||||||||||||||||||||||||||||||||||
if (prevEl) { | ||||||||||||||||||||||||||||||||||||||||
const prevElBox = prevEl.getBoundingClientRect(); | ||||||||||||||||||||||||||||||||||||||||
const prevElStyle = window.getComputedStyle(prevEl); | ||||||||||||||||||||||||||||||||||||||||
const prevElTop = parseInt(prevElStyle.top, 10); | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
if (!shouldApplyOffset) { | ||||||||||||||||||||||||||||||||||||||||
y = prevElTop + offset; | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
Comment on lines
+38
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
- const prevElStyle = window.getComputedStyle(prevEl);
- const prevElTop = parseInt(prevElStyle.top, 10);
+ const prevElStyle = window.getComputedStyle(prevEl);
+ const rawTop = prevElStyle.top;
+ const prevElTop =
+ rawTop === 'auto' ? prevElBox.top : parseInt(rawTop, 10); Guarding this value makes the positioning routine robust regardless of the parent element’s CSS positioning context.
🤖 Prompt for AI Agents (early access)
|
||||||||||||||||||||||||||||||||||||||||
const showOnRight = prevElBox.x + prevElBox.width + elBox.width < window.innerWidth; | ||||||||||||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||||||||||||
x: showOnRight ? prevElBox.width : -elBox.width, | ||||||||||||||||||||||||||||||||||||||||
y, | ||||||||||||||||||||||||||||||||||||||||
y | ||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
return { x: 0, y }; | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
const MenuSection = ({ level, items, path, setPath, hide }) => { | ||||||||||||||||||||||||||||||||||||||||
const MenuSection = ({ level, items, path, setPath, hide, parentItemCount = 0 }) => { | ||||||||||||||||||||||||||||||||||||||||
const sectionRef = useRef(null); | ||||||||||||||||||||||||||||||||||||||||
const [position, setPosition] = useState(); | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||
const newPosition = getPositionToFitVisibleScreen(sectionRef); | ||||||||||||||||||||||||||||||||||||||||
const newPosition = getPositionToFitVisibleScreen( | ||||||||||||||||||||||||||||||||||||||||
sectionRef, | ||||||||||||||||||||||||||||||||||||||||
path[level] * 30, | ||||||||||||||||||||||||||||||||||||||||
parentItemCount, | ||||||||||||||||||||||||||||||||||||||||
items.length | ||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||
newPosition && setPosition(newPosition); | ||||||||||||||||||||||||||||||||||||||||
}, [sectionRef]); | ||||||||||||||||||||||||||||||||||||||||
Comment on lines
60
to
68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Missing dependencies in - }, [sectionRef]);
+ }, [sectionRef, path, level, items.length, parentItemCount]); Adding the full dependency list prevents subtle mis-alignments and silences the React-hooks ESLint rule. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents (early access)
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
const style = position | ||||||||||||||||||||||||||||||||||||||||
? { | ||||||||||||||||||||||||||||||||||||||||
left: position.x, | ||||||||||||||||||||||||||||||||||||||||
top: position.y + path[level] * 30, | ||||||||||||||||||||||||||||||||||||||||
top: position.y, | ||||||||||||||||||||||||||||||||||||||||
maxHeight: '80vh', | ||||||||||||||||||||||||||||||||||||||||
overflowY: 'scroll', | ||||||||||||||||||||||||||||||||||||||||
opacity: 1, | ||||||||||||||||||||||||||||||||||||||||
|
@@ -79,6 +107,10 @@ const MenuSection = ({ level, items, path, setPath, hide }) => { | |||||||||||||||||||||||||||||||||||||||
item.callback && item.callback(); | ||||||||||||||||||||||||||||||||||||||||
hide(); | ||||||||||||||||||||||||||||||||||||||||
}} | ||||||||||||||||||||||||||||||||||||||||
onMouseEnter={() => { | ||||||||||||||||||||||||||||||||||||||||
const newPath = path.slice(0, level + 1); | ||||||||||||||||||||||||||||||||||||||||
setPath(newPath); | ||||||||||||||||||||||||||||||||||||||||
}} | ||||||||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||||||||
{item.text} | ||||||||||||||||||||||||||||||||||||||||
{item.subtext && <span> - {item.subtext}</span>} | ||||||||||||||||||||||||||||||||||||||||
|
@@ -92,6 +124,8 @@ const MenuSection = ({ level, items, path, setPath, hide }) => { | |||||||||||||||||||||||||||||||||||||||
const ContextMenu = ({ x, y, items }) => { | ||||||||||||||||||||||||||||||||||||||||
const [path, setPath] = useState([0]); | ||||||||||||||||||||||||||||||||||||||||
const [visible, setVisible] = useState(true); | ||||||||||||||||||||||||||||||||||||||||
const menuRef = useRef(null); | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||
setVisible(true); | ||||||||||||||||||||||||||||||||||||||||
}, [items]); | ||||||||||||||||||||||||||||||||||||||||
|
@@ -101,10 +135,6 @@ const ContextMenu = ({ x, y, items }) => { | |||||||||||||||||||||||||||||||||||||||
setPath([0]); | ||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
//#region Closing menu after clicking outside it | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
const menuRef = useRef(null); | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
function handleClickOutside(event) { | ||||||||||||||||||||||||||||||||||||||||
if (menuRef.current && !menuRef.current.contains(event.target)) { | ||||||||||||||||||||||||||||||||||||||||
hide(); | ||||||||||||||||||||||||||||||||||||||||
|
@@ -118,8 +148,6 @@ const ContextMenu = ({ x, y, items }) => { | |||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
//#endregion | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
if (!visible) { | ||||||||||||||||||||||||||||||||||||||||
return null; | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
|
@@ -142,14 +170,19 @@ const ContextMenu = ({ x, y, items }) => { | |||||||||||||||||||||||||||||||||||||||
}} | ||||||||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||||||||
{path.map((position, level) => { | ||||||||||||||||||||||||||||||||||||||||
const itemsForLevel = getItemsFromLevel(level); | ||||||||||||||||||||||||||||||||||||||||
const parentItemCount = | ||||||||||||||||||||||||||||||||||||||||
level === 0 ? items.length : getItemsFromLevel(level - 1).length; | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||||||||
<MenuSection | ||||||||||||||||||||||||||||||||||||||||
key={`section-${position}-${level}`} | ||||||||||||||||||||||||||||||||||||||||
path={path} | ||||||||||||||||||||||||||||||||||||||||
setPath={setPath} | ||||||||||||||||||||||||||||||||||||||||
level={level} | ||||||||||||||||||||||||||||||||||||||||
items={getItemsFromLevel(level)} | ||||||||||||||||||||||||||||||||||||||||
items={itemsForLevel} | ||||||||||||||||||||||||||||||||||||||||
hide={hide} | ||||||||||||||||||||||||||||||||||||||||
parentItemCount={parentItemCount} | ||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||
})} | ||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Guard against NaN offset & out-of-viewport jumps
getPositionToFitVisibleScreen
nicely improves submenu placement, but two edge-cases can still break the computedy
:mainItemCount === 0
(top-level menu)shouldApplyOffset
is alwaystrue
, yetoffset
is often0
, so the earlyprojectedTop/Bottom
check may skip a necessary upward shift if the root element is already partially off-screen.parseInt
later in the function (see below) may yieldNaN
, propagating intoy
and producingstyle={{ top: NaN }}
which React silently drops, leaving the menu in an undefined position.Consider calculating a safe offset and using
Number.isFinite(...)
guards:This prevents a “0-offset no-op” and protects against accidental NaN propagation a few lines later.
🏁 Script executed:
Length of output: 351
🏁 Script executed:
Length of output: 288
Guard against NaN offsets and zero-offset no-ops
We should tighten
getPositionToFitVisibleScreen
to handle two remaining edge‐cases:When
mainItemCount === 0
,subItemCount > mainItemCount
is always true yetoffset
is often 0—so we end up skipping a needed reposition for a root menu already off-screen.parseInt
.If
window.getComputedStyle(...).top
is"auto"
(or any non-numeric),parseInt(...,10)
returnsNaN
, which propagates intoy
and yieldsstyle={{ top: NaN }}
.Apply these fixes:
offset === 0
won’t silently bypass needed shifts on root menusparseInt
in aNumber.isFinite
guard to avoidNaN
leaking intoy
and the renderedtop
Please integrate these changes to shore up both edge-cases.
📝 Committable suggestion
🤖 Prompt for AI Agents (early access)