From b941cc03ec9151c6dbbc30cc0651850b70ea13c3 Mon Sep 17 00:00:00 2001 From: Simon Cordt Date: Wed, 24 Sep 2025 23:11:38 +0200 Subject: [PATCH] Fix: Properly position and manage visibility of filters popover --- web/css/queue_style.css | 1 + web/pqueue.js | 86 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/web/css/queue_style.css b/web/css/queue_style.css index 56286ba..dfeb351 100644 --- a/web/css/queue_style.css +++ b/web/css/queue_style.css @@ -111,6 +111,7 @@ .pqueue-history-sentinel{display:flex;align-items:center;justify-content:center;min-height:48px;opacity:.7} .pqueue-popover{position:fixed;min-width:320px;max-width:420px;max-height:80vh;overflow-y:auto;overflow-x:hidden;padding:0;background:#1a1a1a;border:1px solid rgba(82,82,91,.22);border-radius:14px;box-shadow:0 10px 30px rgba(0,0,0,.35);z-index:100000} .pqueue-popover__arrow{position:absolute;top:-8px;left:24px;width:16px;height:16px;background:#1a1a1a;border-left:1px solid rgba(82,82,91,.22);border-top:1px solid rgba(82,82,91,.22);transform:rotate(45deg);border-top-left-radius:3px} +.pqueue-popover--above .pqueue-popover__arrow{top:auto;bottom:-8px;transform:rotate(45deg);background:#1a1a1a;border-left:none;border-top:none;border-right:1px solid rgba(82,82,91,.22);border-bottom:1px solid rgba(82,82,91,.22)} .pqueue-popover__header{position:sticky;top:0;display:flex;align-items:center;gap:.5rem;justify-content:space-between;padding:.7rem .85rem;border-bottom:1px solid rgba(82,82,91,.22);background:#1a1a1a;backdrop-filter:blur(6px);border-top-left-radius:14px;border-top-right-radius:14px} .pqueue-popover__title{font-weight:600;color:var(--heading-text,#f9fafb)} .pqueue-popover__hint{font-size:.72rem;color:var(--subtle-text,#9ca3af)} diff --git a/web/pqueue.js b/web/pqueue.js index 59b1ea4..07cb94c 100644 --- a/web/pqueue.js +++ b/web/pqueue.js @@ -1670,14 +1670,98 @@ toggleFiltersPopover(anchor) { try { + // If open, close and cleanup listeners if (state.dom.filtersPopover) { - state.dom.filtersPopover.remove(); + const pop = state.dom.filtersPopover; + if (state.dom.filtersPopoverListeners) { + const { onDocPointer, onResize, onScroll } = state.dom.filtersPopoverListeners; + if (onDocPointer) document.removeEventListener('pointerdown', onDocPointer, true); + if (onResize) window.removeEventListener('resize', onResize); + if (onScroll) window.removeEventListener('scroll', onScroll, true); + state.dom.filtersPopoverListeners = null; + } + pop.remove(); state.dom.filtersPopover = null; return; } + const pop = UI.buildFiltersPopover(anchor); document.body.appendChild(pop); state.dom.filtersPopover = pop; + + // Positioning function to keep popover within viewport + const position = () => { + try { + const margin = 10; + const rect = anchor.getBoundingClientRect(); + const viewportW = window.innerWidth; + const viewportH = window.innerHeight; + const scrollX = window.scrollX; + const scrollY = window.scrollY; + + // reset classes and styles affecting size + pop.classList.remove('pqueue-popover--above'); + pop.style.maxHeight = '80vh'; + + const popW = pop.offsetWidth; + const popH = pop.offsetHeight; + + const anchorCenter = rect.left + rect.width / 2 + scrollX; + let left = Math.round(anchorCenter - popW / 2); + left = Math.max(scrollX + margin, Math.min(left, scrollX + viewportW - popW - margin)); + + // Prefer below; flip above if overflowing bottom + let top = rect.bottom + margin + scrollY; + const bottomOverflow = (top + popH) - (scrollY + viewportH - margin); + if (bottomOverflow > 0) { + const aboveTop = rect.top + scrollY - popH - margin; + if (aboveTop >= scrollY + margin) { + top = aboveTop; + pop.classList.add('pqueue-popover--above'); + } else { + // Clamp within viewport if neither fits perfectly + top = Math.max(scrollY + margin, Math.min(top, scrollY + viewportH - popH - margin)); + } + } + + pop.style.left = `${left}px`; + pop.style.top = `${top}px`; + + // Position arrow + const arrow = pop.querySelector('.pqueue-popover__arrow'); + if (arrow) { + const popLeft = left; + const offset = Math.max(14, Math.min(anchorCenter - popLeft, popW - 14)); + arrow.style.left = `${offset}px`; + } + } catch (err) { /* noop */ } + }; + + // Outside click/tap closes popover + const onDocPointer = (ev) => { + try { + if (!state.dom.filtersPopover) return; + const target = ev.target; + if (state.dom.filtersPopover.contains(target)) return; + if (anchor && (target === anchor || (anchor.contains && anchor.contains(target)))) return; + // Close + document.removeEventListener('pointerdown', onDocPointer, true); + window.removeEventListener('resize', onResize); + window.removeEventListener('scroll', onScroll, true); + state.dom.filtersPopoverListeners = null; + state.dom.filtersPopover.remove(); + state.dom.filtersPopover = null; + } catch (err) { /* noop */ } + }; + const onResize = () => position(); + const onScroll = () => position(); + document.addEventListener('pointerdown', onDocPointer, true); + window.addEventListener('resize', onResize); + window.addEventListener('scroll', onScroll, true); + state.dom.filtersPopoverListeners = { onDocPointer, onResize, onScroll }; + + // Initial position + position(); } catch (err) { /* noop */ }