@tangle-network/ui 11.23.0 → 11.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.24.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6098fc6: Pagination: add a link mode for lists paged by URL. Pass `hrefFor` (zero-based page in, URL out) instead of `onPageChange` and each enabled control renders as a link, through `linkComponent` (default `"a"`) when a router link is needed. The current page carries `aria-current="page"`, and a disabled previous or next renders as a `<span aria-disabled="true">` with no `href`. `PaginationProps` is now a union of `PaginationButtonProps` and `PaginationLinkProps`; button mode is unchanged.
8
+
3
9
  ## 11.23.0
4
10
 
5
11
  ### Minor Changes
@@ -1,19 +1,57 @@
1
1
  import * as React from "react";
2
2
  //#region src/primitives/pagination.d.ts
3
+ interface PaginationBaseProps extends Omit<React.HTMLAttributes<HTMLElement>, "onChange" | "children"> {
4
+ /** The current page, zero-based. */
5
+ page: number;
6
+ pageCount: number;
7
+ /** Pages shown on each side of the current one before an ellipsis. */
8
+ siblings?: number;
9
+ }
10
+ export interface PaginationButtonProps extends PaginationBaseProps {
11
+ onPageChange: (page: number) => void;
12
+ hrefFor?: never;
13
+ linkComponent?: never;
14
+ }
15
+ export interface PaginationLinkProps extends PaginationBaseProps {
16
+ /** The URL of a page, given the zero-based page. */
17
+ hrefFor: (page: number) => string;
18
+ /** Renders each enabled control. Receives `href`; defaults to `"a"`. */
19
+ linkComponent?: React.ElementType;
20
+ /**
21
+ * Called with the target page when a link to another page is clicked, unless
22
+ * the click opens it in another tab or window.
23
+ */
24
+ onPageChange?: (page: number) => void;
25
+ }
3
26
  /**
4
27
  * Page controls for a paged list or table: previous, the page numbers around
5
28
  * the current one with the first and last always reachable, and next.
6
29
  *
7
30
  * Pages are zero-based in the API and one-based on screen. With one page or
8
31
  * none there is nothing to choose, so it renders nothing.
32
+ *
33
+ * Give `onPageChange` to render buttons, or `hrefFor` to render links for a
34
+ * list paged by URL. In link mode each enabled control is rendered by
35
+ * `linkComponent` (an `<a>` by default) with `href`, `className`,
36
+ * `aria-label`, `aria-current` and children; a disabled previous or next is a
37
+ * `<span aria-disabled="true">` with no `href`. A router whose link takes `to`
38
+ * instead of `href` needs a small adapter:
39
+ *
40
+ * ```tsx
41
+ * import { Link } from "react-router";
42
+ *
43
+ * const RouterLink = ({ href, ...props }: React.ComponentProps<"a">) => (
44
+ * <Link to={href ?? ""} {...props} />
45
+ * );
46
+ *
47
+ * <Pagination
48
+ * page={page}
49
+ * pageCount={pageCount}
50
+ * hrefFor={(p) => `?page=${p + 1}`}
51
+ * linkComponent={RouterLink}
52
+ * />
53
+ * ```
9
54
  */
10
- export interface PaginationProps extends Omit<React.HTMLAttributes<HTMLElement>, "onChange" | "children"> {
11
- /** The current page, zero-based. */
12
- page: number;
13
- pageCount: number;
14
- onPageChange: (page: number) => void;
15
- /** Pages shown on each side of the current one before an ellipsis. */
16
- siblings?: number;
17
- }
18
- export declare function Pagination({ page, pageCount, onPageChange, siblings, className, "aria-label": ariaLabel, ...rest }: PaginationProps): React.JSX.Element | null;
55
+ export type PaginationProps = PaginationButtonProps | PaginationLinkProps;
56
+ export declare function Pagination({ page, pageCount, onPageChange, hrefFor, linkComponent: LinkComponent, siblings, className, "aria-label": ariaLabel, ...rest }: PaginationProps): React.JSX.Element | null;
19
57
  //#endregion
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../lib/utils.js";
2
2
  import { buttonVariants } from "./button.js";
