Search box animation
This commit is contained in:
Fraxle
2026-06-26 13:34:27 +01:00
parent 4d98df43d7
commit fc5a6b6d70
2 changed files with 54 additions and 10 deletions
+34 -2
View File
@@ -165,8 +165,8 @@
} }
.utci-search-toggle[aria-expanded="true"] { .utci-search-toggle[aria-expanded="true"] {
color: #fff; color: #c8922a;
background: #c8922a; background: #fef3dc;
border-color: #c8922a; border-color: #c8922a;
} }
@@ -486,6 +486,38 @@ body.tl-scrubbing * { transition: none !important; }
left: 0; left: 0;
width: 100%; width: 100%;
z-index: 60; z-index: 60;
/* "Eyes opening": unfurl from the top edge while fading in, so the drop
shadow grows with the box instead of flashing in fully-formed. */
transform-origin: center center;
animation: utci-search-open 0.26s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes utci-search-open {
from {
opacity: 0;
transform: scaleY(0.05);
}
to {
opacity: 1;
transform: scaleY(1);
}
}
/* Reverse the reveal when dismissing — close from the vertical middle and
fade out rather than vanishing instantly. */
.utci-search-overlay.is-closing {
animation: utci-search-close 0.2s cubic-bezier(0.4, 0, 1, 1) forwards;
}
@keyframes utci-search-close {
from {
opacity: 1;
transform: scaleY(1);
}
to {
opacity: 0;
transform: scaleY(0.05);
}
} }
.utci-search-overlay .utci-search { .utci-search-overlay .utci-search {
+20 -8
View File
@@ -219,6 +219,11 @@ export function UTCIForecast() {
// Search is hidden by default and revealed via the magnifier next to the // Search is hidden by default and revealed via the magnifier next to the
// location. The input then overlays the location line to save space. // location. The input then overlays the location line to save space.
const [searchOpen, setSearchOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false);
// While closing, keep the overlay mounted so it can fade/close out before
// unmounting; cleared when the exit animation ends.
const [searchClosing, setSearchClosing] = useState(false);
const openSearch = () => { setSearchClosing(false); setSearchOpen(true); };
const closeSearch = () => { setSearchOpen(false); setSearchClosing(true); };
const searchWrapRef = useRef(null); const searchWrapRef = useRef(null);
const [colTogglesOpen, setColTogglesOpen] = useState(false); const [colTogglesOpen, setColTogglesOpen] = useState(false);
const searchInputRef = useRef(null); const searchInputRef = useRef(null);
@@ -286,12 +291,11 @@ export function UTCIForecast() {
if (!searchOpen) return; if (!searchOpen) return;
const onDown = (e) => { const onDown = (e) => {
if (searchWrapRef.current && !searchWrapRef.current.contains(e.target)) { if (searchWrapRef.current && !searchWrapRef.current.contains(e.target)) {
setSearchOpen(false); closeSearch();
setSearchQuery('');
} }
}; };
const onKey = (e) => { const onKey = (e) => {
if (e.key === 'Escape') { setSearchOpen(false); setSearchQuery(''); } if (e.key === 'Escape') { closeSearch(); }
}; };
document.addEventListener('mousedown', onDown); document.addEventListener('mousedown', onDown);
document.addEventListener('keydown', onKey); document.addEventListener('keydown', onKey);
@@ -625,7 +629,7 @@ export function UTCIForecast() {
class="utci-loc-name" class="utci-loc-name"
aria-label="Search for a town or city" aria-label="Search for a town or city"
aria-expanded=${searchOpen} aria-expanded=${searchOpen}
onClick=${() => setSearchOpen((v) => !v)} onClick=${() => (searchOpen ? closeSearch() : openSearch())}
> >
<svg class="utci-loc-pin" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"> <svg class="utci-loc-pin" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">
<path fill="currentColor" d="M12 2c-3.87 0-7 3.13-7 7 0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" /> <path fill="currentColor" d="M12 2c-3.87 0-7 3.13-7 7 0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" />
@@ -638,7 +642,7 @@ export function UTCIForecast() {
class="utci-search-toggle" class="utci-search-toggle"
aria-label="Search for a town or city" aria-label="Search for a town or city"
aria-expanded=${searchOpen} aria-expanded=${searchOpen}
onClick=${() => setSearchOpen((v) => !v)} onClick=${() => (searchOpen ? closeSearch() : openSearch())}
> >
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"> <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<circle cx="10.5" cy="10.5" r="6.5" fill="none" stroke="currentColor" stroke-width="2" /> <circle cx="10.5" cy="10.5" r="6.5" fill="none" stroke="currentColor" stroke-width="2" />
@@ -653,8 +657,16 @@ export function UTCIForecast() {
<div class=${'utci-fetch-time' + (isStale ? ' is-stale' : '')}> <div class=${'utci-fetch-time' + (isStale ? ' is-stale' : '')}>
${isStale ? html`<span class="stale-dot" title="Data is older than expected - retrying">● </span>` : ''}Last update: ${fetchedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} ${isStale ? html`<span class="stale-dot" title="Data is older than expected - retrying">● </span>` : ''}Last update: ${fetchedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>`} </div>`}
${searchOpen && html` ${(searchOpen || searchClosing) && html`
<div class="utci-search-wrap utci-search-overlay"> <div
class=${'utci-search-wrap utci-search-overlay' + (searchOpen ? '' : ' is-closing')}
onAnimationEnd=${(e) => {
if (e.target === e.currentTarget && !searchOpen) {
setSearchClosing(false);
setSearchQuery('');
}
}}
>
<input <input
ref=${searchInputRef} ref=${searchInputRef}
class="utci-search" class="utci-search"
@@ -679,7 +691,7 @@ export function UTCIForecast() {
setSearchQuery(''); setSearchQuery('');
setSearchResults([]); setSearchResults([]);
setSelectedDay(0); setSelectedDay(0);
setSearchOpen(false); closeSearch();
}} }}
> >
<div>${r.name}${r.admin1 ? `, ${r.admin1}` : ''}</div> <div>${r.name}${r.admin1 ? `, ${r.admin1}` : ''}</div>