Skip to content

Commit

Permalink
fix(button): correctly infer type from as prop
Browse files Browse the repository at this point in the history
`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
  • Loading branch information
nigellima committed Jan 23, 2024
1 parent afc4c64 commit 38bd24f
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 107 deletions.
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,
});
23 changes: 23 additions & 0 deletions src/helpers/generic-as-prop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type React from 'react';

export type AsProp<C extends React.ElementType> = {
as?: C | null;
};

export type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

// eslint-disable-next-line @typescript-eslint/ban-types
export type PolymorphicComponentProp<C extends React.ElementType, Props = {}> = React.PropsWithChildren<
Props & AsProp<C>
> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;

// eslint-disable-next-line @typescript-eslint/ban-types
export type PolymorphicComponentPropWithRef<C extends React.ElementType, Props = {}> = PolymorphicComponentProp<
C,
Props
> & {
ref?: PolymorphicRef<C>;
};

export type PolymorphicRef<C extends React.ElementType> = React.ComponentPropsWithRef<C>['ref'];
13 changes: 0 additions & 13 deletions src/helpers/generic-forward-ref.ts

This file was deleted.

0 comments on commit 38bd24f

Please sign in to comment.