diff --git a/index.html b/index.html index 9be3e96..8ff117d 100644 --- a/index.html +++ b/index.html @@ -40,6 +40,28 @@ + + + + Search + + + + + Back + + + + + Sort + + + + + + + + Search diff --git a/script.js b/script.js index 132cd29..13cce9c 100644 --- a/script.js +++ b/script.js @@ -59,15 +59,68 @@ // UX Manager const UX = (() => { let currentBase = null; // { view, el } - const overlayStack = []; // [{ view, el, lastFocus }] + const overlayStack = []; // [{ view, el, lastFocus, isHidden, isClosing }] function ensureViewEl(view) { if (!view || !view.el) throw new Error('Invalid view'); view.el.classList.add('ux-view'); + if (view.kind) view.el.dataset.uxKind = view.kind; return view.el; } + function assertKind(view, expected) { + if (!view) throw new Error('Missing view'); + if (view.kind && view.kind !== expected) { + throw new Error(`Expected view kind "${expected}" but received "${view.kind}"`); + } + } + + async function closeAllOverlays({ restoreFocus = false } = {}) { + while (overlayStack.length) { + const shouldRestore = restoreFocus && overlayStack.length === 1; + await closeTop({ restoreFocus: shouldRestore }); + } + } + + 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) { + assertKind(view, 'base'); + await closeAllOverlays({ restoreFocus: false }); const el = ensureViewEl(view); const prev = currentBase; if (prev) { @@ -80,30 +133,40 @@ } $uxRoot.appendChild(el); currentBase = { view, el }; + syncOverlayVisibility(); await fadeIn(el); if (typeof view.onShow === 'function') view.onShow(); } async function openOverlay(view) { + assertKind(view, 'overlay'); const el = ensureViewEl(view); el.classList.add('ux-view--overlay'); 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); + syncOverlayVisibility(); await fadeIn(el); - overlayStack.push({ view, el, lastFocus }); - $uxRoot.setAttribute('aria-hidden', 'true'); if (typeof view.onShow === 'function') view.onShow(); } - async function closeTop() { - const top = overlayStack.pop(); + async function closeTop({ restoreFocus = true } = {}) { + const top = overlayStack[overlayStack.length - 1]; if (!top) return; + if (top.isClosing) return; + top.isClosing = true; const { view, el, lastFocus } = top; + if (view && typeof view.onHide === 'function') view.onHide(); await fadeOut(el); if (el.parentNode) el.parentNode.removeChild(el); if (typeof view.destroy === 'function') view.destroy(); - if (overlayStack.length === 0) $uxRoot.removeAttribute('aria-hidden'); - if (lastFocus && typeof lastFocus.focus === 'function') lastFocus.focus(); + overlayStack.pop(); + syncOverlayVisibility(); + if (restoreFocus && lastFocus && typeof lastFocus.focus === 'function') { + lastFocus.focus(); + } } window.addEventListener('keydown', (e) => { @@ -113,7 +176,7 @@ } }); - return { replace, openOverlay, closeTop }; + return { replace, openOverlay, closeTop, closeAllOverlays }; })(); // --- IndexedDB helpers (minimal, no external deps) --- @@ -679,35 +742,8 @@ const $form = el.querySelector('[data-ref="form"]'); const $q = el.querySelector('[data-ref="q"]'); const $results = el.querySelector('[data-ref="results"]'); - - // Back + sort toolbar keeps controls grouped for accessibility. - const $toolbar = document.createElement('div'); - $toolbar.className = 'level mb-4 is-mobile'; - const $toolbarLeft = document.createElement('div'); - $toolbarLeft.className = 'level-left'; - const $toolbarLeftItem = document.createElement('div'); - $toolbarLeftItem.className = 'level-item'; - const $backBtn = document.createElement('button'); - $backBtn.type = 'button'; - $backBtn.className = 'button is-small is-text'; - $backBtn.textContent = 'Back to menu'; - $toolbarLeftItem.appendChild($backBtn); - $toolbarLeft.appendChild($toolbarLeftItem); - const $toolbarRight = document.createElement('div'); - $toolbarRight.className = 'level-right'; - const $toolbarRightItem = document.createElement('div'); - $toolbarRightItem.className = 'level-item'; - const $sortLabel = document.createElement('span'); - $sortLabel.className = 'is-size-7 has-text-grey mr-2'; - $sortLabel.textContent = 'Sort'; - const $sortButtons = document.createElement('div'); - $sortButtons.className = 'buttons has-addons is-small'; - $toolbarRightItem.appendChild($sortLabel); - $toolbarRightItem.appendChild($sortButtons); - $toolbarRight.appendChild($toolbarRightItem); - $toolbar.appendChild($toolbarLeft); - $toolbar.appendChild($toolbarRight); - $form.insertAdjacentElement('afterend', $toolbar); + const $backBtn = el.querySelector('[data-action="back"]'); + const $sortButtons = el.querySelector('[data-ref="sort-buttons"]'); const $status = document.createElement('p'); $status.className = 'has-text-grey'; @@ -753,6 +789,8 @@ { key: 'alpha', label: 'A–Z' }, ]; + if (!$backBtn) throw new Error('Missing back button'); + if (!$sortButtons) throw new Error('Missing sort container'); sortOptions.forEach((opt) => { const btn = document.createElement('button'); btn.type = 'button'; diff --git a/site.css b/site.css index 1830dda..835fcdb 100644 --- a/site.css +++ b/site.css @@ -11,6 +11,7 @@ .ux-overlays { position: absolute; inset: 0; pointer-events: none; } .ux-view { width: 100%; } .ux-view--overlay { position: absolute; inset: 0; z-index: 10; pointer-events: auto; } +.ux-view[hidden] { display: none !important; } .is-sr-only { position: absolute;