diff --git a/examples/button/button.polymorph.tsx b/examples/button/button.polymorph.tsx index ea99ef348..3609ffff8 100644 --- a/examples/button/button.polymorph.tsx +++ b/examples/button/button.polymorph.tsx @@ -46,6 +46,7 @@ function Component() { + {/* @ts-expect-error TODO: fix `as` inference */} diff --git a/examples/dropdown/dropdown.customItem.tsx b/examples/dropdown/dropdown.customItem.tsx index d6fbc7aa0..96509097b 100644 --- a/examples/dropdown/dropdown.customItem.tsx +++ b/examples/dropdown/dropdown.customItem.tsx @@ -41,6 +41,7 @@ function Component() { function Component() { return ( + {/* @ts-expect-error TODO: fix `as` inference */} Home diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 53c672d12..1df740f29 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,6 +1,7 @@ -import type { ElementType } from 'react'; -import { forwardRef, type ReactNode } from 'react'; +import type { ComponentPropsWithoutRef, ElementType, ForwardedRef } from 'react'; +import { 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'; @@ -15,7 +16,6 @@ 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; @@ -67,110 +67,105 @@ export interface ButtonSizes extends Pick = PolymorphicComponentPropWithRef< - T, - { - 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; - } ->; - -type ButtonComponentType = (( - props: ButtonProps, -) => React.ReactNode | null) & { displayName?: string }; +export type ButtonProps = { + 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; +} & ComponentPropsWithoutRef; -const ButtonComponentFn: ButtonComponentType = forwardRef( - ( - { - 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, - ref: PolymorphicRef, - ) => { - const { buttonGroup: groupTheme, button: buttonTheme } = getTheme(); - const theme = mergeDeep(buttonTheme, customTheme); +const ButtonComponentFn = ( + { + 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, + ref: ForwardedRef, +) => { + const { buttonGroup: groupTheme, button: buttonTheme } = getTheme(); + const theme = mergeDeep(buttonTheme, customTheme); - const theirProps = props as ButtonBaseProps; + const theirProps = props as ButtonBaseProps; - return ( - + - + {isProcessing && ( + + {processingSpinner || } + )} - > - <> - {isProcessing && ( - - {processingSpinner || } - - )} - {typeof children !== 'undefined' ? ( - children - ) : ( - - {isProcessing ? processingLabel : label} - - )} - - - - ); - }, -); + {typeof children !== 'undefined' ? ( + children + ) : ( + + {isProcessing ? processingLabel : label} + + )} + + + + ); +}; ButtonComponentFn.displayName = 'Button'; -export const Button = Object.assign(ButtonComponentFn, { + +const ButtonComponent = genericForwardRef(ButtonComponentFn); + +export const Button = Object.assign(ButtonComponent, { Group: ButtonGroup, }); diff --git a/src/components/Button/ButtonBase.tsx b/src/components/Button/ButtonBase.tsx index 6bb5a97c1..42bd13799 100644 --- a/src/components/Button/ButtonBase.tsx +++ b/src/components/Button/ButtonBase.tsx @@ -1,19 +1,18 @@ -import { createElement, type ComponentPropsWithoutRef, type ElementType, type ForwardedRef, forwardRef } from 'react'; +import { createElement, type ComponentPropsWithoutRef, type ElementType, type ForwardedRef } from 'react'; +import genericForwardRef from '../../helpers/generic-forward-ref'; export type ButtonBaseProps = { as?: T; href?: string; } & ComponentPropsWithoutRef; -export const ButtonBase = forwardRef( - ( - { children, as: Component, href, type = 'button', ...props }: ButtonBaseProps, - ref: ForwardedRef, - ) => { - const BaseComponent = Component || (href ? 'a' : 'button'); +const ButtonBaseComponent = ( + { children, as: Component, href, type = 'button', ...props }: ButtonBaseProps, + ref: ForwardedRef, +) => { + const BaseComponent = Component || (href ? 'a' : 'button'); - return createElement(BaseComponent, { ref, href, type, ...props }, children); - }, -); + return createElement(BaseComponent, { ref, href, type, ...props }, children); +}; -ButtonBase.displayName = 'ButtonBaseComponent'; +export const ButtonBase = genericForwardRef(ButtonBaseComponent); diff --git a/src/components/Dropdown/Dropdown.spec.tsx b/src/components/Dropdown/Dropdown.spec.tsx index fec8846ae..c5f1895c8 100644 --- a/src/components/Dropdown/Dropdown.spec.tsx +++ b/src/components/Dropdown/Dropdown.spec.tsx @@ -167,7 +167,7 @@ describe('Components / Dropdown', () => { }); describe('Dropdown item render', async () => { - it('should override Dropdown.Item base component when using `as` prop', async () => { + it('should override Dropdownn.Item base component when using `as` prop', async () => { const user = userEvent.setup(); const CustomBaseItem = ({ children }: PropsWithChildren) => { diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 3b34ac8f1..8398785b1 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -10,6 +10,7 @@ import type { MutableRefObject, ReactElement, ReactNode, + RefCallback, SetStateAction, } from 'react'; import { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -97,7 +98,13 @@ const Trigger = ({ {children} ) : ( - ); diff --git a/src/components/Dropdown/DropdownItem.tsx b/src/components/Dropdown/DropdownItem.tsx index 2c36c3206..673304c62 100644 --- a/src/components/Dropdown/DropdownItem.tsx +++ b/src/components/Dropdown/DropdownItem.tsx @@ -1,13 +1,12 @@ 'use client'; -import { useListItem, useMergeRefs } from '@floating-ui/react'; -import { forwardRef, type ComponentProps, type ElementType, type FC, type RefCallback } from 'react'; +import { useListItem } from '@floating-ui/react'; +import type { ComponentProps, ComponentPropsWithoutRef, ElementType, FC, RefCallback } from 'react'; import { twMerge } from 'tailwind-merge'; import { mergeDeep } from '../../helpers/merge-deep'; import type { DeepPartial } from '../../types'; import { ButtonBase, type ButtonBaseProps } from '../Button/ButtonBase'; import { useDropdownContext } from './DropdownContext'; -import type { PolymorphicComponentPropWithRef, PolymorphicRef } from '~/src/helpers/generic-as-prop'; export interface FlowbiteDropdownItemTheme { container: string; @@ -15,52 +14,47 @@ export interface FlowbiteDropdownItemTheme { icon: string; } -export type DropdownItemProps = PolymorphicComponentPropWithRef< - T, - { - href?: string; - icon?: FC>; - onClick?: () => void; - theme?: DeepPartial; - } ->; +export type DropdownItemProps = { + // TODO: make it work with `Link` from Next.js + as?: T; + href?: string; + icon?: FC>; + onClick?: () => void; + theme?: DeepPartial; +} & ComponentPropsWithoutRef; -type DropdownItemComponentType = (( - props: DropdownItemProps, -) => React.ReactNode | null) & { displayName?: string }; +export const DropdownItem = ({ + children, + className, + icon: Icon, + onClick, + theme: customTheme = {}, + ...props +}: DropdownItemProps) => { + const { ref, index } = useListItem({ label: typeof children === 'string' ? children : undefined }); + const { theme: rootTheme, activeIndex, dismissOnClick, getItemProps, handleSelect } = useDropdownContext(); + const isActive = activeIndex === index; + const theme = mergeDeep(rootTheme.floating.item, customTheme); -export const DropdownItem: DropdownItemComponentType = forwardRef( - ( - { children, className, icon: Icon, onClick, theme: customTheme = {}, ...props }: DropdownItemProps, - forwardedRef: PolymorphicRef, - ) => { - const { ref: listItemRef, index } = useListItem({ label: typeof children === 'string' ? children : undefined }); - const ref = useMergeRefs([forwardedRef, listItemRef]); - const { theme: rootTheme, activeIndex, dismissOnClick, getItemProps, handleSelect } = useDropdownContext(); - const isActive = activeIndex === index; - const theme = mergeDeep(rootTheme.floating.item, customTheme); + const theirProps = props as ButtonBaseProps; - const theirProps = props as ButtonBaseProps; - - return ( -
  • - } - className={twMerge(theme.base, className)} - {...theirProps} - {...getItemProps({ - onClick: () => { - onClick && onClick(); - dismissOnClick && handleSelect(null); - }, - })} - tabIndex={isActive ? 0 : -1} - > - {Icon && } - {children} - -
  • - ); - }, -); -DropdownItem.displayName = 'DropdownItem'; + return ( +
  • + } + className={twMerge(theme.base, className)} + {...theirProps} + {...getItemProps({ + onClick: () => { + onClick && onClick(); + dismissOnClick && handleSelect(null); + }, + })} + tabIndex={isActive ? 0 : -1} + > + {Icon && } + {children} + +
  • + ); +}; diff --git a/src/helpers/generic-as-prop.ts b/src/helpers/generic-as-prop.ts deleted file mode 100644 index 53c65add8..000000000 --- a/src/helpers/generic-as-prop.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type React from 'react'; - -export type AsProp = { - as?: C | null; -}; - -export type PropsToOmit = keyof (AsProp & P); - -// eslint-disable-next-line @typescript-eslint/ban-types -export type PolymorphicComponentProp = React.PropsWithChildren< - Props & AsProp -> & - Omit, PropsToOmit>; - -// eslint-disable-next-line @typescript-eslint/ban-types -export type PolymorphicComponentPropWithRef = PolymorphicComponentProp< - C, - Props -> & { - ref?: PolymorphicRef; -}; - -export type PolymorphicRef = React.ComponentPropsWithRef['ref']; diff --git a/src/helpers/generic-forward-ref.ts b/src/helpers/generic-forward-ref.ts new file mode 100644 index 000000000..28460fa5e --- /dev/null +++ b/src/helpers/generic-forward-ref.ts @@ -0,0 +1,13 @@ +import type React from 'react'; +import { forwardRef } from 'react'; + +/** This allow the `forwardRef` to be used with generic components */ + +// eslint-disable-next-line @typescript-eslint/ban-types +type FixedForwardRef = ( + render: (props: P, ref: React.Ref) => JSX.Element, +) => (props: P & React.RefAttributes) => JSX.Element; + +const genericForwardRef = forwardRef as FixedForwardRef; + +export default genericForwardRef;