Fix container actions menu stacking above toolbar
Release rolling / release (push) Successful in 9m47s

Use Bootstrap Popper fixed strategy and a high z-index so the containers ⋮ menu is no longer covered by search, headers, or the titlebar.
This commit is contained in:
Raven Scott
2026-07-11 21:27:46 -04:00
parent 9511214382
commit 6c70b6c144
2 changed files with 112 additions and 37 deletions
+77 -4
View File
@@ -7522,10 +7522,10 @@ function buildContainerRow(container) {
</div>
<div class="dropdown container-actions-more">
<button
class="btn btn-sm btn-outline-secondary dropdown-toggle p-1"
class="btn btn-sm btn-outline-secondary dropdown-toggle p-1 container-actions-more-toggle"
type="button"
data-bs-toggle="dropdown"
data-bs-display="static"
data-bs-auto-close="true"
aria-expanded="false"
title="More actions"
>
@@ -7579,15 +7579,85 @@ function buildContainerRow(container) {
showContainerDetails(container);
});
}
// Popper fixed strategy: menu paints above search/header/titlebar
const moreToggle = row.querySelector('.container-actions-more-toggle');
if (moreToggle) initContainerActionsDropdown(moreToggle);
addActionListeners(row, container);
return row;
}
/**
* Bootstrap Dropdown with fixed Popper strategy so the menu is never covered
* by sticky toolbars, search, or the app titlebar (table overflow stacking).
* @param {HTMLElement} toggleBtn
*/
function initContainerActionsDropdown(toggleBtn) {
if (!toggleBtn || typeof window.bootstrap === 'undefined' || !window.bootstrap.Dropdown) return null;
try {
const existing = window.bootstrap.Dropdown.getInstance(toggleBtn);
if (existing) return existing;
const dd = new window.bootstrap.Dropdown(toggleBtn, {
autoClose: true,
popperConfig(defaultBsPopperConfig) {
const base = typeof defaultBsPopperConfig === 'function'
? defaultBsPopperConfig()
: { ...(defaultBsPopperConfig || {}) };
const modifiers = [...(base.modifiers || [])];
// Prefer fixed so we escape overflow:hidden / stacking contexts in the table
const withoutOverflow = modifiers.filter((m) => m.name !== 'preventOverflow' && m.name !== 'flip');
withoutOverflow.push(
{
name: 'preventOverflow',
options: {
boundary: 'viewport',
padding: 8,
altAxis: true,
},
},
{
name: 'flip',
options: {
fallbackPlacements: ['top-end', 'bottom-end', 'top-start', 'bottom-start'],
padding: 8,
},
}
);
return {
...base,
strategy: 'fixed',
placement: 'bottom-end',
modifiers: withoutOverflow,
};
},
});
// Ensure open menu is painted above titlebar/search (Popper may set its own z-index)
toggleBtn.addEventListener('shown.bs.dropdown', () => {
const menu = toggleBtn.nextElementSibling;
if (menu?.classList?.contains('dropdown-menu')) {
menu.style.zIndex = '2800';
}
});
return dd;
} catch {
return null;
}
}
// Lazy-init fixed dropdowns if a row was rendered before bootstrap was ready
document.addEventListener(
'click',
(e) => {
const btn = e.target?.closest?.('.container-actions-more-toggle');
if (btn) initContainerActionsDropdown(btn);
},
true
);
/** Close any open container action menus before DOM moves (avoids stuck fixed menus). */
function closeContainerActionMenus(root) {
const host = root || domCache.containerList || containerList || document;
try {
host.querySelectorAll?.('.container-actions-more .dropdown-toggle[aria-expanded="true"]').forEach((btn) => {
host.querySelectorAll?.('.container-actions-more .dropdown-toggle[aria-expanded="true"], .container-actions-more-toggle[aria-expanded="true"]').forEach((btn) => {
try {
const inst = window.bootstrap?.Dropdown?.getInstance?.(btn);
if (inst) inst.hide();
@@ -7603,7 +7673,9 @@ function closeContainerActionMenus(root) {
}
});
// Force-hide any leftover open menus after list refresh / popper desync
host.querySelectorAll?.('.container-actions-dropdown-menu.show').forEach((menu) => {
// (include body-level leftovers if Popper reparented styles)
document.querySelectorAll?.('.container-actions-dropdown-menu.show').forEach((menu) => {
if (root && root !== document && !root.contains(menu) && !menu.isConnected) return;
menu.classList.remove('show');
menu.style.removeProperty('position');
menu.style.removeProperty('inset');
@@ -7612,6 +7684,7 @@ function closeContainerActionMenus(root) {
menu.style.removeProperty('left');
menu.style.removeProperty('right');
menu.style.removeProperty('bottom');
menu.style.removeProperty('margin');
});
} catch {
// ignore
+35 -33
View File
@@ -2394,35 +2394,36 @@ textarea::placeholder {
}
/*
* ⋮ more-actions menu: use static display (no Popper fixed) so menus stay
* anchored to the row and never stick to the bottom of the viewport after
* list refreshes. Elevate the open row above neighbors; never clip the menu.
* ⋮ more-actions menu: Popper strategy "fixed" (see app.js) so the menu escapes
* table/overflow stacking and always paints above toolbars, search, titlebar.
* z-index must beat #titlebar (1100), #sidebar (1000), activity tray (1200).
* Stay below full-screen modals (3000).
*/
.container-actions-more {
position: static;
position: relative;
}
.container-actions-cell .dropdown {
position: static;
position: relative;
}
.container-actions .dropdown-menu,
.container-actions-dropdown-menu {
min-width: 11.5rem;
font-size: 13px;
z-index: 1080 !important;
z-index: 2800 !important;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border-strong);
background: var(--bg-elevated) !important;
/* Anchor to the actions cell, open upward when near table bottom */
position: absolute !important;
inset: auto auto 100% auto !important;
top: auto !important;
left: auto !important;
right: 0 !important;
bottom: 100% !important;
margin: 0 0 4px 0 !important;
transform: none !important;
}
/* Popper fixed menus — do not force absolute/inset (that re-clips under headers) */
.container-actions-dropdown-menu.show,
.dropdown-menu.container-actions-dropdown-menu.show {
z-index: 2800 !important;
display: block !important;
visibility: visible !important;
pointer-events: auto !important;
}
/* Closed menus must never paint (Popper leftovers / list refresh desync) */
@@ -2433,25 +2434,10 @@ textarea::placeholder {
pointer-events: none !important;
}
/* Elevate the open row so the menu paints above neighboring table rows */
#container-list > tr:has(.container-actions-more.show),
#container-list > tr:has(.dropdown-menu.show),
#container-list > tr:has(.dropdown.show) {
position: relative;
z-index: 20;
}
#container-list > tr:has(.container-actions-more.show) > td,
#container-list > tr:has(.dropdown-menu.show) > td,
#container-list > tr:has(.dropdown.show) > td {
background: var(--bg-elevated);
overflow: visible;
}
/* Containers table shell: never grow a horizontal scrollbar */
#containers-view .table-responsive {
overflow-x: hidden !important;
overflow-y: visible;
overflow-y: auto;
position: relative;
z-index: 1;
max-width: 100%;
@@ -2464,18 +2450,27 @@ textarea::placeholder {
table-layout: fixed;
}
/* Text truncates by default; actions cell may overflow for menus */
/* Text truncates by default; actions cell stays usable */
#containers-view #container-list > tr > td {
overflow: hidden;
}
#containers-view #container-list > tr > td.container-actions-cell {
overflow: visible;
position: relative;
z-index: 2;
}
/* Open-row elevation already sets background; keep action cell usable */
#containers-view #container-list > tr:has(.dropdown.show) > td.container-actions-cell {
overflow: visible;
z-index: 5;
}
/* Search / filter toolbar must not stack above open action menus */
#containers-view .page-header,
#containers-view .view-toolbar {
position: relative;
z-index: 1;
}
/* Accordion bodies must stay collapsed when not .show (defensive vs BS CDN lag) */
@@ -3235,6 +3230,13 @@ select.bg-dark {
background: var(--bg-elevated);
border: 1px solid var(--border-color);
color: var(--text-primary);
z-index: 1080;
}
/* Container ⋮ menus always win over toolbars / titlebar (Popper fixed + high stack) */
.dropdown-menu.container-actions-dropdown-menu,
.container-actions-dropdown-menu.dropdown-menu {
z-index: 2800 !important;
}
.dropdown-item {