From 53f4b8ced3d6cb0193f4c42fcbc7dc28fc5a49e6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 7 Apr 2025 22:05:31 +1000 Subject: [PATCH 1/8] Update ContextMenu.react.js --- .../ContextMenu/ContextMenu.react.js | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.react.js b/src/components/ContextMenu/ContextMenu.react.js index fdd309c6e4..0b36a7c1c5 100644 --- a/src/components/ContextMenu/ContextMenu.react.js +++ b/src/components/ContextMenu/ContextMenu.react.js @@ -9,20 +9,36 @@ 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) => { 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; + } + + // Apply offset only if it doesn't push the element offscreen again + const projectedTop = elBox.top + y + offset; + const projectedBottom = projectedTop + elBox.height; + + if (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 showOnRight = prevElBox.x + prevElBox.width + elBox.width < window.innerWidth; return { x: showOnRight ? prevElBox.width : -elBox.width, - y, + y }; } @@ -35,14 +51,14 @@ const MenuSection = ({ level, items, path, setPath, hide }) => { const [position, setPosition] = useState(); useEffect(() => { - const newPosition = getPositionToFitVisibleScreen(sectionRef); + const newPosition = getPositionToFitVisibleScreen(sectionRef, path[level] * 30); newPosition && setPosition(newPosition); }, [sectionRef]); const style = position ? { left: position.x, - top: position.y + path[level] * 30, + top: position.y, maxHeight: '80vh', overflowY: 'scroll', opacity: 1, From 18d5c975077fe891e3962e710d79193baf74d71c Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 9 Apr 2025 20:50:38 +1000 Subject: [PATCH 2/8] Update ContextMenu.react.js --- .../ContextMenu/ContextMenu.react.js | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.react.js b/src/components/ContextMenu/ContextMenu.react.js index 0b36a7c1c5..b5f03cfa36 100644 --- a/src/components/ContextMenu/ContextMenu.react.js +++ b/src/components/ContextMenu/ContextMenu.react.js @@ -9,7 +9,7 @@ import PropTypes from 'lib/PropTypes'; import React, { useState, useEffect, useRef } from 'react'; import styles from 'components/ContextMenu/ContextMenu.scss'; -const getPositionToFitVisibleScreen = (ref, offset = 0) => { +const getPositionToFitVisibleScreen = (ref, offset = 0, mainItemCount = 0, subItemCount = 0) => { if (ref.current) { const elBox = ref.current.getBoundingClientRect(); let y = 0; @@ -24,17 +24,24 @@ const getPositionToFitVisibleScreen = (ref, offset = 0) => { y = upperLimit - elBox.top; } - // Apply offset only if it doesn't push the element offscreen again const projectedTop = elBox.top + y + offset; const projectedBottom = projectedTop + elBox.height; - if (projectedTop >= upperLimit && projectedBottom <= lowerLimit) { + const shouldApplyOffset = subItemCount > mainItemCount; + if (shouldApplyOffset && projectedTop >= upperLimit && projectedBottom <= lowerLimit) { y += offset; } 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; + } + const showOnRight = prevElBox.x + prevElBox.width + elBox.width < window.innerWidth; return { x: showOnRight ? prevElBox.width : -elBox.width, @@ -46,23 +53,28 @@ const getPositionToFitVisibleScreen = (ref, offset = 0) => { } }; -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, path[level] * 30); + const newPosition = getPositionToFitVisibleScreen( + sectionRef, + path[level] * 30, + parentItemCount, + items.length + ); newPosition && setPosition(newPosition); }, [sectionRef]); const style = position ? { - left: position.x, - top: position.y, - maxHeight: '80vh', - overflowY: 'scroll', - opacity: 1, - } + left: position.x, + top: position.y, + maxHeight: '80vh', + overflowY: 'scroll', + opacity: 1, + } : {}; return ( @@ -108,6 +120,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]); @@ -117,10 +131,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(); @@ -134,8 +144,6 @@ const ContextMenu = ({ x, y, items }) => { }; }); - //#endregion - if (!visible) { return null; } @@ -158,14 +166,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 ( ); })} From a66522faefc1dd1c36fa8a2ad2cf55a5e5f6e78a Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 10 Apr 2025 02:09:58 +0100 Subject: [PATCH 3/8] lint --- src/components/ContextMenu/ContextMenu.react.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.react.js b/src/components/ContextMenu/ContextMenu.react.js index b5f03cfa36..9f26d1b8d8 100644 --- a/src/components/ContextMenu/ContextMenu.react.js +++ b/src/components/ContextMenu/ContextMenu.react.js @@ -69,12 +69,12 @@ const MenuSection = ({ level, items, path, setPath, hide, parentItemCount = 0 }) const style = position ? { - left: position.x, - top: position.y, - maxHeight: '80vh', - overflowY: 'scroll', - opacity: 1, - } + left: position.x, + top: position.y, + maxHeight: '80vh', + overflowY: 'scroll', + opacity: 1, + } : {}; return ( From e220b16558ad9aea30c7b793ddda7f10edf31a2b Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 25 Apr 2025 21:05:52 +1000 Subject: [PATCH 4/8] Update ContextMenu.react.js --- src/components/ContextMenu/ContextMenu.react.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/ContextMenu/ContextMenu.react.js b/src/components/ContextMenu/ContextMenu.react.js index 9f26d1b8d8..4099159be7 100644 --- a/src/components/ContextMenu/ContextMenu.react.js +++ b/src/components/ContextMenu/ContextMenu.react.js @@ -107,6 +107,10 @@ const MenuSection = ({ level, items, path, setPath, hide, parentItemCount = 0 }) item.callback && item.callback(); hide(); }} + onMouseEnter={() => { + const newPath = path.slice(0, level + 1); + setPath(newPath); + }} > {item.text} {item.subtext && - {item.subtext}} From 40cde809cf68cd3d5cfe8c8e456f02d3f37b995c Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 18 May 2025 19:50:20 +1000 Subject: [PATCH 5/8] Update ContextMenu.react.js --- src/components/ContextMenu/ContextMenu.react.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.react.js b/src/components/ContextMenu/ContextMenu.react.js index 4099159be7..754004dafe 100644 --- a/src/components/ContextMenu/ContextMenu.react.js +++ b/src/components/ContextMenu/ContextMenu.react.js @@ -27,7 +27,7 @@ const getPositionToFitVisibleScreen = (ref, offset = 0, mainItemCount = 0, subIt const projectedTop = elBox.top + y + offset; const projectedBottom = projectedTop + elBox.height; - const shouldApplyOffset = subItemCount > mainItemCount; + const shouldApplyOffset = mainItemCount === 0 || subItemCount > mainItemCount; if (shouldApplyOffset && projectedTop >= upperLimit && projectedBottom <= lowerLimit) { y += offset; } @@ -36,7 +36,10 @@ const getPositionToFitVisibleScreen = (ref, offset = 0, mainItemCount = 0, subIt if (prevEl) { const prevElBox = prevEl.getBoundingClientRect(); const prevElStyle = window.getComputedStyle(prevEl); - const prevElTop = parseInt(prevElStyle.top, 10); + const rawTop = prevElStyle.top; + + const parsedTop = parseInt(rawTop, 10); + const prevElTop = Number.isFinite(parsedTop) ? parsedTop : prevElBox.top; if (!shouldApplyOffset) { y = prevElTop + offset; @@ -65,7 +68,7 @@ const MenuSection = ({ level, items, path, setPath, hide, parentItemCount = 0 }) items.length ); newPosition && setPosition(newPosition); - }, [sectionRef]); + }, [sectionRef, path, level, items.length, parentItemCount]); const style = position ? { From ebc6e1c8b1986746582388fa7ad255a56960269c Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 22 May 2025 21:26:04 +1000 Subject: [PATCH 6/8] Update ContextMenu.react.js --- .../ContextMenu/ContextMenu.react.js | 176 ++++++++---------- 1 file changed, 78 insertions(+), 98 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.react.js b/src/components/ContextMenu/ContextMenu.react.js index 754004dafe..00037f95cd 100644 --- a/src/components/ContextMenu/ContextMenu.react.js +++ b/src/components/ContextMenu/ContextMenu.react.js @@ -5,115 +5,103 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ + import PropTypes from 'lib/PropTypes'; import React, { useState, useEffect, useRef } from 'react'; import styles from 'components/ContextMenu/ContextMenu.scss'; -const getPositionToFitVisibleScreen = (ref, offset = 0, mainItemCount = 0, subItemCount = 0) => { - if (ref.current) { - const elBox = ref.current.getBoundingClientRect(); - 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 getPositionToFitVisibleScreen = ( + ref, + offset = 0, + mainItemCount = 0, + subItemCount = 0 +) => { + if (!ref.current) return; - const shouldApplyOffset = mainItemCount === 0 || subItemCount > mainItemCount; - if (shouldApplyOffset && projectedTop >= upperLimit && projectedBottom <= lowerLimit) { - y += offset; - } + const elBox = ref.current.getBoundingClientRect(); + const menuHeight = elBox.height; + const footerHeight = 50; + const lowerLimit = window.innerHeight - footerHeight; + const upperLimit = 0; - const prevEl = ref.current.previousSibling; - if (prevEl) { - const prevElBox = prevEl.getBoundingClientRect(); - const prevElStyle = window.getComputedStyle(prevEl); - const rawTop = prevElStyle.top; + const shouldApplyOffset = mainItemCount === 0 || subItemCount > mainItemCount; + const prevEl = ref.current.previousSibling; - const parsedTop = parseInt(rawTop, 10); - const prevElTop = Number.isFinite(parsedTop) ? parsedTop : prevElBox.top; + if (prevEl) { + const prevElBox = prevEl.getBoundingClientRect(); + const showOnRight = prevElBox.x + prevElBox.width + elBox.width < window.innerWidth; - if (!shouldApplyOffset) { - y = prevElTop + offset; - } + let proposedTop = shouldApplyOffset + ? prevElBox.top + offset + : prevElBox.top; - const showOnRight = prevElBox.x + prevElBox.width + elBox.width < window.innerWidth; - return { - x: showOnRight ? prevElBox.width : -elBox.width, - y - }; - } + proposedTop = Math.max(upperLimit, Math.min(proposedTop, lowerLimit - menuHeight)); - return { x: 0, y }; + return { + x: showOnRight ? prevElBox.width : -elBox.width, + y: proposedTop - elBox.top, + }; } + + const proposedTop = elBox.top + offset; + const clampedTop = Math.max(upperLimit, Math.min(proposedTop, lowerLimit - menuHeight)); + return { + x: 0, + y: clampedTop - elBox.top, + }; }; const MenuSection = ({ level, items, path, setPath, hide, parentItemCount = 0 }) => { const sectionRef = useRef(null); - const [position, setPosition] = useState(); + const [position, setPosition] = useState(null); + const hasPositioned = useRef(false); useEffect(() => { - const newPosition = getPositionToFitVisibleScreen( - sectionRef, - path[level] * 30, - parentItemCount, - items.length - ); - newPosition && setPosition(newPosition); - }, [sectionRef, path, level, items.length, parentItemCount]); + if (!hasPositioned.current) { + const newPosition = getPositionToFitVisibleScreen( + sectionRef, + path[level] * 30, + parentItemCount, + items.length + ); + if (newPosition) { + setPosition(newPosition); + hasPositioned.current = true; + } + } + }, []); const style = position ? { - left: position.x, - top: position.y, - maxHeight: '80vh', - overflowY: 'scroll', - opacity: 1, - } + transform: `translate(${position.x}px, ${position.y}px)`, + maxHeight: '80vh', + overflowY: 'auto', + opacity: 1, + position: 'absolute', + } : {}; return (
    {items.map((item, index) => { - if (item.items) { - return ( -
  • { - const newPath = path.slice(0, level + 1); - newPath.push(index); - setPath(newPath); - }} - > - {item.text} -
  • - ); - } + const handleHover = () => { + const newPath = path.slice(0, level + 1); + newPath.push(index); + setPath(newPath); + }; + return (
  • { - if (item.disabled === true) { - return; + if (!item.disabled) { + item.callback?.(); + hide(); } - item.callback && item.callback(); - hide(); - }} - onMouseEnter={() => { - const newPath = path.slice(0, level + 1); - setPath(newPath); }} + onMouseEnter={handleHover} > {item.text} {item.subtext && - {item.subtext}} @@ -138,27 +126,24 @@ const ContextMenu = ({ x, y, items }) => { setPath([0]); }; - function handleClickOutside(event) { - if (menuRef.current && !menuRef.current.contains(event.target)) { - hide(); - } - } - useEffect(() => { + const handleClickOutside = event => { + if (menuRef.current && !menuRef.current.contains(event.target)) { + hide(); + } + }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; - }); + }, []); - if (!visible) { - return null; - } + if (!visible) return null; const getItemsFromLevel = level => { let result = items; - for (let index = 1; index <= level; index++) { - result = result[path[index]].items; + for (let i = 1; i <= level; i++) { + result = result[path[i]]?.items || []; } return result; }; @@ -167,19 +152,16 @@ const ContextMenu = ({ x, y, items }) => {
    - {path.map((position, level) => { + {path.map((_, level) => { const itemsForLevel = getItemsFromLevel(level); const parentItemCount = level === 0 ? items.length : getItemsFromLevel(level - 1).length; return ( { ContextMenu.propTypes = { x: PropTypes.number.isRequired.describe('X context menu position.'), y: PropTypes.number.isRequired.describe('Y context menu position.'), - items: PropTypes.array.isRequired.describe( - 'Array with tree representation of context menu items.' - ), + items: PropTypes.array.isRequired.describe('Array with tree representation of context menu items.'), }; export default ContextMenu; From 9f47a5454238b32b26875eaf5e6f8d9937b0dbce Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 24 May 2025 20:21:27 +0200 Subject: [PATCH 7/8] Update src/components/ContextMenu/ContextMenu.react.js Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- src/components/ContextMenu/ContextMenu.react.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/ContextMenu/ContextMenu.react.js b/src/components/ContextMenu/ContextMenu.react.js index 00037f95cd..2ca355a72c 100644 --- a/src/components/ContextMenu/ContextMenu.react.js +++ b/src/components/ContextMenu/ContextMenu.react.js @@ -16,7 +16,9 @@ const getPositionToFitVisibleScreen = ( mainItemCount = 0, subItemCount = 0 ) => { - if (!ref.current) return; + if (!ref.current) { + return; + } const elBox = ref.current.getBoundingClientRect(); const menuHeight = elBox.height; From 7bbecde3cf9dd62b7bd0f8d6712097608a937f63 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 24 May 2025 20:28:27 +0200 Subject: [PATCH 8/8] lint fix --- .../ContextMenu/ContextMenu.react.js | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/ContextMenu/ContextMenu.react.js b/src/components/ContextMenu/ContextMenu.react.js index 00037f95cd..278a8aecb7 100644 --- a/src/components/ContextMenu/ContextMenu.react.js +++ b/src/components/ContextMenu/ContextMenu.react.js @@ -16,7 +16,9 @@ const getPositionToFitVisibleScreen = ( mainItemCount = 0, subItemCount = 0 ) => { - if (!ref.current) return; + if (!ref.current) { + return; + } const elBox = ref.current.getBoundingClientRect(); const menuHeight = elBox.height; @@ -73,12 +75,12 @@ const MenuSection = ({ level, items, path, setPath, hide, parentItemCount = 0 }) const style = position ? { - transform: `translate(${position.x}px, ${position.y}px)`, - maxHeight: '80vh', - overflowY: 'auto', - opacity: 1, - position: 'absolute', - } + transform: `translate(${position.x}px, ${position.y}px)`, + maxHeight: '80vh', + overflowY: 'auto', + opacity: 1, + position: 'absolute', + } : {}; return ( @@ -138,7 +140,9 @@ const ContextMenu = ({ x, y, items }) => { }; }, []); - if (!visible) return null; + if (!visible) { + return null; + } const getItemsFromLevel = level => { let result = items;