3
3
  import "react";
4
- import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { ChevronLeft, ChevronRight } from "lucide-react";
6
6
  //#region src/primitives/pagination.tsx
7
7
  /**
@@ -24,13 +24,46 @@ const pageButton = cn(buttonVariants({
24
24
  variant: "ghost",
25
25
  size: "sm"
26
26
  }), "min-w-8 px-2 tabular-nums");
27
- function Pagination({ page, pageCount, onPageChange, siblings = 1, className, "aria-label": ariaLabel = "Pagination", ...rest }) {
27
+ /** A click that opens the link in another tab or window leaves this page where it is. */
28
+ function opensElsewhere(event) {
29
+ return event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
30
+ }
31
+ function Pagination({ page, pageCount, onPageChange, hrefFor, linkComponent: LinkComponent = "a", siblings = 1, className, "aria-label": ariaLabel = "Pagination", ...rest }) {
28
32
  if (pageCount <= 1) return null;
29
33
  const last = pageCount - 1;
30
34
  const current = Math.min(Math.max(page, 0), last);
31
35
  const go = (next) => {
32
36
  const target = Math.min(Math.max(next, 0), last);
33
- if (target !== current) onPageChange(target);
37
+ if (target !== current) onPageChange?.(target);
38
+ };
39
+ const control = ({ target, label, className: controlClass, disabled, current: isCurrent, children }) => {
40
+ const ariaCurrent = isCurrent ? "page" : void 0;
41
+ if (!hrefFor) return /* @__PURE__ */ jsx("button", {
42
+ type: "button",
43
+ "aria-label": label,
44
+ "aria-current": ariaCurrent,
45
+ disabled,
46
+ onClick: () => go(target),
47
+ className: controlClass,
48
+ children
49
+ });
50
+ if (disabled) return /* @__PURE__ */ jsx("span", {
51
+ role: "link",
52
+ "aria-label": label,
53
+ "aria-disabled": "true",
54
+ className: cn(controlClass, "pointer-events-none"),
55
+ children
56
+ });
57
+ return /* @__PURE__ */ jsx(LinkComponent, {
58
+ href: hrefFor(target),
59
+ "aria-label": label,
60
+ "aria-current": ariaCurrent,
61
+ onClick: onPageChange ? (event) => {
62
+ if (!opensElsewhere(event)) go(target);
63
+ } : void 0,
64
+ className: controlClass,
65
+ children
66
+ });
34
67
  };
35
68
  return /* @__PURE__ */ jsx("nav", {
36
69
  "aria-label": ariaLabel,
@@ -39,22 +72,20 @@ function Pagination({ page, pageCount, onPageChange, siblings = 1, className, "a
39
72
  children: /* @__PURE__ */ jsxs("ul", {
40
73
  className: "flex flex-wrap items-center gap-1",
41
74
  children: [
42
- /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
43
- type: "button",
44
- "aria-label": "Previous page",
75
+ /* @__PURE__ */ jsx("li", { children: control({
76
+ target: current - 1,
77
+ label: "Previous page",
45
78
  disabled: current === 0,
46
- onClick: () => go(current - 1),
47
79
  className: cn(pageButton, "gap-1 pl-1.5"),
48
- children: [/* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true" }), /* @__PURE__ */ jsx("span", {
80
+ children: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true" }), /* @__PURE__ */ jsx("span", {
49
81
  className: "hidden sm:inline",
50
82
  children: "Previous"
51
- })]
83
+ })] })
52
84
  }) }),
53
- pageSlots(current, pageCount, siblings).map((slot) => typeof slot === "number" ? /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
54
- type: "button",
55
- "aria-label": `Page ${slot + 1}`,
56
- "aria-current": slot === current ? "page" : void 0,
57
- onClick: () => go(slot),
85
+ pageSlots(current, pageCount, siblings).map((slot) => typeof slot === "number" ? /* @__PURE__ */ jsx("li", { children: control({
86
+ target: slot,
87
+ label: `Page ${slot + 1}`,
88
+ current: slot === current,
58
89
  className: cn(pageButton, slot === current && "border-border bg-card font-semibold text-foreground hover:bg-card"),
59
90
  children: slot + 1
60
91
  }) }, slot) : /* @__PURE__ */ jsx("li", {
@@ -62,16 +93,15 @@ function Pagination({ page, pageCount, onPageChange, siblings = 1, className, "a
62
93
  className: "flex min-w-8 justify-center text-sm text-muted-foreground",
63
94
  children: "…"
64
95
  }, slot)),
65
- /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
66
- type: "button",
67
- "aria-label": "Next page",
96
+ /* @__PURE__ */ jsx("li", { children: control({
97
+ target: current + 1,
98
+ label: "Next page",
68
99
  disabled: current === last,
69
- onClick: () => go(current + 1),
70
100
  className: cn(pageButton, "gap-1 pr-1.5"),
71
- children: [/* @__PURE__ */ jsx("span", {
101
+ children: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
72
102
  className: "hidden sm:inline",
73
103
  children: "Next"
74
- }), /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })]
104
+ }), /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })] })
75
105
  }) })
