feat(search): show spinner in search input while navigation is in-flight
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-06 15:52:37 +02:00
parent 023b6ddb49
commit 0e13fd194b
4 changed files with 53 additions and 6 deletions

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { navigating } from '$app/state';
import { untrack } from 'svelte';
import { SvelteURLSearchParams } from 'svelte/reactivity';
import SearchFilterBar from './SearchFilterBar.svelte';
@@ -102,6 +103,7 @@ const showRightColumn = $derived(data.canWrite || (data.incompleteDocs?.length ?
bind:tagQ={tagQ}
initialSenderName={data.initialValues?.senderName}
initialReceiverName={data.initialValues?.receiverName}
isLoading={navigating.to !== null}
onSearch={handleTextSearch}
onfocus={() => (qFocused = true)}
onblur={() => (qFocused = false)}

View File

@@ -18,6 +18,7 @@ let {
showAdvanced = $bindable(false),
initialSenderName = '',
initialReceiverName = '',
isLoading = false,
onSearch,
onfocus,
onblur
@@ -34,6 +35,7 @@ let {
showAdvanced?: boolean;
initialSenderName?: string;
initialReceiverName?: string;
isLoading?: boolean;
onSearch: () => void;
onfocus?: () => void;
onblur?: () => void;
@@ -70,12 +72,31 @@ $effect(() => {
class="block w-full border-line py-2.5 pr-10 pl-3 placeholder-ink-3 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
/>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Mag-Glass-MD.svg"
alt=""
aria-hidden="true"
class="h-4 w-4 opacity-40"
/>
{#if isLoading}
<svg
role="status"
aria-label="Suche läuft…"
class="h-4 w-4 animate-spin text-ink-3"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
></path>
</svg>
{:else}
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Mag-Glass-MD.svg"
alt=""
aria-hidden="true"
class="h-4 w-4 opacity-40"
/>
{/if}
</div>
</div>

View File

@@ -27,6 +27,20 @@ describe('SearchFilterBar sort controls', () => {
});
});
describe('SearchFilterBar loading spinner', () => {
it('shows search icon when isLoading is false', async () => {
render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc', isLoading: false });
const spinner = page.getByRole('status');
await expect.element(spinner).not.toBeInTheDocument();
});
it('shows spinner and hides search icon when isLoading is true', async () => {
render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc', isLoading: true });
const spinner = page.getByRole('status');
await expect.element(spinner).toBeInTheDocument();
});
});
describe('SearchFilterBar tagQ live filter', () => {
it('calls onSearch when tag text changes in TagInput', async () => {
vi.stubGlobal(

View File

@@ -240,6 +240,16 @@ describe('Home page error state', () => {
});
});
// ─── Loading spinner ──────────────────────────────────────────────────────────
describe('Home page loading spinner', () => {
it('does not show spinner by default', async () => {
render(Page, { data: emptyData });
const spinner = page.getByRole('status');
await expect.element(spinner).not.toBeInTheDocument();
});
});
// ─── Sort controls ────────────────────────────────────────────────────────────
describe('Home page sort controls', () => {