From 5750d93edded8786b34031665106012c58de5499 Mon Sep 17 00:00:00 2001 From: Radexito Date: Mon, 3 Aug 2026 08:43:46 +0200 Subject: [PATCH] fix: flip context menu inside viewport when near bottom/right edge (#310) --- renderer/src/FileExplorerView.jsx | 26 ++++++++++++++++++++++++-- renderer/src/MusicLibrary.jsx | 27 +++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/renderer/src/FileExplorerView.jsx b/renderer/src/FileExplorerView.jsx index 8eb71fff..06a7a665 100644 --- a/renderer/src/FileExplorerView.jsx +++ b/renderer/src/FileExplorerView.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; +import { useState, useEffect, useLayoutEffect, useRef, useCallback, useMemo } from 'react'; import { List } from 'react-window'; import { usePlayer } from './PlayerContext.jsx'; import { artworkUrl } from './artworkUrl.js'; @@ -308,6 +308,10 @@ export default function FileExplorerView({ style }) { const [selectedPaths, setSelectedPaths] = useState(new Set()); const [playlists, setPlaylists] = useState([]); const [contextMenu, setContextMenu] = useState(null); + // Nudge applied after measuring the rendered menu so it never overflows the + // viewport bottom/right edge (#310). + const menuRef = useRef(null); + const [menuShift, setMenuShift] = useState({ x: 0, y: 0 }); const [detailsTrack, setDetailsTrack] = useState(null); const [beatGridTrack, setBeatGridTrack] = useState(null); const [toast, setToast] = useState(null); @@ -692,6 +696,23 @@ export default function FileExplorerView({ style }) { [selectedPaths, displayItems] ); + // Shift the menu up/left by exactly the overflow once it renders, so it + // always stays inside the window bounds (#310). + useLayoutEffect(() => { + if (!contextMenu) { + setMenuShift({ x: 0, y: 0 }); + return; + } + const el = menuRef.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + const shiftX = Math.max(0, rect.right - window.innerWidth + 8); + const shiftY = Math.max(0, rect.bottom - window.innerHeight + 8); + setMenuShift((prev) => + prev.x === -shiftX && prev.y === -shiftY ? prev : { x: -shiftX, y: -shiftY } + ); + }, [contextMenu]); + const closeMenu = useCallback(() => setContextMenu(null), []); // ── Details save ────────────────────────────────────────────────────────── @@ -944,7 +965,8 @@ export default function FileExplorerView({ style }) {
e.stopPropagation()} > {menuIsDir ? ( diff --git a/renderer/src/MusicLibrary.jsx b/renderer/src/MusicLibrary.jsx index 0eacb765..0e8276ac 100644 --- a/renderer/src/MusicLibrary.jsx +++ b/renderer/src/MusicLibrary.jsx @@ -1,5 +1,6 @@ import { useEffect, + useLayoutEffect, useState, useRef, useCallback, @@ -560,6 +561,10 @@ function MusicLibrary({ selectedPlaylist, search, onSearchChange }) { const [selectedIds, setSelectedIds] = useState(new Set()); const [contextMenu, setContextMenu] = useState(null); // { x, y, targetIds } + // Nudge applied after measuring the rendered menu so it never overflows the + // viewport bottom/right edge (#310). + const menuRef = useRef(null); + const [menuShift, setMenuShift] = useState({ x: 0, y: 0 }); const [toast, setToast] = useState(null); // { msg, ok } | null const toastTimerRef = useRef(null); const [drillStack, setDrillStack] = useState([]); // overlay drill-down stack [{ id, label, content }] @@ -1112,6 +1117,23 @@ function MusicLibrary({ selectedPlaylist, search, onSearchChange }) { [selectedIds] ); + // Re-measure once the menu (or its drill/submenu content) renders: shift the + // whole menu up/left by exactly the overflow so it stays inside the window. + useLayoutEffect(() => { + if (!contextMenu) { + setMenuShift({ x: 0, y: 0 }); + return; + } + const el = menuRef.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + const shiftX = Math.max(0, rect.right - window.innerWidth + 8); + const shiftY = Math.max(0, rect.bottom - window.innerHeight + 8); + setMenuShift((prev) => + prev.x === -shiftX && prev.y === -shiftY ? prev : { x: -shiftX, y: -shiftY } + ); + }, [contextMenu, drillStack, playlistSubmenu]); + const handleReanalyze = useCallback(async () => { const targetIds = contextMenu?.targetIds ?? []; setContextMenu(null); @@ -1646,12 +1668,13 @@ function MusicLibrary({ selectedPlaylist, search, onSearchChange }) { ] .filter(Boolean) .join(' ')} + ref={menuRef} style={ contextMenu.overlayMode ? undefined : { - top: contextMenu.y, - left: contextMenu.x, + top: contextMenu.y + menuShift.y, + left: contextMenu.x + menuShift.x, '--submenu-max-h': `${contextMenu.submenuMaxH}px`, } }