fix(web): prevent interaction blocking during search transitions

Remove document-level view transition fallback that applies
visibility:hidden to the entire page. Use scoped table transitions to
keep filters and controls interactive during search result updates.
This commit is contained in:
2026-01-31 12:16:36 -06:00
parent 02b18f0c66
commit 7f0f08725a
9 changed files with 16 additions and 28 deletions
@@ -82,7 +82,7 @@ function toggle(key: "instructionalMethod" | "campus" | "partOfTerm" | "attribut
>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps} style:view-transition-name="filter-overlay">
<div {...wrapperProps}>
<div {...props} transition:fly={{ duration: 150, y: -4 }}>
<div class="flex flex-col gap-3">
{#each sections as { label, key, dataKey }, i (key)}
+1 -1
View File
@@ -52,7 +52,7 @@ function parseNumberInput(value: string): number | null {
>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps} style:view-transition-name="filter-overlay">
<div {...wrapperProps}>
<div {...props} transition:fly={{ duration: 150, y: -4 }}>
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-1.5">
+2 -1
View File
@@ -48,7 +48,8 @@ function inTransition(_node: HTMLElement): TransitionConfig {
function outTransition(_node: HTMLElement): TransitionConfig {
const dir = navigationStore.direction;
const base = "position: absolute; top: 0; left: 0; width: 100%; height: 100%";
const base =
"position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none";
if (dir === "fade") {
return {
duration: DURATION,
@@ -91,7 +91,7 @@ function formatTime(time: string | null): string {
>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps} style:view-transition-name="filter-overlay">
<div {...wrapperProps}>
<div {...props} transition:fly={{ duration: 150, y: -4 }}>
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-1.5">
+1 -1
View File
@@ -34,7 +34,7 @@ const hasActiveFilters = $derived(openOnly || waitCountMax !== null);
>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps} style:view-transition-name="filter-overlay">
<div {...wrapperProps}>
<div {...props} transition:fly={{ duration: 150, y: -4 }}>
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-1.5">
@@ -126,7 +126,7 @@ $effect(() => {
>
{#snippet child({ wrapperProps, props, open: isOpen })}
{#if isOpen}
<div {...wrapperProps} style:view-transition-name="filter-overlay">
<div {...wrapperProps}>
<div {...props} transition:fly={{ duration: 150, y: -4 }}>
<Combobox.Viewport class="p-0.5">
{#each filteredSubjects as subject (subject.code)}
+1 -1
View File
@@ -96,7 +96,7 @@ $effect(() => {
>
{#snippet child({ wrapperProps, props, open: isOpen })}
{#if isOpen}
<div {...wrapperProps} style:view-transition-name="filter-overlay">
<div {...wrapperProps}>
<div {...props} transition:fly={{ duration: 150, y: -4 }}>
<Combobox.Viewport class="p-0.5">
{#each filteredTerms as term, i (term.slug)}
+7 -11
View File
@@ -439,23 +439,19 @@ async function performSearch() {
};
};
// Scoped view transitions only affect the table element, so filters and
// other controls remain fully interactive. Document-level transitions
// apply visibility:hidden to the entire page for the transition duration,
// blocking all pointer interactions — so we skip those entirely and let
// Svelte's animate:flip / in:fade handle the visual update instead.
const tableEl = document.querySelector("[data-search-results]") as HTMLElement | null;
const scopedSupport = tableEl && "startViewTransition" in tableEl;
if (scopedSupport) {
// Scoped transition — no top-layer issue, no need for filter-overlay workaround
if (tableEl && "startViewTransition" in tableEl) {
const transition = (tableEl as any).startViewTransition(async () => {
applyUpdate();
await tick();
});
await transition.finished;
} else if (document.startViewTransition) {
// Document-level fallback with z-index layering for filter overlays
const transition = document.startViewTransition(async () => {
applyUpdate();
await tick();
});
await transition.finished;
await transition.updateCallbackDone;
} else {
applyUpdate();
}
+1 -10
View File
@@ -133,7 +133,7 @@ input[type="checkbox"]:checked::before {
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
/* View Transitions API - disable default cross-fade so JS can animate clip-path */
/* View Transitions API - disable default cross-fade for scoped table transitions */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
@@ -251,12 +251,3 @@ body::-webkit-scrollbar {
::view-transition-new(search-results) {
animation-duration: 200ms;
}
/* Keep filter overlays above view-transition snapshots */
::view-transition-group(filter-overlay) {
z-index: 100;
}
::view-transition-old(filter-overlay),
::view-transition-new(filter-overlay) {
animation: none;
}