76
106
  ]
77
107
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.23.0",
3
+ "version": "11.24.0",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,6 +25,41 @@ export const PaginationLengths: StoryObj = {
25
25
  ),
26
26
  }
27
27
 
28
+ /** Keeps the story in place; a real app passes its router's link. */
29
+ function StoryLink({ onClick, ...props }: React.ComponentProps<'a'>) {
30
+ return (
31
+ <a
32
+ {...props}
33
+ onClick={(event) => {
34
+ event.preventDefault()
35
+ onClick?.(event)
36
+ }}
37
+ />
38
+ )
39
+ }
40
+
41
+ function LinkedTable({ pageCount }: { pageCount: number }) {
42
+ const [page, setPage] = React.useState(0)
43
+ return (
44
+ <Pagination
45
+ page={page}
46
+ pageCount={pageCount}
47
+ hrefFor={(p) => `?page=${p + 1}`}
48
+ linkComponent={StoryLink}
49
+ onPageChange={setPage}
50
+ />
51
+ )
52
+ }
53
+
54
+ export const PaginationLinks: StoryObj = {
55
+ render: () => (
56
+ <div className="flex flex-col gap-6 bg-background p-6 text-foreground">
57
+ <LinkedTable pageCount={4} />
58
+ <LinkedTable pageCount={48} />
59
+ </div>
60
+ ),
61
+ }
62
+
28
63
  export const AlertTones: StoryObj = {
29
64
  render: () => (
30
65
  <div className="flex max-w-xl flex-col gap-3 bg-background p-6 text-foreground">
@@ -1,5 +1,6 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
+ import type * as React from "react";
3
4
  import { describe, expect, it, vi } from "vitest";
4
5
  import { Alert, AlertDescription, AlertTitle } from "./alert";
5
6
  import { Pagination, pageSlots } from "./pagination";
@@ -63,6 +64,95 @@ describe("Pagination", () => {
63
64
  });
64
65
  });
65
66
 
