Skip to content

Commit a6698d4

Browse files
authored
fix(button): Fixed Typescript error when using Next.js Link in the button as prop (#1244)
* fix(button): correctly infer type from `as` prop `as` prop was throwing TS errors when using Next Link component. Now this correctly infers the type from any component passed to the `as` prop fix #1002 fix #1107 * fix(dropdown-item): applied the generic types to DropdownItem and also wraps it with a forwardRef * docs(button): removed now-unnecessary @ts-expect-error comments * fix(button): replaced use of old genericForwardRef
1 parent afc4c64 commit a6698d4

File tree

9 files changed

+184
-171
lines changed

9 files changed

+184
-171
lines changed

examples/button/button.polymorph.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ function Component() {
4646
<Button as="span" className="cursor-pointer">
4747
Span Button
4848
</Button>
49-
{/* @ts-expect-error TODO: fix `as` inference */}
5049
<Button as={Link} href="#">
5150
Next Link Button
5251
</Button>

examples/dropdown/dropdown.customItem.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ function Component() {
4141
function Component() {
4242
return (
4343
<Dropdown dismissOnClick={false} label="My custom item">
44-
{/* @ts-expect-error TODO: fix `as` inference */}
4544
<DropdownItem as={Link} href="#">
4645
Home
4746
</DropdownItem>

src/components/Button/Button.tsx

Lines changed: 99 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import type { ComponentPropsWithoutRef, ElementType, ForwardedRef } from 'react';
2-
import { type ReactNode } from 'react';
1+
import type { ElementType } from 'react';
2+
import { forwardRef, type ReactNode } from 'react';
33
import { twMerge } from 'tailwind-merge';
4-
import genericForwardRef from '../../helpers/generic-forward-ref';
54
import { mergeDeep } from '../../helpers/merge-deep';
65
import { getTheme } from '../../theme-store';
76
import type { DeepPartial } from '../../types';
@@ -16,6 +15,7 @@ import { Spinner } from '../Spinner';
1615
import { ButtonBase, type ButtonBaseProps } from './ButtonBase';
1716
import type { PositionInButtonGroup } from './ButtonGroup';
1817
import { ButtonGroup } from './ButtonGroup';
18+
import type { PolymorphicComponentPropWithRef, PolymorphicRef } from '../../helpers/generic-as-prop';
1919

2020
export interface FlowbiteButtonTheme {
2121
base: string;
@@ -67,105 +67,110 @@ export interface ButtonSizes extends Pick<FlowbiteSizes, 'xs' | 'sm' | 'lg' | 'x
6767
[key: string]: string;
6868
}
6969

70-
export type ButtonProps<T extends ElementType = 'button'> = {
71-
as?: T | null;
72-
href?: string;
73-
color?: keyof FlowbiteColors;
74-
fullSized?: boolean;
75-
gradientDuoTone?: keyof ButtonGradientDuoToneColors;
76-
gradientMonochrome?: keyof ButtonGradientColors;
77-
target?: string;
78-
isProcessing?: boolean;
79-
processingLabel?: string;
80-
processingSpinner?: ReactNode;
81-
label?: ReactNode;
82-
outline?: boolean;
83-
pill?: boolean;
84-
positionInGroup?: keyof PositionInButtonGroup;
85-
size?: keyof ButtonSizes;
86-
theme?: DeepPartial<FlowbiteButtonTheme>;
87-
} & ComponentPropsWithoutRef<T>;
88-
89-
const ButtonComponentFn = <T extends ElementType = 'button'>(
70+
export type ButtonProps<T extends ElementType = 'button'> = PolymorphicComponentPropWithRef<
71+
T,
9072
{
91-
children,
92-
className,
93-
color = 'info',
94-
disabled,
95-
fullSized,
96-
isProcessing = false,
97-
processingLabel = 'Loading...',
98-
processingSpinner,
99-
gradientDuoTone,
100-
gradientMonochrome,
101-
label,
102-
outline = false,
103-
pill = false,
104-
positionInGroup = 'none',
105-
size = 'md',
106-
theme: customTheme = {},
107-
...props
108-
}: ButtonProps<T>,
109-
ref: ForwardedRef<T>,
110-
) => {
111-
const { buttonGroup: groupTheme, button: buttonTheme } = getTheme();
112-
const theme = mergeDeep(buttonTheme, customTheme);
73+
href?: string;
74+
color?: keyof FlowbiteColors;
75+
fullSized?: boolean;
76+
gradientDuoTone?: keyof ButtonGradientDuoToneColors;
77+
gradientMonochrome?: keyof ButtonGradientColors;
78+
target?: string;
79+
isProcessing?: boolean;
80+
processingLabel?: string;
81+
processingSpinner?: ReactNode;
82+
label?: ReactNode;
83+
outline?: boolean;
84+
pill?: boolean;
85+
positionInGroup?: keyof PositionInButtonGroup;
86+
size?: keyof ButtonSizes;
87+
theme?: DeepPartial<FlowbiteButtonTheme>;
88+
}
89+
>;
90+
91+
type ButtonComponentType = (<C extends React.ElementType = 'button'>(
92+
props: ButtonProps<C>,
93+
) => React.ReactNode | null) & { displayName?: string };
94+
95+
const ButtonComponentFn: ButtonComponentType = forwardRef(
96+
<T extends ElementType = 'button'>(
97+
{
98+
children,
99+
className,
100+
color = 'info',
101+
disabled,
102+
fullSized,
103+
isProcessing = false,
104+
processingLabel = 'Loading...',
105+
processingSpinner,
106+
gradientDuoTone,
107+
gradientMonochrome,
108+
label,
109+
outline = false,
110+
pill = false,
111+
positionInGroup = 'none',
112+
size = 'md',
113+
theme: customTheme = {},
114+
...props
115+
}: ButtonProps<T>,
116+
ref: PolymorphicRef<T>,
117+
) => {
118+
const { buttonGroup: groupTheme, button: buttonTheme } = getTheme();
119+
const theme = mergeDeep(buttonTheme, customTheme);
113120

114-
const theirProps = props as ButtonBaseProps<T>;
121+
const theirProps = props as ButtonBaseProps<T>;
115122

116-
return (
117-
<ButtonBase
118-
ref={ref}
119-
disabled={disabled}
120-
className={twMerge(
121-
theme.base,
122-
disabled && theme.disabled,
123-
!gradientDuoTone && !gradientMonochrome && theme.color[color],
124-
gradientDuoTone && !gradientMonochrome && theme.gradientDuoTone[gradientDuoTone],
125-
!gradientDuoTone && gradientMonochrome && theme.gradient[gradientMonochrome],
126-
outline && (theme.outline.color[color] ?? theme.outline.color.default),
127-
theme.pill[pill ? 'on' : 'off'],
128-
fullSized && theme.fullSized,
129-
groupTheme.position[positionInGroup],
130-
className,
131-
)}
132-
{...theirProps}
133-
>
134-
<span
123+
return (
124+
<ButtonBase
125+
ref={ref}
126+
disabled={disabled}
135127
className={twMerge(
136-
theme.inner.base,
137-
theme.outline[outline ? 'on' : 'off'],
138-
theme.outline.pill[outline && pill ? 'on' : 'off'],
139-
theme.size[size],
140-
outline && !theme.outline.color[color] && theme.inner.outline,
141-
isProcessing && theme.isProcessing,
142-
isProcessing && theme.inner.isProcessingPadding[size],
143-
theme.inner.position[positionInGroup],
128+
theme.base,
129+
disabled && theme.disabled,
130+
!gradientDuoTone && !gradientMonochrome && theme.color[color],
131+
gradientDuoTone && !gradientMonochrome && theme.gradientDuoTone[gradientDuoTone],
132+
!gradientDuoTone && gradientMonochrome && theme.gradient[gradientMonochrome],
133+
outline && (theme.outline.color[color] ?? theme.outline.color.default),
134+
theme.pill[pill ? 'on' : 'off'],
135+
fullSized && theme.fullSized,
136+
groupTheme.position[positionInGroup],
137+
className,
144138
)}
139+
{...theirProps}
145140
>
146-
<>
147-
{isProcessing && (
148-
<span className={twMerge(theme.spinnerSlot, theme.spinnerLeftPosition[size])}>
149-
{processingSpinner || <Spinner size={size} />}
150-
</span>
141+
<span
142+
className={twMerge(
143+
theme.inner.base,
144+
theme.outline[outline ? 'on' : 'off'],
145+
theme.outline.pill[outline && pill ? 'on' : 'off'],
146+
theme.size[size],
147+
outline && !theme.outline.color[color] && theme.inner.outline,
148+
isProcessing && theme.isProcessing,
149+
isProcessing && theme.inner.isProcessingPadding[size],
150+
theme.inner.position[positionInGroup],
151151
)}
152-
{typeof children !== 'undefined' ? (
153-
children
154-
) : (
155-
<span data-testid="flowbite-button-label" className={twMerge(theme.label)}>
156-
{isProcessing ? processingLabel : label}
157-
</span>
158-
)}
159-
</>
160-
</span>
161-
</ButtonBase>
162-
);
163-
};
152+
>
153+
<>
154+
{isProcessing && (
155+
<span className={twMerge(theme.spinnerSlot, theme.spinnerLeftPosition[size])}>
156+
{processingSpinner || <Spinner size={size} />}
157+
</span>
158+
)}
159+
{typeof children !== 'undefined' ? (
160+
children
161+
) : (
162+
<span data-testid="flowbite-button-label" className={twMerge(theme.label)}>
163+
{isProcessing ? processingLabel : label}
164+
</span>
165+
)}
166+
</>
167+
</span>
168+
</ButtonBase>
169+
);
170+
},
171+
);
164172

165173
ButtonComponentFn.displayName = 'Button';
166-
167-
const ButtonComponent = genericForwardRef(ButtonComponentFn);
168-
169-
export const Button = Object.assign(ButtonComponent, {
174+
export const Button = Object.assign(ButtonComponentFn, {
170175
Group: ButtonGroup,
171176
});

src/components/Button/ButtonBase.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import { createElement, type ComponentPropsWithoutRef, type ElementType, type ForwardedRef } from 'react';
2-
import genericForwardRef from '../../helpers/generic-forward-ref';
1+
import { createElement, type ComponentPropsWithoutRef, type ElementType, type ForwardedRef, forwardRef } from 'react';
32

43
export type ButtonBaseProps<T extends ElementType = 'button'> = {
54
as?: T;
65
href?: string;
76
} & ComponentPropsWithoutRef<T>;
87

9-
const ButtonBaseComponent = <T extends ElementType = 'button'>(
10-
{ children, as: Component, href, type = 'button', ...props }: ButtonBaseProps<T>,
11-
ref: ForwardedRef<T>,
12-
) => {
13-
const BaseComponent = Component || (href ? 'a' : 'button');
8+
export const ButtonBase = forwardRef(
9+
<T extends ElementType = 'button'>(
10+
{ children, as: Component, href, type = 'button', ...props }: ButtonBaseProps<T>,
11+
ref: ForwardedRef<T>,
12+
) => {
13+
const BaseComponent = Component || (href ? 'a' : 'button');
1414

15-
return createElement(BaseComponent, { ref, href, type, ...props }, children);
16-
};
15+
return createElement(BaseComponent, { ref, href, type, ...props }, children);
16+
},
17+
);
1718

18-
export const ButtonBase = genericForwardRef(ButtonBaseComponent);
19+
ButtonBase.displayName = 'ButtonBaseComponent';

src/components/Dropdown/Dropdown.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ describe('Components / Dropdown', () => {
167167
});
168168

169169
describe('Dropdown item render', async () => {
170-
it('should override Dropdownn.Item base component when using `as` prop', async () => {
170+
it('should override Dropdown.Item base component when using `as` prop', async () => {
171171
const user = userEvent.setup();
172172

173173
const CustomBaseItem = ({ children }: PropsWithChildren) => {

src/components/Dropdown/Dropdown.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import type {
1010
MutableRefObject,
1111
ReactElement,
1212
ReactNode,
13-
RefCallback,
1413
SetStateAction,
1514
} from 'react';
1615
import { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -98,13 +97,7 @@ const Trigger = ({
9897
{children}
9998
</button>
10099
) : (
101-
<Button
102-
{...buttonProps}
103-
disabled={disabled}
104-
type="button"
105-
ref={refs.setReference as RefCallback<'button'>}
106-
{...a11yProps}
107-
>
100+
<Button {...buttonProps} disabled={disabled} type="button" ref={refs.setReference} {...a11yProps}>
108101
{children}
109102
</Button>
110103
);

0 commit comments

Comments
 (0)