Refine overlay stacking behavior

This commit is contained in:
Jordan Wages 2025-09-18 18:42:52 -05:00
commit 0450ec9e1c
2 changed files with 49 additions and 5 deletions

View file

@ -59,7 +59,7 @@
// UX Manager // UX Manager
const UX = (() => { const UX = (() => {
let currentBase = null; // { view, el } let currentBase = null; // { view, el }
const overlayStack = []; // [{ view, el, lastFocus }] const overlayStack = []; // [{ view, el, lastFocus, isHidden, isClosing }]
function ensureViewEl(view) { function ensureViewEl(view) {
if (!view || !view.el) throw new Error('Invalid view'); if (!view || !view.el) throw new Error('Invalid view');
@ -67,6 +67,42 @@
return view.el; return view.el;
} }
function syncOverlayVisibility() {
const topIndex = overlayStack.length - 1;
overlayStack.forEach((entry, idx) => {
const { el, view } = entry;
const shouldHide = idx !== topIndex;
if (shouldHide) {
if (!el.hasAttribute('hidden')) el.setAttribute('hidden', '');
el.setAttribute('aria-hidden', 'true');
el.setAttribute('inert', '');
el.inert = true;
if (!entry.isHidden) {
entry.isHidden = true;
if (view && typeof view.onHide === 'function') view.onHide();
}
} else {
if (el.hasAttribute('hidden')) el.removeAttribute('hidden');
el.removeAttribute('aria-hidden');
el.removeAttribute('inert');
el.inert = false;
entry.isHidden = false;
}
});
if (currentBase && currentBase.el) {
if (overlayStack.length > 0) {
currentBase.el.setAttribute('aria-hidden', 'true');
currentBase.el.setAttribute('inert', '');
currentBase.el.inert = true;
} else {
currentBase.el.removeAttribute('aria-hidden');
currentBase.el.removeAttribute('inert');
currentBase.el.inert = false;
}
}
}
async function replace(view) { async function replace(view) {
const el = ensureViewEl(view); const el = ensureViewEl(view);
const prev = currentBase; const prev = currentBase;
@ -80,6 +116,7 @@
} }
$uxRoot.appendChild(el); $uxRoot.appendChild(el);
currentBase = { view, el }; currentBase = { view, el };
syncOverlayVisibility();
await fadeIn(el); await fadeIn(el);
if (typeof view.onShow === 'function') view.onShow(); if (typeof view.onShow === 'function') view.onShow();
} }
@ -88,21 +125,27 @@
const el = ensureViewEl(view); const el = ensureViewEl(view);
el.classList.add('ux-view--overlay'); el.classList.add('ux-view--overlay');
const lastFocus = document.activeElement; const lastFocus = document.activeElement;
const prevEntry = overlayStack.length ? overlayStack[overlayStack.length - 1] : null;
const entry = { view, el, lastFocus, isHidden: false, isClosing: false, prev: prevEntry };
overlayStack.push(entry);
$uxOverlays.appendChild(el); $uxOverlays.appendChild(el);
syncOverlayVisibility();
await fadeIn(el); await fadeIn(el);
overlayStack.push({ view, el, lastFocus });
$uxRoot.setAttribute('aria-hidden', 'true');
if (typeof view.onShow === 'function') view.onShow(); if (typeof view.onShow === 'function') view.onShow();
} }
async function closeTop() { async function closeTop() {
const top = overlayStack.pop(); const top = overlayStack[overlayStack.length - 1];
if (!top) return; if (!top) return;
if (top.isClosing) return;
top.isClosing = true;
const { view, el, lastFocus } = top; const { view, el, lastFocus } = top;
if (view && typeof view.onHide === 'function') view.onHide();
await fadeOut(el); await fadeOut(el);
if (el.parentNode) el.parentNode.removeChild(el); if (el.parentNode) el.parentNode.removeChild(el);
if (typeof view.destroy === 'function') view.destroy(); if (typeof view.destroy === 'function') view.destroy();
if (overlayStack.length === 0) $uxRoot.removeAttribute('aria-hidden'); overlayStack.pop();
syncOverlayVisibility();
if (lastFocus && typeof lastFocus.focus === 'function') lastFocus.focus(); if (lastFocus && typeof lastFocus.focus === 'function') lastFocus.focus();
} }

View file

@ -11,6 +11,7 @@
.ux-overlays { position: absolute; inset: 0; pointer-events: none; } .ux-overlays { position: absolute; inset: 0; pointer-events: none; }
.ux-view { width: 100%; } .ux-view { width: 100%; }
.ux-view--overlay { position: absolute; inset: 0; z-index: 10; pointer-events: auto; } .ux-view--overlay { position: absolute; inset: 0; z-index: 10; pointer-events: auto; }
.ux-view[hidden] { display: none !important; }
.is-sr-only { .is-sr-only {
position: absolute; position: absolute;