67
+ describe("Pagination link mode", () => {
68
+ const hrefFor = (page: number) => `?page=${page + 1}`;
69
+
70
+ it("renders links to one-based URLs from zero-based pages", () => {
71
+ render(<Pagination page={4} pageCount={12} hrefFor={hrefFor} />);
72
+
73
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
74
+ expect(screen.getByRole("link", { name: "Previous page" })).toHaveAttribute("href", "?page=4");
75
+ expect(screen.getByRole("link", { name: "Next page" })).toHaveAttribute("href", "?page=6");
76
+ expect(screen.getByRole("link", { name: "Page 1" })).toHaveAttribute("href", "?page=1");
77
+ expect(screen.getByRole("link", { name: "Page 12" })).toHaveAttribute("href", "?page=12");
78
+
79
+ const current = screen.getByRole("link", { name: "Page 5" });
80
+ expect(current.tagName).toBe("A");
81
+ expect(current).toHaveAttribute("href", "?page=5");
82
+ expect(current).toHaveAttribute("aria-current", "page");
83
+ expect(screen.getByRole("link", { name: "Page 4" })).not.toHaveAttribute("aria-current");
84
+ });
85
+
86
+ it("renders a disabled previous or next as a span without an href", () => {
87
+ const { rerender } = render(<Pagination page={0} pageCount={3} hrefFor={hrefFor} />);
88
+ const previous = screen.getByRole("link", { name: "Previous page" });
89
+ expect(previous.tagName).toBe("SPAN");
90
+ expect(previous).toHaveAttribute("aria-disabled", "true");
91
+ expect(previous).not.toHaveAttribute("href");
92
+ expect(previous.className).toContain("aria-disabled:opacity-50");
93
+ expect(screen.getByRole("link", { name: "Next page" })).toHaveAttribute("href", "?page=2");
94
+
95
+ rerender(<Pagination page={2} pageCount={3} hrefFor={hrefFor} />);
96
+ const next = screen.getByRole("link", { name: "Next page" });
97
+ expect(next.tagName).toBe("SPAN");
98
+ expect(next).toHaveAttribute("aria-disabled", "true");
99
+ expect(next).not.toHaveAttribute("href");
100
+ });
101
+
102
+ it("renders each enabled control with a custom link component", () => {
103
+ const received: string[] = [];
104
+ const RouterLink = ({ href, ...props }: React.ComponentProps<"a">) => {
105
+ received.push(href ?? "");
106
+ return <a data-router-link="" href={href} {...props} />;
107
+ };
108
+ render(<Pagination page={1} pageCount={3} hrefFor={hrefFor} linkComponent={RouterLink} />);
109
+
110
+ expect(received).toEqual(["?page=1", "?page=1", "?page=2", "?page=3", "?page=3"]);
111
+ const current = screen.getByRole("link", { name: "Page 2" });
112
+ expect(current).toHaveAttribute("data-router-link");
113
+ expect(current).toHaveAttribute("aria-current", "page");
114
+ });
115
+
116
+ it("reports a plain click on another page and ignores the current page and new-tab clicks", async () => {
117
+ const user = userEvent.setup();
118
+ const onPageChange = vi.fn();
119
+ render(
120
+ <Pagination
121
+ page={1}
122
+ pageCount={3}
123
+ hrefFor={hrefFor}
124
+ linkComponent={({ onClick, ...props }: React.ComponentProps<"a">) => (
125
+ <a
126
+ {...props}
127
+ onClick={(event) => {
128
+ onClick?.(event);
129
+ event.preventDefault();
130
+ }}
131
+ />
132
+ )}
133
+ onPageChange={onPageChange}
134
+ />,
135
+ );
136
+
137
+ await user.click(screen.getByRole("link", { name: "Next page" }));
138
+ await user.click(screen.getByRole("link", { name: "Page 2" }));
139
+ await user.keyboard("{Meta>}");
140
+ await user.click(screen.getByRole("link", { name: "Page 1" }));
141
+ await user.keyboard("{/Meta}");
142
+ expect(onPageChange.mock.calls).toEqual([[2]]);
143
+ });
144
+
145
+ it("requires onPageChange or hrefFor, and a link component only with hrefFor", () => {
146
+ const rejected = [
147
+ // @ts-expect-error neither onPageChange nor hrefFor
148
+ () => <Pagination page={0} pageCount={2} />,
149
+ // @ts-expect-error linkComponent without hrefFor
150
+ () => <Pagination page={0} pageCount={2} onPageChange={vi.fn()} linkComponent="a" />,
151
+ ];
152
+ expect(rejected).toHaveLength(2);
153
+ });
154
+ });
155
+
66
156
  describe("Alert", () => {
67
157
  it("paints a tone from the matched Brand status triple", () => {
68
158
  render(
@@ -3,23 +3,64 @@ import * as React from "react";
3
3
  import { cn } from "../lib/utils";
4
4
  import { buttonVariants } from "./button";
5
5
 
6
- /**
7
- * Page controls for a paged list or table: previous, the page numbers around
8
- * the current one with the first and last always reachable, and next.
9
- *
10
- * Pages are zero-based in the API and one-based on screen. With one page or
11
- * none there is nothing to choose, so it renders nothing.
12
- */
13
- export interface PaginationProps
6
+ interface PaginationBaseProps
14
7
  extends Omit<React.HTMLAttributes<HTMLElement>, "onChange" | "children"> {
15
8
  /** The current page, zero-based. */
16
9
  page: number;
17
10
  pageCount: number;
18
- onPageChange: (page: number) => void;
19
11
  /** Pages shown on each side of the current one before an ellipsis. */
20
12
  siblings?: number;
21
13
  }
22
14
 
15
+ export interface PaginationButtonProps extends PaginationBaseProps {
16
+ onPageChange: (page: number) => void;
17
+ hrefFor?: never;
18
+ linkComponent?: never;
19
+ }
20
+
21
+ export interface PaginationLinkProps extends PaginationBaseProps {
22
+ /** The URL of a page, given the zero-based page. */
23
+ hrefFor: (page: number) => string;
24
+ /** Renders each enabled control. Receives `href`; defaults to `"a"`. */
25
+ linkComponent?: React.ElementType;
26
+ /**
27
+ * Called with the target page when a link to another page is clicked, unless
28
+ * the click opens it in another tab or window.
29
+ */
30
+ onPageChange?: (page: number) => void;
31
+ }
32
+
33
+ /**
34
+ * Page controls for a paged list or table: previous, the page numbers around
35
+ * the current one with the first and last always reachable, and next.
36
+ *
37
+ * Pages are zero-based in the API and one-based on screen. With one page or
38
+ * none there is nothing to choose, so it renders nothing.
39
+ *
40
+ * Give `onPageChange` to render buttons, or `hrefFor` to render links for a
41
+ * list paged by URL. In link mode each enabled control is rendered by
42
+ * `linkComponent` (an `<a>` by default) with `href`, `className`,
43
+ * `aria-label`, `aria-current` and children; a disabled previous or next is a
44
+ * `<span aria-disabled="true">` with no `href`. A router whose link takes `to`
45
+ * instead of `href` needs a small adapter:
46
+ *
47
+ * ```tsx
48
+ * import { Link } from "react-router";
49
+ *
50
+ * const RouterLink = ({ href, ...props }: React.ComponentProps<"a">) => (
51
+ * <Link to={href ?? ""} {...props} />
52
+ * );
53
+ *
54
+ * <Pagination
55
+ * page={page}
56
+ * pageCount={pageCount}
57
+ * hrefFor={(p) => `?page=${p + 1}`}
58
+ * linkComponent={RouterLink}
59
+ * />
60
+ * ```
61
+ */
62
+ export type PaginationProps = PaginationButtonProps | PaginationLinkProps;
63
+
23
64
  type PageSlot = number | "gap-start" | "gap-end";
24
65
 
25
66
  /**
@@ -43,10 +84,26 @@ export function pageSlots(page: number, pageCount: number, siblings = 1): PageSl
43
84
 
44
85
  const pageButton = cn(buttonVariants({ variant: "ghost", size: "sm" }), "min-w-8 px-2 tabular-nums");
45
86
 
87
+ /** A click that opens the link in another tab or window leaves this page where it is. */
88
+ function opensElsewhere(event: React.MouseEvent) {
89
+ return event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
90
+ }
91
+
92
+ interface ControlProps {
93
+ target: number;
94
+ label: string;
95
+ className: string;
96
+ disabled?: boolean;
97
+ current?: boolean;
98
+ children: React.ReactNode;
99
+ }
100
+
46
101
  export function Pagination({
47
102
  page,
48
103
  pageCount,
49
104
  onPageChange,
105
+ hrefFor,
106
+ linkComponent: LinkComponent = "a",
50
107
  siblings = 1,
51
108
  className,
52
109
  "aria-label": ariaLabel = "Pagination",
@@ -58,40 +115,82 @@ export function Pagination({
58
115
  const current = Math.min(Math.max(page, 0), last);
59
116
  const go = (next: number) => {
60
117
  const target = Math.min(Math.max(next, 0), last);
61
- if (target !== current) onPageChange(target);
118
+ if (target !== current) onPageChange?.(target);
119
+ };
120
+
121
+ const control = ({ target, label, className: controlClass, disabled, current: isCurrent, children }: ControlProps) => {
122
+ const ariaCurrent = isCurrent ? ("page" as const) : undefined;
123
+ if (!hrefFor) {
124
+ return (
125
+ <button
126
+ type="button"
127
+ aria-label={label}
128
+ aria-current={ariaCurrent}
129
+ disabled={disabled}
130
+ onClick={() => go(target)}
131
+ className={controlClass}
132
+ >
133
+ {children}
134
+ </button>
135
+ );
136
+ }
137
+ if (disabled) {
138
+ return (
139
+ <span role="link" aria-label={label} aria-disabled="true" className={cn(controlClass, "pointer-events-none")}>
140
+ {children}
141
+ </span>
142
+ );
143
+ }
144
+ return (
145
+ <LinkComponent
146
+ href={hrefFor(target)}
147
+ aria-label={label}
148
+ aria-current={ariaCurrent}
149
+ onClick={
150
+ onPageChange
151
+ ? (event: React.MouseEvent) => {
152
+ if (!opensElsewhere(event)) go(target);
153
+ }
154
+ : undefined
155
+ }
156
+ className={controlClass}
157
+ >
158
+ {children}
159
+ </LinkComponent>
160
+ );
62
161
  };
63
162
 
64
163
  return (
65
164
  <nav aria-label={ariaLabel} className={cn("flex items-center", className)} {...rest}>
66
165
  <ul className="flex flex-wrap items-center gap-1">
67
166
  <li>
68
- <button
69
- type="button"
70
- aria-label="Previous page"
71
- disabled={current === 0}
72
- onClick={() => go(current - 1)}
73
- className={cn(pageButton, "gap-1 pl-1.5")}
74
- >
75
- <ChevronLeft aria-hidden="true" />
76
- <span className="hidden sm:inline">Previous</span>
77
- </button>
167
+ {control({
168
+ target: current - 1,
169
+ label: "Previous page",
170
+ disabled: current === 0,
171
+ className: cn(pageButton, "gap-1 pl-1.5"),
172
+ children: (
173
+ <>
174
+ <ChevronLeft aria-hidden="true" />
175
+ <span className="hidden sm:inline">Previous</span>
176
+ </>
177
+ ),
178
+ })}
78
179
  </li>
79
180
  {pageSlots(current, pageCount, siblings).map((slot) =>
80
181
  typeof slot === "number" ? (
81
182
  <li key={slot}>
82
- <button
83
- type="button"
84
- aria-label={`Page ${slot + 1}`}
85
- aria-current={slot === current ? "page" : undefined}
86
- onClick={() => go(slot)}
87
- className={cn(
183
+ {control({
184
+ target: slot,
185
+ label: `Page ${slot + 1}`,
186
+ current: slot === current,
187
+ className: cn(
88
188
  pageButton,
89
189
  slot === current &&
90
190
  "border-border bg-card font-semibold text-foreground hover:bg-card",
91
- )}
92
- >
93
- {slot + 1}
94
- </button>
191
+ ),
192
+ children: slot + 1,
193
+ })}
95
194
  </li>
96
195
  ) : (
97
196
  <li key={slot} aria-hidden="true" className="flex min-w-8 justify-center text-sm text-muted-foreground">
@@ -100,16 +199,18 @@ export function Pagination({
100
199
  ),
101
200
  )}
102
201
  <li>
103
- <button
104
- type="button"
105
- aria-label="Next page"
106
- disabled={current === last}
107
- onClick={() => go(current + 1)}
108
- className={cn(pageButton, "gap-1 pr-1.5")}
109
- >
110
- <span className="hidden sm:inline">Next</span>
111
- <ChevronRight aria-hidden="true" />
112
- </button>
202
+ {control({
203
+ target: current + 1,
204
+ label: "Next page",
205
+ disabled: current === last,
206
+ className: cn(pageButton, "gap-1 pr-1.5"),
207
+ children: (
208
+ <>
209
+ <span className="hidden sm:inline">Next</span>
210
+ <ChevronRight aria-hidden="true" />
211
+ </>
212
+ ),
213
+ })}
113
214
  </li>
114
215
  </ul>
115
216
  </nav>