Skip to content

Commit 09f4ae4

Browse files
committed
refactor(pagination): use discriminated union types for button/anchor variants
Replace unsafe type casts with a proper discriminated union pattern. PaginationButtonProps and PaginationPrevButtonProps now use href as the discriminator: when href is provided, anchor-specific props apply; otherwise, button-specific props apply. This removes all 'as unknown as' casts and ensures type safety at compile time. Also removes redundant aria-disabled on enabled anchor elements.
1 parent 5e22119 commit 09f4ae4

2 files changed

Lines changed: 59 additions & 46 deletions

File tree

packages/ui/src/components/Pagination/Pagination.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ const DefaultPagination = forwardRef<HTMLElement, DefaultPaginationProps>((props
121121
onPageChange(previousPage);
122122
}
123123

124+
const previousHref = getPageUrl && currentPage > 1 ? getPageUrl(previousPage) : undefined;
125+
const nextHref = getPageUrl && currentPage < totalPages ? getPageUrl(nextPage) : undefined;
126+
124127
return (
125128
<nav ref={ref} className={twMerge(theme.base, className)} {...restProps}>
126129
<ul className={theme.pages.base}>
@@ -129,30 +132,33 @@ const DefaultPagination = forwardRef<HTMLElement, DefaultPaginationProps>((props
129132
className={twMerge(theme.pages.previous.base, showIcon && theme.pages.showIcon)}
130133
onClick={goToPreviousPage}
131134
disabled={currentPage === 1}
132-
href={getPageUrl && currentPage > 1 ? getPageUrl(previousPage) : undefined}
135+
{...(previousHref ? { href: previousHref } : {})}
133136
>
134137
{showIcon && <ChevronLeftIcon aria-hidden className={theme.pages.previous.icon} />}
135138
{previousLabel}
136139
</PaginationNavigation>
137140
</li>
138141
{layout === "pagination" &&
139-
range(firstPage, lastPage).map((page: number) => (
140-
<li aria-current={page === currentPage ? "page" : undefined} key={page}>
141-
{renderPaginationButton({
142-
className: twMerge(theme.pages.selector.base, currentPage === page && theme.pages.selector.active),
143-
active: page === currentPage,
144-
onClick: () => onPageChange(page),
145-
href: getPageUrl ? getPageUrl(page) : undefined,
146-
children: page,
147-
})}
148-
</li>
149-
))}
142+
range(firstPage, lastPage).map((page: number) => {
143+
const pageHref = getPageUrl ? getPageUrl(page) : undefined;
144+
return (
145+
<li aria-current={page === currentPage ? "page" : undefined} key={page}>
146+
{renderPaginationButton({
147+
className: twMerge(theme.pages.selector.base, currentPage === page && theme.pages.selector.active),
148+
active: page === currentPage,
149+
onClick: () => onPageChange(page),
150+
...(pageHref ? { href: pageHref } : {}),
151+
children: page,
152+
})}
153+
</li>
154+
);
155+
})}
150156
<li>
151157
<PaginationNavigation
152158
className={twMerge(theme.pages.next.base, showIcon && theme.pages.showIcon)}
153159
onClick={goToNextPage}
154160
disabled={currentPage === totalPages}
155-
href={getPageUrl && currentPage < totalPages ? getPageUrl(nextPage) : undefined}
161+
{...(nextHref ? { href: nextHref } : {})}
156162
>
157163
{nextLabel}
158164
{showIcon && <ChevronRightIcon aria-hidden className={theme.pages.next.icon} />}

packages/ui/src/components/Pagination/PaginationButton.tsx

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { forwardRef, type ComponentProps, type ReactEventHandler, type ReactNode, type Ref } from "react";
3+
import { forwardRef, type ComponentProps, type ReactNode, type Ref } from "react";
44
import { get } from "../../helpers/get";
55
import { useResolveTheme } from "../../helpers/resolve-theme";
66
import { twMerge } from "../../helpers/tailwind-merge";
@@ -14,21 +14,44 @@ export interface PaginationButtonTheme {
1414
disabled: string;
1515
}
1616

17-
export interface PaginationButtonProps extends ComponentProps<"button">, ThemingProps<PaginationButtonTheme> {
17+
interface PaginationButtonBaseProps extends ThemingProps<PaginationButtonTheme> {
1818
active?: boolean;
1919
children?: ReactNode;
2020
className?: string;
21-
href?: string;
22-
onClick?: ReactEventHandler<HTMLButtonElement>;
2321
}
2422

25-
export interface PaginationPrevButtonProps extends Omit<PaginationButtonProps, "active"> {
23+
type PaginationButtonAsButton = PaginationButtonBaseProps &
24+
Omit<ComponentProps<"button">, keyof PaginationButtonBaseProps> & {
25+
href?: never;
26+
};
27+
28+
type PaginationButtonAsAnchor = PaginationButtonBaseProps &
29+
Omit<ComponentProps<"a">, keyof PaginationButtonBaseProps> & {
30+
href: string;
31+
};
32+
33+
export type PaginationButtonProps = PaginationButtonAsButton | PaginationButtonAsAnchor;
34+
35+
interface PaginationNavigationBaseProps extends ThemingProps<PaginationButtonTheme> {
36+
children?: ReactNode;
37+
className?: string;
2638
disabled?: boolean;
27-
href?: string;
2839
}
2940

41+
type PaginationNavigationAsButton = PaginationNavigationBaseProps &
42+
Omit<ComponentProps<"button">, keyof PaginationNavigationBaseProps> & {
43+
href?: never;
44+
};
45+
46+
type PaginationNavigationAsAnchor = PaginationNavigationBaseProps &
47+
Omit<ComponentProps<"a">, keyof PaginationNavigationBaseProps> & {
48+
href: string;
49+
};
50+
51+
export type PaginationPrevButtonProps = PaginationNavigationAsButton | PaginationNavigationAsAnchor;
52+
3053
export const PaginationButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, PaginationButtonProps>(
31-
({ active, children, className, href, onClick, theme: customTheme, clearTheme, applyTheme, ...props }, ref) => {
54+
({ active, children, className, theme: customTheme, clearTheme, applyTheme, ...props }, ref) => {
3255
const provider = useThemeProvider();
3356
const theme = useResolveTheme(
3457
[paginationTheme, provider.theme?.pagination, customTheme],
@@ -38,28 +61,18 @@ export const PaginationButton = forwardRef<HTMLButtonElement | HTMLAnchorElement
3861

3962
const mergedClassName = twMerge(active && theme.pages.selector.active, className);
4063

41-
if (href) {
64+
if ("href" in props && props.href) {
65+
const { href, ...anchorProps } = props;
4266
return (
43-
<a
44-
ref={ref as Ref<HTMLAnchorElement>}
45-
href={href}
46-
className={mergedClassName}
47-
onClick={onClick as unknown as ReactEventHandler<HTMLAnchorElement>}
48-
{...(props as ComponentProps<"a">)}
49-
>
67+
<a ref={ref as Ref<HTMLAnchorElement>} href={href} className={mergedClassName} {...anchorProps}>
5068
{children}
5169
</a>
5270
);
5371
}
5472

73+
const { href: _, ...buttonProps } = props as PaginationButtonAsButton;
5574
return (
56-
<button
57-
ref={ref as Ref<HTMLButtonElement>}
58-
type="button"
59-
className={mergedClassName}
60-
onClick={onClick}
61-
{...props}
62-
>
75+
<button ref={ref as Ref<HTMLButtonElement>} type="button" className={mergedClassName} {...buttonProps}>
6376
{children}
6477
</button>
6578
);
@@ -71,8 +84,6 @@ PaginationButton.displayName = "PaginationButton";
7184
export function PaginationNavigation({
7285
children,
7386
className,
74-
href,
75-
onClick,
7687
disabled = false,
7788
theme: customTheme,
7889
clearTheme,
@@ -88,22 +99,18 @@ export function PaginationNavigation({
8899

89100
const mergedClassName = twMerge(disabled && theme.pages.selector.disabled, className);
90101

91-
if (href && !disabled) {
102+
if ("href" in props && props.href && !disabled) {
103+
const { href, ...anchorProps } = props;
92104
return (
93-
<a
94-
href={href}
95-
className={mergedClassName}
96-
onClick={onClick as unknown as ReactEventHandler<HTMLAnchorElement>}
97-
aria-disabled={disabled}
98-
{...(props as ComponentProps<"a">)}
99-
>
105+
<a href={href} className={mergedClassName} {...anchorProps}>
100106
{children}
101107
</a>
102108
);
103109
}
104110

111+
const { href: _, ...buttonProps } = props as PaginationNavigationAsButton;
105112
return (
106-
<button type="button" className={mergedClassName} disabled={disabled} onClick={onClick} {...props}>
113+
<button type="button" className={mergedClassName} disabled={disabled} {...buttonProps}>
107114
{children}
108115
</button>
109116
);

0 commit comments

Comments
 (0)