From cf69ad2bb40073383b9d2253e39edd6d4474b660 Mon Sep 17 00:00:00 2001 From: HighImKevin Date: Wed, 7 Oct 2026 07:51:22 -0400 Subject: [PATCH] Fix absolutely positioned popovers --- scripts/cardBuilder.css | 6 +- scripts/cardBuilder.js | 4 +- scripts/homeScreenSectionConfigure.js | 86 ++++++++++++++------ scripts/modal.js | 113 +++++++++++++++++++++++--- 4 files changed, 167 insertions(+), 42 deletions(-) diff --git a/scripts/cardBuilder.css b/scripts/cardBuilder.css index 80fd611..244655c 100644 --- a/scripts/cardBuilder.css +++ b/scripts/cardBuilder.css @@ -520,7 +520,6 @@ font-size: 18px; font-weight: 600; z-index: 2; - padding: 8px 16px; letter-spacing: 2px; word-spacing: 2px; } @@ -1930,6 +1929,7 @@ top: initial !important; transform: initial !important; align-items: center; + height: auto !important; } .spotlight-section-title.headerTabs.sectionTabs, @@ -1941,7 +1941,6 @@ justify-content: start; width: auto; font-size: revert !important; - height: 40px !important; align-items: center; } @@ -1968,7 +1967,8 @@ .spotlight-section:not(.hide) .spotlight-nav-buttons-container { display: flex !important; - padding: 0 0.5em !important; + padding: 0 0 !important; + height: auto !important; } .spotlight-section:not(.hide) .spotlight-nav-buttons-container button { padding: 0.5em; diff --git a/scripts/cardBuilder.js b/scripts/cardBuilder.js index 91f1f17..8f5583c 100644 --- a/scripts/cardBuilder.js +++ b/scripts/cardBuilder.js @@ -1701,6 +1701,7 @@ max-height: min(60vh, 420px); overflow: hidden; padding: 0 !important; + min-width: initial !important; } .kefin-multi-query-modal [data-name="kefin-modal-content"] { overflow-y: auto !important; @@ -1813,8 +1814,7 @@ dialogClassName: 'kefin-multi-query-modal', onOpen: (modal) => { if (modal.backdrop) modal.backdrop.style.background = 'transparent'; - if (modal.dialogContainer) modal.dialogContainer.style.pointerEvents = 'none'; - if (modal.dialog) modal.dialog.style.pointerEvents = 'auto'; + // Document-absolute shell via ModalSystem.positionNearAnchor. if (modal.dialogContent) { modal.dialogContent.style.padding = '0'; modal.dialogContent.style.overflowY = 'auto'; diff --git a/scripts/homeScreenSectionConfigure.js b/scripts/homeScreenSectionConfigure.js index cde0846..33d2eb5 100644 --- a/scripts/homeScreenSectionConfigure.js +++ b/scripts/homeScreenSectionConfigure.js @@ -1066,18 +1066,15 @@ const maxLeft = window.innerWidth - menu.offsetWidth - margin; left = Math.min(Math.max(left, margin), maxLeft); - menu.style.top = `${top}px`; - menu.style.left = `${left}px`; + const container = menu.closest?.('.dialogContainer'); + if (typeof window.ModalSystem?.positionAnchoredPopoverShell === 'function') { + window.ModalSystem.positionAnchoredPopoverShell(container, menu, left, top); + } } function styleAnchoredPopoverDialog(modal, anchorBtn) { if (!modal?.dialog || !anchorBtn) return; - modal.dialog.classList.remove('centeredDialog', 'formDialog', 'smoothScrollY'); - modal.dialog.style.position = 'fixed'; - modal.dialog.style.margin = '0'; - modal.dialog.style.maxHeight = 'none'; modal.dialog.style.minWidth = '8rem'; - modal.dialog.style.width = 'auto'; if (typeof window.ModalSystem?.applyOpenAnimation === 'function') { window.ModalSystem.applyOpenAnimation(modal.dialog); } else { @@ -1091,11 +1088,12 @@ if (modal.backdrop) { modal.backdrop.style.background = 'transparent'; } - if (modal.dialogContainer) { - modal.dialogContainer.style.pointerEvents = 'none'; + if (typeof window.ModalSystem?.prepareAnchoredPopoverDialog === 'function') { + window.ModalSystem.prepareAnchoredPopoverDialog(modal.dialog); + } + if (typeof window.ModalSystem?.prepareAnchoredDialogContainer === 'function') { + window.ModalSystem.prepareAnchoredDialogContainer(modal.dialogContainer); } - modal.dialog.style.pointerEvents = 'auto'; - modal.dialog.style.overflow = 'visible'; requestAnimationFrame(() => positionFormatMenu(modal.dialog, anchorBtn)); } @@ -1192,6 +1190,13 @@ WARN('ModalSystem unavailable for option menu'); return null; } + if ( + window.ModalSystem.isOpen?.(OPTION_MENU_MODAL_ID) + && activePopover?.formatMenuAnchor === anchorBtn + ) { + closeActiveFormatMenu(); + return null; + } closeActiveFormatMenu(); if (!opts?.preserveCardTitleMenu) { closeCardTitleMenuOnly(); @@ -1398,6 +1403,13 @@ WARN('ModalSystem unavailable for card title options'); return null; } + if ( + window.ModalSystem.isOpen?.(CARD_TITLE_MENU_MODAL_ID) + && activePopover?.cardTitleMenuAnchor === anchorBtn + ) { + closeCardTitleMenu(); + return null; + } closeCardTitleMenu(); const cfg = entry.effectiveConfig; @@ -2139,7 +2151,7 @@ } function startPopoverTracking() { - // Placement is fixed for the life of the open popover. + // Placement is locked after first place — no scroll/resize chasing (0c11842). return () => {}; } @@ -2167,6 +2179,12 @@ } function finalizeConfigurePopoverClose() { + if (window.ModalSystem?.isOpen?.(OPTION_MENU_MODAL_ID)) { + window.ModalSystem.close(OPTION_MENU_MODAL_ID); + } + if (window.ModalSystem?.isOpen?.(CARD_TITLE_MENU_MODAL_ID)) { + window.ModalSystem.close(CARD_TITLE_MENU_MODAL_ID); + } if (!activePopover) return; const { entry, stopTracking, mountElement, sectionElement, outsideHandler } = activePopover; stopTracking?.(); @@ -2333,16 +2351,18 @@ } popover.classList.toggle('is-below', placeBelow); - popover.style.position = 'fixed'; - popover.style.left = `${leftViewport}px`; - popover.style.bottom = 'auto'; - popover.style.right = 'auto'; - popover.style.margin = '0'; + let topViewport; if (placeBelow) { - popover.style.top = `${buttonRect.bottom + gap}px`; + topViewport = buttonRect.bottom + gap; } else { const height = getPopoverPlacementHeight(popover, popover.classList.contains('is-expanded')); - popover.style.top = `${Math.max(margin, buttonRect.top - gap - height)}px`; + topViewport = Math.max(margin, buttonRect.top - gap - height); + } + const container = activePopover?.modal?.dialogContainer; + if (typeof window.ModalSystem?.positionAnchoredPopoverShell === 'function') { + window.ModalSystem.positionAnchoredPopoverShell(container, popover, leftViewport, topViewport); + } else if (typeof window.ModalSystem?.prepareAnchoredPopoverDialog === 'function') { + window.ModalSystem.prepareAnchoredPopoverDialog(popover); } } @@ -2507,14 +2527,14 @@ }, onOpen: (modalInstance) => { if (modalInstance.backdrop) modalInstance.backdrop.style.background = 'transparent'; - if (modalInstance.dialogContainer) modalInstance.dialogContainer.style.pointerEvents = 'none'; + if (typeof window.ModalSystem?.prepareAnchoredDialogContainer === 'function') { + window.ModalSystem.prepareAnchoredDialogContainer(modalInstance.dialogContainer); + } + if (typeof window.ModalSystem?.prepareAnchoredPopoverDialog === 'function') { + window.ModalSystem.prepareAnchoredPopoverDialog(modalInstance.dialog); + } if (modalInstance.dialog) { - modalInstance.dialog.classList.remove('centeredDialog', 'formDialog', 'smoothScrollY', 'dialog-fixedSize'); - modalInstance.dialog.style.pointerEvents = 'auto'; - modalInstance.dialog.style.maxHeight = 'none'; modalInstance.dialog.style.minHeight = '0'; - modalInstance.dialog.style.height = 'auto'; - modalInstance.dialog.style.width = 'auto'; } if (modalInstance.dialogContent) { modalInstance.dialogContent.style.padding = '0'; @@ -2526,6 +2546,12 @@ }); const popover = modal.dialog; + if (typeof window.ModalSystem?.prepareAnchoredDialogContainer === 'function') { + window.ModalSystem.prepareAnchoredDialogContainer(modal.dialogContainer); + } + if (typeof window.ModalSystem?.prepareAnchoredPopoverDialog === 'function') { + window.ModalSystem.prepareAnchoredPopoverDialog(modal.dialog); + } // Queries / expand state use the dialog root (has kefin-section-configure-popover) const openEditorBtn = popover.querySelector('.kefin-section-open-editor-btn'); @@ -2863,10 +2889,15 @@ function createConfigureButton(sectionConfig, sectionElement) { const button = document.createElement('button'); button.type = 'button'; - button.className = 'section-configure-button material-icons settings paper-icon-button-light'; + button.setAttribute('is', 'paper-icon-button-light'); + button.className = 'section-configure-button paper-icon-button-light'; if (isTvLayout()) button.classList.add('show-focus'); button.title = 'Configure'; button.setAttribute('aria-label', 'Configure'); + const icon = document.createElement('span'); + icon.className = 'material-icons settings'; + icon.setAttribute('aria-hidden', 'true'); + button.appendChild(icon); button.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); @@ -2944,7 +2975,7 @@ overflow: visible; } .kefin-section-configure-popover { - position: fixed; + position: relative; z-index: 12000; box-sizing: border-box; display: flex; @@ -2953,6 +2984,7 @@ min-width: 14rem; padding: 0.5em; overflow: visible; + inset: auto; } .kefin-section-configure-popover [data-name="kefin-modal-content"] { padding: 0 !important; diff --git a/scripts/modal.js b/scripts/modal.js index 805e75e..12a9afc 100644 --- a/scripts/modal.js +++ b/scripts/modal.js @@ -107,12 +107,103 @@ window.ModalSystem = (function() { ); } + let anchoredPopoverStylesInjected = false; + + function ensureAnchoredPopoverStyles() { + if (anchoredPopoverStylesInjected) return; + anchoredPopoverStylesInjected = true; + const style = document.createElement('style'); + style.id = 'kefinTweaks-anchoredPopover-styles'; + style.textContent = ` + .dialogContainer.kefin-modal-anchored-container { + contain: none !important; + overflow: visible !important; + } + .dialogContainer.kefin-modal-anchored-container > .dialog.kefin-anchored-popover-dialog { + position: relative !important; + inset: auto !important; + top: auto !important; + right: auto !important; + bottom: auto !important; + left: auto !important; + width: auto !important; + height: auto !important; + max-height: none !important; + } + `; + document.head.appendChild(style); + } + /** - * Position a dialog near an anchor (fixed coords, viewport-clamped). + * Document-absolute shell: override Jellyfin fixed full-screen dialogContainer. + * @param {HTMLElement|null|undefined} dialogContainer + */ + function prepareAnchoredDialogContainer(dialogContainer) { + if (!dialogContainer) return; + ensureAnchoredPopoverStyles(); + dialogContainer.classList.add('kefin-modal-anchored-container'); + dialogContainer.setAttribute('data-kefin-anchored', 'true'); + dialogContainer.style.position = 'absolute'; + dialogContainer.style.right = 'auto'; + dialogContainer.style.bottom = 'auto'; + dialogContainer.style.inset = 'auto'; + dialogContainer.style.width = 'auto'; + dialogContainer.style.height = 'auto'; + dialogContainer.style.overflow = 'visible'; + dialogContainer.style.contain = 'none'; + dialogContainer.style.pointerEvents = 'none'; + if (!dialogContainer.style.zIndex) { + dialogContainer.style.zIndex = '1100'; + } + } + + /** + * Inner dialog is in-flow inside the anchored container (not viewport-fixed / inset fill). + * @param {HTMLElement|null|undefined} dialog + */ + function prepareAnchoredPopoverDialog(dialog) { + if (!dialog) return; + ensureAnchoredPopoverStyles(); + dialog.classList.add('kefin-anchored-popover-dialog'); + dialog.classList.remove('centeredDialog', 'formDialog', 'smoothScrollY', 'dialog-fixedSize'); + dialog.style.position = 'relative'; + dialog.style.inset = 'auto'; + dialog.style.top = 'auto'; + dialog.style.right = 'auto'; + dialog.style.bottom = 'auto'; + dialog.style.left = 'auto'; + dialog.style.margin = '0'; + dialog.style.width = 'auto'; + dialog.style.height = 'auto'; + dialog.style.maxHeight = 'none'; + dialog.style.pointerEvents = 'auto'; + dialog.style.overflow = 'visible'; + } + + /** + * Place anchored popover shell in document space; dialog lays out inside the container. + * @param {HTMLElement|null|undefined} dialogContainer + * @param {HTMLElement|null|undefined} dialog + * @param {number} leftViewport + * @param {number} topViewport + */ + function positionAnchoredPopoverShell(dialogContainer, dialog, leftViewport, topViewport) { + prepareAnchoredDialogContainer(dialogContainer); + prepareAnchoredPopoverDialog(dialog); + if (!dialogContainer) return; + const scrollX = window.scrollX || window.pageXOffset || 0; + const scrollY = window.scrollY || window.pageYOffset || 0; + dialogContainer.style.left = `${leftViewport + scrollX}px`; + dialogContainer.style.top = `${topViewport + scrollY}px`; + } + + /** + * Position a dialog near an anchor (document-absolute shell + scroll offsets). * @param {HTMLElement} dialog * @param {HTMLElement} anchor + * @param {{ dialogContainer?: HTMLElement|null }} [options] */ - function positionNearAnchor(dialog, anchor) { + function positionNearAnchor(dialog, anchor, options = {}) { if (!dialog || !anchor) return; const margin = 8; const gap = 8; @@ -138,12 +229,10 @@ window.ModalSystem = (function() { } top = Math.min(Math.max(top, margin), window.innerHeight - Math.min(dialogHeight, window.innerHeight - margin * 2) - margin); - dialog.style.position = 'fixed'; - dialog.style.left = `${left}px`; - dialog.style.top = `${top}px`; - dialog.style.margin = '0'; - dialog.style.right = 'auto'; - dialog.style.bottom = 'auto'; + const dialogContainer = options.dialogContainer + || dialog.closest?.('.dialogContainer') + || null; + positionAnchoredPopoverShell(dialogContainer, dialog, left, top); } function focusElement(el) { @@ -562,8 +651,9 @@ window.ModalSystem = (function() { if (anchor instanceof HTMLElement) { dialog.classList.remove('centeredDialog', 'formDialog', 'smoothScrollY', 'dialog-fixedSize'); dialog.style.maxHeight = 'none'; + prepareAnchoredDialogContainer(dialogContainer); requestAnimationFrame(() => { - positionNearAnchor(dialog, anchor); + positionNearAnchor(dialog, anchor, { dialogContainer }); }); } @@ -622,7 +712,7 @@ window.ModalSystem = (function() { } } - // Remove dialog from DOM + // Remove dialog container from DOM (dialog stays a child for anchored popovers) if (modal.dialogContainer && modal.dialogContainer.parentNode) { modal.dialogContainer.parentNode.removeChild(modal.dialogContainer); } @@ -710,6 +800,9 @@ window.ModalSystem = (function() { closeAll: closeAllModals, applyOpenAnimation, attachFocusTrap, + prepareAnchoredDialogContainer, + prepareAnchoredPopoverDialog, + positionAnchoredPopoverShell, positionNearAnchor, getFocusableElements, };