fix: light dismiss for header menu/search overlays (outside tap closes, never activates)
This commit is contained in:
@@ -104,6 +104,20 @@
|
||||
const a = e.target.closest && e.target.closest('a');
|
||||
remember((a && a.dataset.q) || input.value);
|
||||
});
|
||||
// Dropdown dismiss (opt-in — /search keeps its results list). A suggestion
|
||||
// list is a NON-MODAL combobox popup (ARIA combobox pattern): interacting
|
||||
// outside the field+list closes it but the interaction is NOT swallowed —
|
||||
// unlike the modal header panels, which scrim-dismiss in Base.astro.
|
||||
if (opts.dismiss) {
|
||||
const box = input.closest('form') || input.parentElement;
|
||||
const hide = () => { clearTimeout(timer); list.innerHTML = ''; status.textContent = ''; };
|
||||
document.addEventListener('pointerdown', (e) => { if (list.firstChild && !box.contains(e.target)) hide(); });
|
||||
// relatedTarget may be null mid-click on our own links — pointerdown covers that path
|
||||
box.addEventListener('focusout', (e) => { if (e.relatedTarget && !box.contains(e.relatedTarget)) hide(); });
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && list.firstChild) { hide(); e.stopPropagation(); } // panel Escape stays a second step
|
||||
});
|
||||
}
|
||||
return run; // callers may run() immediately (e.g. /search?q=…)
|
||||
};
|
||||
})();
|
||||
|
||||
+28
-4
@@ -110,11 +110,35 @@ const links = [
|
||||
<script is:inline>
|
||||
// Header search suggestions (desktop field + mobile reveal); the forms
|
||||
// are plain GET /search, so everything below is progressive enhancement.
|
||||
newsSearch({ input: 'hdr-q', list: 'hdr-suggest', status: 'hdr-suggest-status', limit: 6, unit: 'suggestion' });
|
||||
newsSearch({ input: 'pop-q', list: 'pop-suggest', status: 'pop-suggest-status', limit: 6, unit: 'suggestion' });
|
||||
newsSearch({ input: 'hdr-q', list: 'hdr-suggest', status: 'hdr-suggest-status', limit: 6, unit: 'suggestion', dismiss: true });
|
||||
newsSearch({ input: 'pop-q', list: 'pop-suggest', status: 'pop-suggest-status', limit: 6, unit: 'suggestion', dismiss: true });
|
||||
var pop = document.querySelector('.search-pop');
|
||||
pop.addEventListener('toggle', function () {
|
||||
if (pop.open) document.getElementById('pop-q').focus();
|
||||
if (pop) pop.addEventListener('toggle', function () {
|
||||
var q = document.getElementById('pop-q');
|
||||
if (pop.open && q) q.focus();
|
||||
});
|
||||
// Light dismiss for the header panels (menu + search reveal) — scrim
|
||||
// convention (Material/iOS, NN/g error prevention): a tap outside an
|
||||
// open panel ONLY closes it and must never activate what's underneath.
|
||||
// <details> has no native light dismiss; the Popover API was evaluated
|
||||
// and rejected here — its light dismiss deliberately lets the outside
|
||||
// click THROUGH (::backdrop is spec-forced pointer-events: none), the
|
||||
// exact behavior research ruled out. Canceling pointerdown doesn't
|
||||
// cancel the later click (Pointer Events spec), hence the click guard.
|
||||
var swallow = false;
|
||||
document.addEventListener('pointerdown', function (e) {
|
||||
var open = document.querySelector('details[name="header-panel"][open]');
|
||||
swallow = !!(open && !open.contains(e.target));
|
||||
if (swallow) { open.open = false; e.preventDefault(); }
|
||||
}, true);
|
||||
document.addEventListener('click', function (e) {
|
||||
if (swallow) { swallow = false; e.preventDefault(); e.stopPropagation(); }
|
||||
}, true);
|
||||
// Escape closes the open panel and returns focus to its trigger button
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key !== 'Escape') return;
|
||||
var open = document.querySelector('details[name="header-panel"][open]');
|
||||
if (open) { open.open = false; open.querySelector('summary').focus(); }
|
||||
});
|
||||
</script>
|
||||
<main id="main">
|
||||
|
||||
+26
-12
@@ -22,7 +22,8 @@
|
||||
--on-galaxy-muted: #c7cde9;
|
||||
--tint-cherenkov: color-mix(in srgb, var(--cherenkov) 16%, transparent); /* <mark> highlight only */
|
||||
--cherenkov-hover: color-mix(in oklab, var(--cherenkov), var(--galaxy) 12%); /* CTA hover state */
|
||||
--ico-ext: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"%3E%3Cpath d="M7 17 17 7M9 7h8v8" fill="none" stroke="black" stroke-width="2.5"/%3E%3C/svg%3E');
|
||||
/* stroke 3: at ~0.72em rendering this matches the .ico family's ~1.5px optical weight */
|
||||
--ico-ext: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"%3E%3Cpath d="M7 17 17 7M9 7h8v8" fill="none" stroke="black" stroke-width="3"/%3E%3C/svg%3E');
|
||||
/* Type — fonts (BRAND D.3.1/D.3.2) and the ONLY font sizes in this codebase */
|
||||
--font-body: "Inter", "Inter Fallback", Arial, system-ui, sans-serif;
|
||||
--font-display: "Space Grotesk", var(--font-body); /* headlines only */
|
||||
@@ -213,7 +214,8 @@ img { max-width: 100%; height: auto; display: block; }
|
||||
spec-required search). Chrome stays whisper-quiet: hairline on navy.
|
||||
Flexes 140-200px (only it and the spacer flex — one-row invariant). */
|
||||
.nav-search { position: relative; display: flex; align-items: center; flex: 0 1 200px; min-width: 140px; margin-left: 6px; }
|
||||
.nav-search .search-ico { left: 11px; width: 14px; height: 14px; color: rgba(255, 255, 255, 0.7); }
|
||||
/* stroke 2.6 ≈ the family's 1.5px rendered weight at 14px (icons must not thin out when small) */
|
||||
.nav-search .search-ico { left: 11px; width: 14px; height: 14px; color: rgba(255, 255, 255, 0.7); stroke-width: 2.6; }
|
||||
.nav-search input {
|
||||
width: 100%; min-width: 0; font: inherit; font-size: var(--fs-s); color: #fff;
|
||||
background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.45); /* 3.55:1 vs Galaxy — WCAG 1.4.11 */
|
||||
@@ -345,6 +347,7 @@ a.btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: no
|
||||
}
|
||||
.suggest:empty { display: none; }
|
||||
.suggest li { display: flex; align-items: flex-start; gap: var(--space-s); padding: 0 12px; border-radius: calc(var(--radius) - 8px); }
|
||||
.suggest li + li { margin-top: 2px; } /* rows breathe — hover fills read as discrete items */
|
||||
.suggest li:hover, .suggest li:focus-within { background: var(--moon); }
|
||||
/* Focused-empty state labels ("Recent" / "Latest news") — quiet, non-interactive */
|
||||
li.suggest-label {
|
||||
@@ -352,16 +355,20 @@ li.suggest-label {
|
||||
text-transform: uppercase; letter-spacing: 0.08em; padding-top: 8px;
|
||||
}
|
||||
.suggest li.suggest-label:hover { background: none; }
|
||||
/* Each later group opens with a hairline — label + its rows read as one unit */
|
||||
.suggest li.suggest-label:not(:first-child) {
|
||||
margin-top: 6px; padding-top: 12px; border-top: 1px solid var(--border); border-radius: 0;
|
||||
}
|
||||
.search-results li.suggest-label { background: none; border: 0; box-shadow: none; padding: 8px 0 0; }
|
||||
/* The link holds <mark>-split text nodes — it must lay out as flowing text,
|
||||
never as a flex row (flex would turn every fragment into its own item and
|
||||
stop titles from wrapping). ≥44px target comes from the block padding. */
|
||||
.suggest a {
|
||||
flex: 1; min-width: 0; display: block; padding-block: 10px;
|
||||
flex: 1; min-width: 0; display: block; padding-block: 12px; /* row ≥44px — real tap target */
|
||||
color: var(--text); font-weight: 500; font-size: var(--fs-s); text-decoration: none;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.suggest small { margin-top: 12px; } /* date tracks the first title line */
|
||||
.suggest small { margin-top: 14px; } /* date tracks the first title line */
|
||||
.suggest::after {
|
||||
content: "Press Enter for all results"; display: block; margin-top: 4px;
|
||||
padding: 8px 12px 2px; border-top: 1px solid var(--border);
|
||||
@@ -494,7 +501,7 @@ span.page-step { color: var(--muted); } /* disabled end stop — non-interactive
|
||||
/* Inline rendering (< 1200px): light disclosure row with a chevron */
|
||||
.toc--inline { border: 1px solid var(--border); border-radius: var(--radius); padding: 4px var(--space-m); margin-bottom: var(--space-l); }
|
||||
.toc--inline summary { display: flex; align-items: center; justify-content: space-between; gap: var(--space-s); min-height: 40px; }
|
||||
.toc--inline summary .ico { width: 14px; height: 14px; }
|
||||
.toc--inline summary .ico { width: 14px; height: 14px; stroke-width: 2.6; } /* optical weight match at 14px */
|
||||
.toc--inline[open] summary .ico { transform: rotate(180deg); }
|
||||
.toc--inline ol { padding-bottom: var(--space-s); }
|
||||
.toc--rail { display: none; }
|
||||
@@ -576,17 +583,24 @@ span.page-step { color: var(--muted); } /* disabled end stop — non-interactive
|
||||
.notice { color: var(--muted); font-size: var(--fs-s); margin-top: 10px; }
|
||||
|
||||
/* Search results (list built by the inline script on /search) — card-like rows;
|
||||
same single-signal chrome as cards (borderless on Moon, shadow hover) */
|
||||
.search-results { list-style: none; margin: var(--space-m) 0 0; padding: 0; max-width: 720px; display: grid; gap: var(--space-s); }
|
||||
same single-signal chrome as cards (borderless on Moon, shadow hover).
|
||||
Margins, not grid gap: idle-state group labels must hug their rows. */
|
||||
.search-results { list-style: none; margin: var(--space-m) 0 0; padding: 0; max-width: 720px; }
|
||||
.search-results li + li { margin-top: var(--space-m); } /* each article reads as its own card */
|
||||
.search-results li.suggest-label + li { margin-top: var(--space-s); } /* rows hug their group label */
|
||||
.search-results li {
|
||||
display: flex; align-items: flex-start; gap: var(--space-s);
|
||||
background: #fff; border-radius: var(--radius);
|
||||
padding: 12px 18px; box-shadow: var(--shadow-ambient);
|
||||
padding: var(--space-m); box-shadow: var(--shadow-ambient);
|
||||
}
|
||||
.search-results li:hover { box-shadow: var(--shadow-lifted); }
|
||||
/* Title flows as inline text (never display:flex — see .suggest a note) */
|
||||
.search-results a { flex: 1; min-width: 0; overflow-wrap: break-word; text-decoration: none; font-weight: 500; }
|
||||
.search-results a:hover { text-decoration: underline; }
|
||||
/* Title flows as inline text (never display:flex — see .suggest a note);
|
||||
card-title treatment (fs-l Galaxy) so titles outweigh the muted meta */
|
||||
.search-results a {
|
||||
flex: 1; min-width: 0; overflow-wrap: break-word; text-decoration: none;
|
||||
font-weight: 600; font-size: var(--fs-l); line-height: 1.3; color: var(--galaxy);
|
||||
}
|
||||
.search-results a:hover { color: var(--link); text-decoration: underline; }
|
||||
/* Dates on results and suggestions — plain muted text, no chip chrome */
|
||||
.search-results small, .suggest small { color: var(--muted); white-space: nowrap; font-size: var(--fs-xs); }
|
||||
.search-results small { margin-top: 5px; } /* optical align with the first title line */
|
||||
@@ -709,7 +723,7 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va
|
||||
border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
|
||||
}
|
||||
.card, .search-results li { transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease); }
|
||||
.card:hover { transform: translateY(-2px); } /* the one elevate gesture (no cover zoom) */
|
||||
.card:hover, .search-results li:hover { transform: translateY(-2px); } /* the one elevate gesture (no cover zoom) */
|
||||
}
|
||||
@keyframes nebula { /* inert unless applied inside the block above */
|
||||
from { transform: translate3d(-8%, -5%, 0) rotate(-6deg) scale(1); }
|
||||
|
||||
Reference in New Issue
Block a user