Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/deep-areas-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@stackoverflow/stacks-svelte": minor
---

Migrate `Pagination` components to use Svelte 5 runes API.

BREAKING CHANGES:
- `PaginationItem`: `on:click` event forwarding is replaced by `onclick` callback prop.
- `PaginationController`: `on:pagechange` event is replaced by `onpagechange` callback prop with simplified signature. Previously the event passed `{ detail: pageNumber }`, now the callback directly receives the page number as the argument: `onpagechange(pageNumber)`.

Migration example:
```svelte
<!-- Before (Svelte 4) -->
<PaginationController
on:pagechange={(e) => handlePageChange(e.detail)}
/>

<!-- After (Svelte 5) -->
<PaginationController
onpagechange={(pageNumber) => handlePageChange(pageNumber)}
/>
```

17 changes: 15 additions & 2 deletions packages/stacks-svelte/src/components/Pagination/Pagination.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
<script lang="ts">
export let i18nNavigationLabel: string = "Pagination";
import type { Snippet } from "svelte";

interface Props {
/**
* Localized translation for the navigation aria-label
*/
i18nNavigationLabel?: string;
/**
* Content rendered in the pagination
*/
children?: Snippet;
}

let { i18nNavigationLabel = "Pagination", children }: Props = $props();
</script>

<nav aria-label={i18nNavigationLabel} class="pl24">
<ul class="list-reset s-pagination">
<slot />
{@render children?.()}
</ul>
</nav>
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,58 @@
import Pagination from "./Pagination.svelte";
import PaginationItem from "./PaginationItem.svelte";
import PaginationItemClear from "./PaginationItemClear.svelte";
import { createEventDispatcher } from "svelte";
import { generatePagination } from "./pagination-generator";

/**
* The current page number
* @type {number}
*/
export let page: number;
/**
* The total number of pages
* @type {number}
*/
export let totalPages: number;
/**
* Function to generate the URL for a given page number
* @type {(page: number) => string}
*/
export let urlGenerator: (page: number) => string;
/**
* Whether to follow the link natively or rely on the pagechange event
* @type {boolean}
*/
export let followLink = true;
/**
* Localized translation for the visible "Next" page link text
*/
export let i18nNextText = "Next";
/**
* Localized translation for the visible "Prev" page link text
*/
export let i18nPrevText = "Prev";
/**
* Localized translation for Next/Prev "page" screen reader text
*/
export let i18nPageText = "page";
/**
* Localized translation aria-label on nav element wrapping the pagination component
*/
export let i18nNavigationLabel: string = "Pagination";
interface Props {
/**
* The current page number
*/
page: number;
/**
* The total number of pages
*/
totalPages: number;
/**
* Function to generate the URL for a given page number
*/
urlGenerator: (page: number) => string;
/**
* Whether to follow the link natively or rely on the pagechange event
*/
followLink?: boolean;
/**
* Localized translation for the visible "Next" page link text
*/
i18nNextText?: string;
/**
* Localized translation for the visible "Prev" page link text
*/
i18nPrevText?: string;
/**
* Localized translation for Next/Prev "page" screen reader text
*/
i18nPageText?: string;
/**
* Localized translation aria-label on nav element wrapping the pagination component
*/
i18nNavigationLabel?: string;
/**
* Callback fired when a page is changed
*/
onpagechange?: (pageNumber: number) => void;
}

const dispatch = createEventDispatcher<{ pagechange: number }>();
let {
page,
totalPages,
urlGenerator,
followLink = true,
i18nNextText = "Next",
i18nPrevText = "Prev",
i18nPageText = "page",
i18nNavigationLabel = "Pagination",
onpagechange,
}: Props = $props();

/**
* Handles the click event for a pagination item
Expand All @@ -52,7 +63,7 @@
const onPaginationItemClick = (page: number) => (evt: Event) => {
if (!followLink) {
evt.preventDefault();
dispatch("pagechange", page);
onpagechange?.(page);
}
};
</script>
Expand All @@ -61,7 +72,7 @@
{#if page > 1}
<PaginationItem
url={urlGenerator(page - 1)}
on:click={onPaginationItemClick(page - 1)}
onclick={onPaginationItemClick(page - 1)}
>
{i18nPrevText} <span class="v-visible-sr">{i18nPageText}</span>
</PaginationItem>
Expand All @@ -70,7 +81,7 @@
{#if typeof p === "number"}
<PaginationItem
url={urlGenerator(p)}
on:click={onPaginationItemClick(p)}
onclick={onPaginationItemClick(p)}
selected={p === page}
>
<span class="v-visible-sr">{i18nPageText}</span>
Expand All @@ -83,7 +94,7 @@
{#if page < totalPages}
<PaginationItem
url={urlGenerator(page + 1)}
on:click={onPaginationItemClick(page + 1)}
onclick={onPaginationItemClick(page + 1)}
>
{i18nNextText} <span class="v-visible-sr">{i18nPageText}</span>
</PaginationItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,18 @@ describe("PaginationController", () => {
totalPages: 5,
urlGenerator,
followLink: false,
// @ts-expect-error events are not yet typed in the component
$$events: {
pagechange: mockHandler,
},
onpagechange: mockHandler,
},
});

await userEvent.click(screen.getByText("3"));
expect(mockHandler).to.have.been.calledWith(sinon.match({ detail: 3 }));
expect(mockHandler).to.have.been.calledWith(3);

await userEvent.click(screen.getByText("Prev"));
expect(mockHandler).to.have.been.calledWith(sinon.match({ detail: 1 }));
expect(mockHandler).to.have.been.calledWith(1);

await userEvent.click(screen.getByText("Next"));
expect(mockHandler).to.have.been.calledWith(sinon.match({ detail: 3 }));
expect(mockHandler).to.have.been.calledWith(3);
});

it("disables previous button on first page", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
<script lang="ts">
export let url: string;
export let selected: boolean = false;
import type { Snippet } from "svelte";

interface Props {
/**
* The URL for the pagination item link
*/
url: string;
/**
* Whether this pagination item is currently selected
*/
selected?: boolean;
/**
* Callback fired when the pagination item is clicked
*/
onclick?: (e: MouseEvent) => void;
/**
* Content rendered in the pagination item
*/
children?: Snippet;
}

let { url, selected = false, onclick, children }: Props = $props();
</script>

<li>
Expand All @@ -9,8 +29,8 @@
class:is-selected={selected}
aria-current={selected ? "page" : undefined}
href={url}
on:click
{onclick}
>
<slot />
{@render children?.()}
</a>
</li>