Compare commits
2 commits
8b2bc9bb1c
...
fc85729374
| Author | SHA1 | Date | |
|---|---|---|---|
| fc85729374 | |||
| 0450ec9e1c |
3 changed files with 98 additions and 37 deletions
22
index.html
22
index.html
|
|
@ -40,6 +40,28 @@
|
||||||
<template id="tpl-search">
|
<template id="tpl-search">
|
||||||
<div class="ux-view fade">
|
<div class="ux-view fade">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
<header class="level mb-4">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item">
|
||||||
|
<h2 class="title is-4 mb-0">Search</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="level-item">
|
||||||
|
<button class="button is-text" type="button" data-action="back">Back</button>
|
||||||
|
</div>
|
||||||
|
<div class="level-item">
|
||||||
|
<div class="field is-grouped is-grouped-right is-align-items-center">
|
||||||
|
<div class="control">
|
||||||
|
<span class="is-size-7 has-text-grey mr-2">Sort</span>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<div class="buttons has-addons is-small" role="group" aria-label="Sort results" data-ref="sort-buttons"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
<form class="mb-4" role="search" aria-label="Track search" data-ref="form">
|
<form class="mb-4" role="search" aria-label="Track search" data-ref="form">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="q">Search</label>
|
<label class="label" for="q">Search</label>
|
||||||
|
|
|
||||||
112
script.js
112
script.js
|
|
@ -59,15 +59,68 @@
|
||||||
// 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');
|
||||||
view.el.classList.add('ux-view');
|
view.el.classList.add('ux-view');
|
||||||
|
if (view.kind) view.el.dataset.uxKind = view.kind;
|
||||||
return view.el;
|
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) {
|
async function replace(view) {
|
||||||
|
assertKind(view, 'base');
|
||||||
|
await closeAllOverlays({ restoreFocus: false });
|
||||||
const el = ensureViewEl(view);
|
const el = ensureViewEl(view);
|
||||||
const prev = currentBase;
|
const prev = currentBase;
|
||||||
if (prev) {
|
if (prev) {
|
||||||
|
|
@ -80,30 +133,40 @@
|
||||||
}
|
}
|
||||||
$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();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openOverlay(view) {
|
async function openOverlay(view) {
|
||||||
|
assertKind(view, 'overlay');
|
||||||
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({ restoreFocus = true } = {}) {
|
||||||
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();
|
||||||
if (lastFocus && typeof lastFocus.focus === 'function') lastFocus.focus();
|
syncOverlayVisibility();
|
||||||
|
if (restoreFocus && lastFocus && typeof lastFocus.focus === 'function') {
|
||||||
|
lastFocus.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
|
|
@ -113,7 +176,7 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { replace, openOverlay, closeTop };
|
return { replace, openOverlay, closeTop, closeAllOverlays };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// --- IndexedDB helpers (minimal, no external deps) ---
|
// --- IndexedDB helpers (minimal, no external deps) ---
|
||||||
|
|
@ -679,35 +742,8 @@
|
||||||
const $form = el.querySelector('[data-ref="form"]');
|
const $form = el.querySelector('[data-ref="form"]');
|
||||||
const $q = el.querySelector('[data-ref="q"]');
|
const $q = el.querySelector('[data-ref="q"]');
|
||||||
const $results = el.querySelector('[data-ref="results"]');
|
const $results = el.querySelector('[data-ref="results"]');
|
||||||
|
const $backBtn = el.querySelector('[data-action="back"]');
|
||||||
// Back + sort toolbar keeps controls grouped for accessibility.
|
const $sortButtons = el.querySelector('[data-ref="sort-buttons"]');
|
||||||
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 $status = document.createElement('p');
|
const $status = document.createElement('p');
|
||||||
$status.className = 'has-text-grey';
|
$status.className = 'has-text-grey';
|
||||||
|
|
@ -753,6 +789,8 @@
|
||||||
{ key: 'alpha', label: 'A–Z' },
|
{ key: 'alpha', label: 'A–Z' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!$backBtn) throw new Error('Missing back button');
|
||||||
|
if (!$sortButtons) throw new Error('Missing sort container');
|
||||||
sortOptions.forEach((opt) => {
|
sortOptions.forEach((opt) => {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.type = 'button';
|
btn.type = 'button';
|
||||||
|
|
|
||||||
1
site.css
1
site.css
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue