Skip to content

Commit 9cfbe19

Browse files
authored
chore(pagination): update to svelte 5 runes apis (#2018)
1 parent 067d647 commit 9cfbe19

File tree

5 files changed

+119
-55
lines changed

5 files changed

+119
-55
lines changed

.changeset/deep-areas-fail.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@stackoverflow/stacks-svelte": minor
3+
---
4+
5+
Migrate `Pagination` components to use Svelte 5 runes API.
6+
7+
BREAKING CHANGES:
8+
- `PaginationItem`: `on:click` event forwarding is replaced by `onclick` callback prop.
9+
- `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)`.
10+
11+
Migration example:
12+
```svelte
13+
<!-- Before (Svelte 4) -->
14+
<PaginationController
15+
on:pagechange={(e) => handlePageChange(e.detail)}
16+
/>
17+
18+
<!-- After (Svelte 5) -->
19+
<PaginationController
20+
onpagechange={(pageNumber) => handlePageChange(pageNumber)}
21+
/>
22+
```
23+
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
<script lang="ts">
2-
export let i18nNavigationLabel: string = "Pagination";
2+
import type { Snippet } from "svelte";
3+
4+
interface Props {
5+
/**
6+
* Localized translation for the navigation aria-label
7+
*/
8+
i18nNavigationLabel?: string;
9+
/**
10+
* Content rendered in the pagination
11+
*/
12+
children?: Snippet;
13+
}
14+
15+
let { i18nNavigationLabel = "Pagination", children }: Props = $props();
316
</script>
417

518
<nav aria-label={i18nNavigationLabel} class="pl24">
619
<ul class="list-reset s-pagination">
7-
<slot />
20+
{@render children?.()}
821
</ul>
922
</nav>

packages/stacks-svelte/src/components/Pagination/PaginationController.svelte

Lines changed: 53 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,58 @@
22
import Pagination from "./Pagination.svelte";
33
import PaginationItem from "./PaginationItem.svelte";
44
import PaginationItemClear from "./PaginationItemClear.svelte";
5-
import { createEventDispatcher } from "svelte";
65
import { generatePagination } from "./pagination-generator";
76
8-
/**
9-
* The current page number
10-
* @type {number}
11-
*/
12-
export let page: number;
13-
/**
14-
* The total number of pages
15-
* @type {number}
16-
*/
17-
export let totalPages: number;
18-
/**
19-
* Function to generate the URL for a given page number
20-
* @type {(page: number) => string}
21-
*/
22-
export let urlGenerator: (page: number) => string;
23-
/**
24-
* Whether to follow the link natively or rely on the pagechange event
25-
* @type {boolean}
26-
*/
27-
export let followLink = true;
28-
/**
29-
* Localized translation for the visible "Next" page link text
30-
*/
31-
export let i18nNextText = "Next";
32-
/**
33-
* Localized translation for the visible "Prev" page link text
34-
*/
35-
export let i18nPrevText = "Prev";
36-
/**
37-
* Localized translation for Next/Prev "page" screen reader text
38-
*/
39-
export let i18nPageText = "page";
40-
/**
41-
* Localized translation aria-label on nav element wrapping the pagination component
42-
*/
43-
export let i18nNavigationLabel: string = "Pagination";
7+
interface Props {
8+
/**
9+
* The current page number
10+
*/
11+
page: number;
12+
/**
13+
* The total number of pages
14+
*/
15+
totalPages: number;
16+
/**
17+
* Function to generate the URL for a given page number
18+
*/
19+
urlGenerator: (page: number) => string;
20+
/**
21+
* Whether to follow the link natively or rely on the pagechange event
22+
*/
23+
followLink?: boolean;
24+
/**
25+
* Localized translation for the visible "Next" page link text
26+
*/
27+
i18nNextText?: string;
28+
/**
29+
* Localized translation for the visible "Prev" page link text
30+
*/
31+
i18nPrevText?: string;
32+
/**
33+
* Localized translation for Next/Prev "page" screen reader text
34+
*/
35+
i18nPageText?: string;
36+
/**
37+
* Localized translation aria-label on nav element wrapping the pagination component
38+
*/
39+
i18nNavigationLabel?: string;
40+
/**
41+
* Callback fired when a page is changed
42+
*/
43+
onpagechange?: (pageNumber: number) => void;
44+
}
4445
45-
const dispatch = createEventDispatcher<{ pagechange: number }>();
46+
let {
47+
page,
48+
totalPages,
49+
urlGenerator,
50+
followLink = true,
51+
i18nNextText = "Next",
52+
i18nPrevText = "Prev",
53+
i18nPageText = "page",
54+
i18nNavigationLabel = "Pagination",
55+
onpagechange,
56+
}: Props = $props();
4657
4758
/**
4859
* Handles the click event for a pagination item
@@ -52,7 +63,7 @@
5263
const onPaginationItemClick = (page: number) => (evt: Event) => {
5364
if (!followLink) {
5465
evt.preventDefault();
55-
dispatch("pagechange", page);
66+
onpagechange?.(page);
5667
}
5768
};
5869
</script>
@@ -61,7 +72,7 @@
6172
{#if page > 1}
6273
<PaginationItem
6374
url={urlGenerator(page - 1)}
64-
on:click={onPaginationItemClick(page - 1)}
75+
onclick={onPaginationItemClick(page - 1)}
6576
>
6677
{i18nPrevText} <span class="v-visible-sr">{i18nPageText}</span>
6778
</PaginationItem>
@@ -70,7 +81,7 @@
7081
{#if typeof p === "number"}
7182
<PaginationItem
7283
url={urlGenerator(p)}
73-
on:click={onPaginationItemClick(p)}
84+
onclick={onPaginationItemClick(p)}
7485
selected={p === page}
7586
>
7687
<span class="v-visible-sr">{i18nPageText}</span>
@@ -83,7 +94,7 @@
8394
{#if page < totalPages}
8495
<PaginationItem
8596
url={urlGenerator(page + 1)}
86-
on:click={onPaginationItemClick(page + 1)}
97+
onclick={onPaginationItemClick(page + 1)}
8798
>
8899
{i18nNextText} <span class="v-visible-sr">{i18nPageText}</span>
89100
</PaginationItem>

packages/stacks-svelte/src/components/Pagination/PaginationController.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,21 +104,18 @@ describe("PaginationController", () => {
104104
totalPages: 5,
105105
urlGenerator,
106106
followLink: false,
107-
// @ts-expect-error events are not yet typed in the component
108-
$$events: {
109-
pagechange: mockHandler,
110-
},
107+
onpagechange: mockHandler,
111108
},
112109
});
113110

114111
await userEvent.click(screen.getByText("3"));
115-
expect(mockHandler).to.have.been.calledWith(sinon.match({ detail: 3 }));
112+
expect(mockHandler).to.have.been.calledWith(3);
116113

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

120117
await userEvent.click(screen.getByText("Next"));
121-
expect(mockHandler).to.have.been.calledWith(sinon.match({ detail: 3 }));
118+
expect(mockHandler).to.have.been.calledWith(3);
122119
});
123120

124121
it("disables previous button on first page", () => {
Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
<script lang="ts">
2-
export let url: string;
3-
export let selected: boolean = false;
2+
import type { Snippet } from "svelte";
3+
4+
interface Props {
5+
/**
6+
* The URL for the pagination item link
7+
*/
8+
url: string;
9+
/**
10+
* Whether this pagination item is currently selected
11+
*/
12+
selected?: boolean;
13+
/**
14+
* Callback fired when the pagination item is clicked
15+
*/
16+
onclick?: (e: MouseEvent) => void;
17+
/**
18+
* Content rendered in the pagination item
19+
*/
20+
children?: Snippet;
21+
}
22+
23+
let { url, selected = false, onclick, children }: Props = $props();
424
</script>
525

626
<li>
@@ -9,8 +29,8 @@
929
class:is-selected={selected}
1030
aria-current={selected ? "page" : undefined}
1131
href={url}
12-
on:click
32+
{onclick}
1333
>
14-
<slot />
34+
{@render children?.()}
1535
</a>
1636
</li>

0 commit comments

Comments
 (0)