feat(search): show spinner in search input while navigation is in-flight
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { navigating } from '$app/state';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
||||||
import SearchFilterBar from './SearchFilterBar.svelte';
|
import SearchFilterBar from './SearchFilterBar.svelte';
|
||||||
@@ -102,6 +103,7 @@ const showRightColumn = $derived(data.canWrite || (data.incompleteDocs?.length ?
|
|||||||
bind:tagQ={tagQ}
|
bind:tagQ={tagQ}
|
||||||
initialSenderName={data.initialValues?.senderName}
|
initialSenderName={data.initialValues?.senderName}
|
||||||
initialReceiverName={data.initialValues?.receiverName}
|
initialReceiverName={data.initialValues?.receiverName}
|
||||||
|
isLoading={navigating.to !== null}
|
||||||
onSearch={handleTextSearch}
|
onSearch={handleTextSearch}
|
||||||
onfocus={() => (qFocused = true)}
|
onfocus={() => (qFocused = true)}
|
||||||
onblur={() => (qFocused = false)}
|
onblur={() => (qFocused = false)}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ let {
|
|||||||
showAdvanced = $bindable(false),
|
showAdvanced = $bindable(false),
|
||||||
initialSenderName = '',
|
initialSenderName = '',
|
||||||
initialReceiverName = '',
|
initialReceiverName = '',
|
||||||
|
isLoading = false,
|
||||||
onSearch,
|
onSearch,
|
||||||
onfocus,
|
onfocus,
|
||||||
onblur
|
onblur
|
||||||
@@ -34,6 +35,7 @@ let {
|
|||||||
showAdvanced?: boolean;
|
showAdvanced?: boolean;
|
||||||
initialSenderName?: string;
|
initialSenderName?: string;
|
||||||
initialReceiverName?: string;
|
initialReceiverName?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
onSearch: () => void;
|
onSearch: () => void;
|
||||||
onfocus?: () => void;
|
onfocus?: () => void;
|
||||||
onblur?: () => 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"
|
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">
|
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||||
<img
|
{#if isLoading}
|
||||||
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Mag-Glass-MD.svg"
|
<svg
|
||||||
alt=""
|
role="status"
|
||||||
aria-hidden="true"
|
aria-label="Suche läuft…"
|
||||||
class="h-4 w-4 opacity-40"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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', () => {
|
describe('SearchFilterBar – tagQ live filter', () => {
|
||||||
it('calls onSearch when tag text changes in TagInput', async () => {
|
it('calls onSearch when tag text changes in TagInput', async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
|
|||||||
@@ -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 ────────────────────────────────────────────────────────────
|
// ─── Sort controls ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('Home page – sort controls', () => {
|
describe('Home page – sort controls', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user