Files
familienarchiv/frontend/src/lib/components/Pagination.svelte
Marcel 3311dc29ae
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m7s
CI / OCR Service Tests (push) Successful in 34s
CI / Backend Unit Tests (push) Failing after 2m59s
CI / Unit & Component Tests (pull_request) Failing after 3m10s
CI / OCR Service Tests (pull_request) Successful in 34s
CI / Backend Unit Tests (pull_request) Failing after 3m1s
feat(documents): paginate search with a Pagination control
Frontend side of the /documents pagination work. The page.server.ts load
reads ?page= from the URL, forwards page+size=50 to the backend, and
exposes the new totalElements/pageNumber/pageSize/totalPages fields on
`data`. +page.svelte renders a <Pagination> component below the result
list; buildPageHref preserves every filter param and only updates page.
The existing triggerSearch debounce flow intentionally drops `page`
when any filter changes, so filter edits reset to page 0 automatically.

<Pagination> uses plain <a href> links (not goto) so SvelteKit's default
scroll restoration scrolls new pages to the top — the expected senior-UX
behaviour. Decorative chevrons wrapped in aria-hidden spans, 44px touch
targets, focus-visible ring, stacks vertically under 640px. The control
hides itself when totalPages ≤ 1.

Test coverage: 9 cases on Pagination (label, aria-current, prev/next
enable/disable, makeHref invocation, decorative chevron, touch target),
plus a filter-reset assertion on +page.svelte (page 5 → edit q →
goto URL must drop page=). Adds i18n keys in de/en/es. Manual edit to
api.ts pending a post-merge npm run generate:api against a rebuilt
dev backend. (#315)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 08:34:01 +02:00

59 lines
1.9 KiB
Svelte

<script lang="ts">
import * as m from '$lib/paraglide/messages.js';
interface Props {
/** 0-indexed current page. */
page: number;
/** Total number of pages. `0` or `1` hides the control as trivially there's nothing to navigate. */
totalPages: number;
/** Given a 0-indexed page number, returns the href the link should point at. */
makeHref: (page: number) => string;
/** Optional override for the outer `<nav>`'s aria-label. */
ariaLabel?: string;
}
const { page, totalPages, makeHref, ariaLabel }: Props = $props();
const hasPrev = $derived(page > 0);
const hasNext = $derived(page < totalPages - 1);
const linkBase =
'inline-flex min-h-[44px] min-w-[44px] items-center justify-center gap-1.5 rounded-sm border border-line bg-white px-4 py-2 font-sans text-sm font-bold text-ink transition-colors hover:bg-surface focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-2 focus-visible:outline-none aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-40';
</script>
{#if totalPages > 1}
<nav
aria-label={ariaLabel ?? m.pagination_nav_label()}
class="mt-6 flex flex-col items-center gap-3 sm:flex-row sm:justify-between"
>
<a
data-testid="pagination-prev"
aria-label={m.pagination_prev()}
aria-disabled={!hasPrev}
href={hasPrev ? makeHref(page - 1) : undefined}
class={linkBase}
>
<span aria-hidden="true">«</span>
{m.pagination_prev()}
</a>
<span
data-testid="pagination-page-label"
aria-current="page"
class="font-sans text-sm text-ink-2"
>
{m.pagination_page_of({ page: page + 1, total: totalPages })}
</span>
<a
data-testid="pagination-next"
aria-label={m.pagination_next()}
aria-disabled={!hasNext}
href={hasNext ? makeHref(page + 1) : undefined}
class={linkBase}
>
{m.pagination_next()}
<span aria-hidden="true">»</span>
</a>
</nav>
{/if}