feat: open three-dot menus upward when insufficient space below

This commit is contained in:
Gigi
2025-10-15 17:28:44 +02:00
parent ef848aa93e
commit 0ce64fe83f
2 changed files with 36 additions and 3 deletions

View File

@@ -100,6 +100,9 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
const [showArticleMenu, setShowArticleMenu] = useState(false) const [showArticleMenu, setShowArticleMenu] = useState(false)
const [showVideoMenu, setShowVideoMenu] = useState(false) const [showVideoMenu, setShowVideoMenu] = useState(false)
const [showExternalMenu, setShowExternalMenu] = useState(false) const [showExternalMenu, setShowExternalMenu] = useState(false)
const [articleMenuOpenUpward, setArticleMenuOpenUpward] = useState(false)
const [videoMenuOpenUpward, setVideoMenuOpenUpward] = useState(false)
const [externalMenuOpenUpward, setExternalMenuOpenUpward] = useState(false)
const articleMenuRef = useRef<HTMLDivElement>(null) const articleMenuRef = useRef<HTMLDivElement>(null)
const videoMenuRef = useRef<HTMLDivElement>(null) const videoMenuRef = useRef<HTMLDivElement>(null)
const externalMenuRef = useRef<HTMLDivElement>(null) const externalMenuRef = useRef<HTMLDivElement>(null)
@@ -161,6 +164,35 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
} }
}, [showArticleMenu, showVideoMenu, showExternalMenu]) }, [showArticleMenu, showVideoMenu, showExternalMenu])
// Check available space and position menu upward if needed
useEffect(() => {
const checkMenuPosition = (menuRef: React.RefObject<HTMLDivElement>, setOpenUpward: (value: boolean) => void) => {
if (!menuRef.current) return
const menuWrapper = menuRef.current
const menuElement = menuWrapper.querySelector('.article-menu') as HTMLElement
if (!menuElement) return
const rect = menuWrapper.getBoundingClientRect()
const viewportHeight = window.innerHeight
const spaceBelow = viewportHeight - rect.bottom
const menuHeight = menuElement.offsetHeight || 300 // estimate if not rendered yet
// Open upward if there's not enough space below (with 20px buffer)
setOpenUpward(spaceBelow < menuHeight + 20 && rect.top > menuHeight)
}
if (showArticleMenu) {
checkMenuPosition(articleMenuRef, setArticleMenuOpenUpward)
}
if (showVideoMenu) {
checkMenuPosition(videoMenuRef, setVideoMenuOpenUpward)
}
if (showExternalMenu) {
checkMenuPosition(externalMenuRef, setExternalMenuOpenUpward)
}
}, [showArticleMenu, showVideoMenu, showExternalMenu])
const readingStats = useMemo(() => { const readingStats = useMemo(() => {
const content = markdown || html || '' const content = markdown || html || ''
if (!content) return null if (!content) return null
@@ -588,7 +620,7 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
<FontAwesomeIcon icon={faEllipsisH} /> <FontAwesomeIcon icon={faEllipsisH} />
</button> </button>
{showVideoMenu && ( {showVideoMenu && (
<div className="article-menu"> <div className={`article-menu ${videoMenuOpenUpward ? 'open-upward' : ''}`}>
<button className="article-menu-item" onClick={handleOpenVideoExternal}> <button className="article-menu-item" onClick={handleOpenVideoExternal}>
<FontAwesomeIcon icon={faExternalLinkAlt} /> <FontAwesomeIcon icon={faExternalLinkAlt} />
<span>Open Link</span> <span>Open Link</span>
@@ -669,7 +701,7 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
</button> </button>
{showExternalMenu && ( {showExternalMenu && (
<div className="article-menu"> <div className={`article-menu ${externalMenuOpenUpward ? 'open-upward' : ''}`}>
<button <button
className="article-menu-item" className="article-menu-item"
onClick={handleShareExternalUrl} onClick={handleShareExternalUrl}
@@ -717,7 +749,7 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
</button> </button>
{showArticleMenu && ( {showArticleMenu && (
<div className="article-menu"> <div className={`article-menu ${articleMenuOpenUpward ? 'open-upward' : ''}`}>
<button <button
className="article-menu-item" className="article-menu-item"
onClick={handleShareBoris} onClick={handleShareBoris}

View File

@@ -192,6 +192,7 @@
.article-menu-btn { background: none; border: none; color: var(--color-text-secondary); cursor: pointer; padding: 0.5rem 0.75rem; font-size: 0.875rem; display: flex; align-items: center; gap: 0.5rem; transition: all 0.2s ease; border-radius: 6px; } .article-menu-btn { background: none; border: none; color: var(--color-text-secondary); cursor: pointer; padding: 0.5rem 0.75rem; font-size: 0.875rem; display: flex; align-items: center; gap: 0.5rem; transition: all 0.2s ease; border-radius: 6px; }
.article-menu-btn:hover { color: var(--color-primary); background: rgba(99, 102, 241, 0.1); } .article-menu-btn:hover { color: var(--color-primary); background: rgba(99, 102, 241, 0.1); }
.article-menu { position: absolute; right: 0; top: calc(100% + 4px); background: var(--color-bg-elevated); border: 1px solid var(--color-border-subtle); border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 1000; min-width: 180px; overflow: hidden; } .article-menu { position: absolute; right: 0; top: calc(100% + 4px); background: var(--color-bg-elevated); border: 1px solid var(--color-border-subtle); border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 1000; min-width: 180px; overflow: hidden; }
.article-menu.open-upward { top: auto; bottom: calc(100% + 4px); }
.article-menu-item { width: 100%; background: none; border: none; color: var(--color-text); padding: 0.75rem 1rem; font-size: 0.875rem; display: flex; align-items: center; gap: 0.75rem; cursor: pointer; transition: all 0.15s ease; text-align: left; white-space: nowrap; } .article-menu-item { width: 100%; background: none; border: none; color: var(--color-text); padding: 0.75rem 1rem; font-size: 0.875rem; display: flex; align-items: center; gap: 0.75rem; cursor: pointer; transition: all 0.15s ease; text-align: left; white-space: nowrap; }
.article-menu-item:hover { background: rgba(99, 102, 241, 0.15); color: var(--color-text); } .article-menu-item:hover { background: rgba(99, 102, 241, 0.15); color: var(--color-text); }
.article-menu-item svg { font-size: 0.875rem; flex-shrink: 0; } .article-menu-item svg { font-size: 0.875rem; flex-shrink: 0; }