Fix absolutely positioned popovers

This commit is contained in:
HighImKevin
2026-10-07 07:51:22 -04:00
parent d8f0395dea
commit cf69ad2bb4
4 changed files with 167 additions and 42 deletions
+3 -3
View File
@@ -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;
+2 -2
View File
@@ -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';
+59 -27
View File
@@ -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;
+103 -10
View File
@@ -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,
};