Skip to content

Commit

Permalink
fix(button): Fixed Typescript error when using Next.js Link in the bu…
Browse files Browse the repository at this point in the history
…tton `as` prop (themesberg#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 themesberg#1002 fix themesberg#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
  • Loading branch information
nigellima authored Jan 24, 2024
1 parent afc4c64 commit a6698d4
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 171 deletions.
1 change: 0 additions & 1 deletion examples/button/button.polymorph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ function Component() {
<Button as="span" className="cursor-pointer">
Span Button
</Button>
{/* @ts-expect-error TODO: fix `as` inference */}
<Button as={Link} href="#">
Next Link Button
</Button>
Expand Down
1 change: 0 additions & 1 deletion examples/dropdown/dropdown.customItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ function Component() {
function Component() {
return (
<Dropdown dismissOnClick={false} label="My custom item">
{/* @ts-expect-error TODO: fix `as` inference */}
<DropdownItem as={Link} href="#">
Home
</DropdownItem>
Expand Down
193 changes: 99 additions & 94 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { ComponentPropsWithoutRef, ElementType, ForwardedRef } from 'react';
import { type ReactNode } from 'react';
import type { ElementType } from 'react';
import { forwardRef, type ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
import genericForwardRef from '../../helpers/generic-forward-ref';
import { mergeDeep } from '../../helpers/merge-deep';
import { getTheme } from '../../theme-store';
import type { DeepPartial } from '../../types';
Expand All @@ -16,6 +15,7 @@ import { Spinner } from '../Spinner';
import { ButtonBase, type ButtonBaseProps } from './ButtonBase';
import type { PositionInButtonGroup } from './ButtonGroup';
import { ButtonGroup } from './ButtonGroup';
import type { PolymorphicComponentPropWithRef, PolymorphicRef } from '../../helpers/generic-as-prop';

export interface FlowbiteButtonTheme {
base: string;
Expand Down Expand Up @@ -67,105 +67,110 @@ export interface ButtonSizes extends Pick<FlowbiteSizes, 'xs' | 'sm' | 'lg' | 'x
[key: string]: string;
}

export type ButtonProps<T extends ElementType = 'button'> = {
as?: T | null;
href?: string;
color?: keyof FlowbiteColors;
fullSized?: boolean;
gradientDuoTone?: keyof ButtonGradientDuoToneColors;
gradientMonochrome?: keyof ButtonGradientColors;
target?: string;
isProcessing?: boolean;
processingLabel?: string;
processingSpinner?: ReactNode;
label?: ReactNode;
outline?: boolean;
pill?: boolean;
positionInGroup?: keyof PositionInButtonGroup;
size?: keyof ButtonSizes;
theme?: DeepPartial<FlowbiteButtonTheme>;
} & ComponentPropsWithoutRef<T>;

const ButtonComponentFn = <T extends ElementType = 'button'>(
export type ButtonProps<T extends ElementType = 'button'> = PolymorphicComponentPropWithRef<
T,
{
children,
className,
color = 'info',
disabled,
fullSized,
isProcessing = false,
processingLabel = 'Loading...',
processingSpinner,
gradientDuoTone,
gradientMonochrome,
label,
outline = false,
pill = false,
positionInGroup = 'none',
size = 'md',
theme: customTheme = {},
...props
}: ButtonProps<T>,
ref: ForwardedRef<T>,
) => {
const { buttonGroup: groupTheme, button: buttonTheme } = getTheme();
const theme = mergeDeep(buttonTheme, customTheme);
href?: string;
color?: keyof FlowbiteColors;
fullSized?: boolean;
gradientDuoTone?: keyof ButtonGradientDuoToneColors;
gradientMonochrome?: keyof ButtonGradientColors;
target?: string;
isProcessing?: boolean;
processingLabel?: string;
processingSpinner?: ReactNode;
label?: ReactNode;
outline?: boolean;
pill?: boolean;
positionInGroup?: keyof PositionInButtonGroup;
size?: keyof ButtonSizes;
theme?: DeepPartial<FlowbiteButtonTheme>;
}
>;

type ButtonComponentType = (<C extends React.ElementType = 'button'>(
props: ButtonProps<C>,
) => React.ReactNode | null) & { displayName?: string };

const ButtonComponentFn: ButtonComponentType = forwardRef(
<T extends ElementType = 'button'>(
{
children,
className,
color = 'info',
disabled,
fullSized,
isProcessing = false,
processingLabel = 'Loading...',
processingSpinner,
gradientDuoTone,
gradientMonochrome,
label,
outline = false,
pill = false,
positionInGroup = 'none',
size = 'md',
theme: customTheme = {},
...props
}: ButtonProps<T>,
ref: PolymorphicRef<T>,
) => {
const { buttonGroup: groupTheme, button: buttonTheme } = getTheme();
const theme = mergeDeep(buttonTheme, customTheme);

const theirProps = props as ButtonBaseProps<T>;
const theirProps = props as ButtonBaseProps<T>;

return (
<ButtonBase
ref={ref}
disabled={disabled}
className={twMerge(
theme.base,
disabled && theme.disabled,
!gradientDuoTone && !gradientMonochrome && theme.color[color],
gradientDuoTone && !gradientMonochrome && theme.gradientDuoTone[gradientDuoTone],
!gradientDuoTone && gradientMonochrome && theme.gradient[gradientMonochrome],
outline && (theme.outline.color[color] ?? theme.outline.color.default),
theme.pill[pill ? 'on' : 'off'],
fullSized && theme.fullSized,
groupTheme.position[positionInGroup],
className,
)}
{...theirProps}
>
<span
return (
<ButtonBase
ref={ref}
disabled={disabled}
className={twMerge(
theme.inner.base,
theme.outline[outline ? 'on' : 'off'],
theme.outline.pill[outline && pill ? 'on' : 'off'],
theme.size[size],
outline && !theme.outline.color[color] && theme.inner.outline,
isProcessing && theme.isProcessing,
isProcessing && theme.inner.isProcessingPadding[size],
theme.inner.position[positionInGroup],
theme.base,
disabled && theme.disabled,
!gradientDuoTone && !gradientMonochrome && theme.color[color],
gradientDuoTone && !gradientMonochrome && theme.gradientDuoTone[gradientDuoTone],
!gradientDuoTone && gradientMonochrome && theme.gradient[gradientMonochrome],
outline && (theme.outline.color[color] ?? theme.outline.color.default),
theme.pill[pill ? 'on' : 'off'],
fullSized && theme.fullSized,
groupTheme.position[positionInGroup],
className,
)}
{...theirProps}
>
<>
{isProcessing && (
<span className={twMerge(theme.spinnerSlot, theme.spinnerLeftPosition[size])}>
{processingSpinner || <Spinner size={size} />}
</span>
<span
className={twMerge(
theme.inner.base,
theme.outline[outline ? 'on' : 'off'],
theme.outline.pill[outline && pill ? 'on' : 'off'],
theme.size[size],
outline && !theme.outline.color[color] && theme.inner.outline,
isProcessing && theme.isProcessing,
isProcessing && theme.inner.isProcessingPadding[size],
theme.inner.position[positionInGroup],
)}
{typeof children !== 'undefined' ? (
children
) : (
<span data-testid="flowbite-button-label" className={twMerge(theme.label)}>
{isProcessing ? processingLabel : label}
</span>
)}
</>
</span>
</ButtonBase>
);
};
>
<>
{isProcessing && (
<span className={twMerge(theme.spinnerSlot, theme.spinnerLeftPosition[size])}>
{processingSpinner || <Spinner size={size} />}
</span>
)}
{typeof children !== 'undefined' ? (
children
) : (
<span data-testid="flowbite-button-label" className={twMerge(theme.label)}>
{isProcessing ? processingLabel : label}
</span>
)}
</>
</span>
</ButtonBase>
);
},
);

ButtonComponentFn.displayName = 'Button';

const ButtonComponent = genericForwardRef(ButtonComponentFn);

export const Button = Object.assign(ButtonComponent, {
export const Button = Object.assign(ButtonComponentFn, {
Group: ButtonGroup,
});
21 changes: 11 additions & 10 deletions src/components/Button/ButtonBase.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { createElement, type ComponentPropsWithoutRef, type ElementType, type ForwardedRef } from 'react';
import genericForwardRef from '../../helpers/generic-forward-ref';
import { createElement, type ComponentPropsWithoutRef, type ElementType, type ForwardedRef, forwardRef } from 'react';

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

const ButtonBaseComponent = <T extends ElementType = 'button'>(
{ children, as: Component, href, type = 'button', ...props }: ButtonBaseProps<T>,
ref: ForwardedRef<T>,
) => {
const BaseComponent = Component || (href ? 'a' : 'button');
export const ButtonBase = forwardRef(
<T extends ElementType = 'button'>(
{ children, as: Component, href, type = 'button', ...props }: ButtonBaseProps<T>,
ref: ForwardedRef<T>,
) => {
const BaseComponent = Component || (href ? 'a' : 'button');

return createElement(BaseComponent, { ref, href, type, ...props }, children);
};
return createElement(BaseComponent, { ref, href, type, ...props }, children);
},
);

export const ButtonBase = genericForwardRef(ButtonBaseComponent);
ButtonBase.displayName = 'ButtonBaseComponent';
2 changes: 1 addition & 1 deletion src/components/Dropdown/Dropdown.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ describe('Components / Dropdown', () => {
});

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

const CustomBaseItem = ({ children }: PropsWithChildren) => {
Expand Down
9 changes: 1 addition & 8 deletions src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type {
MutableRefObject,
ReactElement,
ReactNode,
RefCallback,
SetStateAction,
} from 'react';
import { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
Expand Down Expand Up @@ -98,13 +97,7 @@ const Trigger = ({
{children}
</button>
) : (
<Button
{...buttonProps}
disabled={disabled}
type="button"
ref={refs.setReference as RefCallback<'button'>}
{...a11yProps}
>
<Button {...buttonProps} disabled={disabled} type="button" ref={refs.setReference} {...a11yProps}>
{children}
</Button>
);
Expand Down
Loading

0 comments on commit a6698d4

Please sign in to comment.