Compare commits

...

3 Commits

Author SHA1 Message Date
Jürgen Mummert 7541c12f66 Add subtle hidden-based event filter animation 2026-04-05 14:01:30 +02:00
Jürgen Mummert e8fd218c74 Remove view transitions and restore hidden-only event filtering 2026-04-05 13:58:54 +02:00
Jürgen Mummert 5f652530ed Scope view transitions to event list and prevent page shift 2026-04-05 13:55:44 +02:00
@@ -38,12 +38,18 @@
</div> </div>
<style> <style>
@media (prefers-reduced-motion: no-preference) { .event-filter-target-list > .event {
::view-transition-old(root), opacity: 1;
::view-transition-new(root) { transform: translateY(0);
animation-duration: 240ms; transition: opacity 180ms ease, transform 180ms ease;
animation-timing-function: ease; transition-behavior: allow-discrete;
} will-change: opacity, transform;
}
.event-filter-target-list > .event.is-filtered-out {
opacity: 0;
transform: translateY(6px);
pointer-events: none;
} }
</style> </style>
@@ -124,63 +130,11 @@
const stateStorageKey = 'event-filter-state'; const stateStorageKey = 'event-filter-state';
const stateQueryKey = 'event_filter'; const stateQueryKey = 'event_filter';
const animationMs = 220; const animationMs = 180;
let hideTimers = new WeakMap(); let hideTimers = new WeakMap();
const supportsViewTransitions = typeof document.startViewTransition === 'function';
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let activeViewTransition = null;
let isViewTransitionMutation = false;
let currentFilter = { type: 'all', value: '' }; let currentFilter = { type: 'all', value: '' };
let suppressedChangeEvents = 0; let suppressedChangeEvents = 0;
const runWithLayoutTransition = (mutation) => {
if (!supportsViewTransitions || prefersReducedMotion) {
mutation();
return;
}
if (activeViewTransition) {
mutation();
return;
}
try {
const transition = document.startViewTransition(() => {
isViewTransitionMutation = true;
try {
mutation();
} finally {
isViewTransitionMutation = false;
}
});
activeViewTransition = transition;
transition.ready.catch(() => {
// Can reject when the transition is skipped.
});
transition.updateCallbackDone.catch(() => {
// Keep skipped/aborted update callbacks from surfacing as uncaught.
});
transition.finished
.catch(() => {
// Browsers can skip overlapping transitions; ignore these rejections.
})
.finally(() => {
if (activeViewTransition === transition) {
activeViewTransition = null;
}
});
} catch (error) {
mutation();
}
};
const shouldMutateVisibilityImmediately = () => supportsViewTransitions && !prefersReducedMotion;
const hasOptionValue = (selectElement, value) => { const hasOptionValue = (selectElement, value) => {
if (!selectElement) { if (!selectElement) {
return false; return false;
@@ -313,38 +267,21 @@
} }
}; };
const showEvent = (eventItem, { immediateVisibility = false } = {}) => { const showEvent = (eventItem) => {
clearHideTimer(eventItem); clearHideTimer(eventItem);
eventItem.hidden = false;
if (immediateVisibility) {
eventItem.hidden = false;
eventItem.classList.remove('is-filtered-out');
return;
}
if (eventItem.hidden) {
eventItem.hidden = false;
}
requestAnimationFrame(() => { requestAnimationFrame(() => {
eventItem.classList.remove('is-filtered-out'); eventItem.classList.remove('is-filtered-out');
}); });
}; };
const hideEvent = (eventItem, { immediateVisibility = false } = {}) => { const hideEvent = (eventItem) => {
clearHideTimer(eventItem); clearHideTimer(eventItem);
if (immediateVisibility) {
eventItem.classList.add('is-filtered-out');
eventItem.hidden = true;
return;
}
eventItem.classList.add('is-filtered-out'); eventItem.classList.add('is-filtered-out');
const timer = window.setTimeout(() => { const timer = window.setTimeout(() => {
eventItem.hidden = true; eventItem.hidden = true;
hideTimers.delete(eventItem); hideTimers.delete(eventItem);
}, animationMs); }, animationMs);
@@ -436,16 +373,12 @@
currentFilter = filterState; currentFilter = filterState;
setActiveControl(filterState); setActiveControl(filterState);
const immediateVisibility = shouldMutateVisibilityImmediately(); events.forEach((eventItem) => {
if (matches(eventItem, filterState)) {
runWithLayoutTransition(() => { showEvent(eventItem);
events.forEach((eventItem) => { } else {
if (matches(eventItem, filterState)) { hideEvent(eventItem);
showEvent(eventItem, { immediateVisibility }); }
} else {
hideEvent(eventItem, { immediateVisibility });
}
});
}); });
updateStatus(filterState); updateStatus(filterState);