From 28f0271f25b4f6168a1d6cf8fa4af35d356e5bbb Mon Sep 17 00:00:00 2001 From: Nathan Kluth Date: Thu, 9 Nov 2023 12:42:51 -0700 Subject: [PATCH] refactor: polymorphic component (#211) * refactor: polymorphic component * cleanup --- packages/nextjs/components/Card.tsx | 6 +- .../shared-ui/components/Button/Button.tsx | 55 ++++++------------ packages/shared-ui/components/Card/Card.tsx | 27 +++++---- .../shared-ui/uitls/polymorphicComponent.ts | 57 +++++++++++++++++++ 4 files changed, 91 insertions(+), 54 deletions(-) create mode 100644 packages/shared-ui/uitls/polymorphicComponent.ts diff --git a/packages/nextjs/components/Card.tsx b/packages/nextjs/components/Card.tsx index e8236b4a..52161acf 100644 --- a/packages/nextjs/components/Card.tsx +++ b/packages/nextjs/components/Card.tsx @@ -29,11 +29,11 @@ export const Card = ({ {imageProps.alt} diff --git a/packages/shared-ui/components/Button/Button.tsx b/packages/shared-ui/components/Button/Button.tsx index c818c13a..fb80ac4f 100644 --- a/packages/shared-ui/components/Button/Button.tsx +++ b/packages/shared-ui/components/Button/Button.tsx @@ -1,31 +1,22 @@ -import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ForwardedRef } from "react"; -import * as React from "react"; import classNames from "classnames"; +import { forwardRef } from "react"; +import { PolymorphicComponentPropsWithRef, PolymorphicRef } from "../../uitls/polymorphicComponent"; -interface BaseProps { +export interface ButtonProps { + children?: React.ReactNode; variant: "primary" | "secondary" | "tertiary" | "text"; - disabled?: boolean; - children: React.ReactNode; leftIcon?: React.ReactNode; rightIcon?: React.ReactNode; } -type ButtonAsButton = BaseProps & - Omit, keyof BaseProps> & { - as?: "button"; - }; +type CombinedProps = PolymorphicComponentPropsWithRef; +type ButtonComponent = (props: CombinedProps) => React.ReactElement | null; -type ButtonAsLink = BaseProps & - Omit, keyof BaseProps> & { - as: "a"; - }; - -type ButtonProps = ButtonAsButton | ButtonAsLink; - -function ButtonComponent( - { as = "button", variant, disabled, className, leftIcon, rightIcon, ...props }: ButtonProps, - ref: ForwardedRef -) { +const ButtonComponent = ( + { as, variant, className, children, leftIcon, rightIcon, ...componentProps }: CombinedProps, + ref: PolymorphicRef +) => { + const Component = as || "button"; const styles = classNames( "inline-flex items-center rounded-lg py-4 px-8 text-body-reg transition transition-colors duration-150 text-center", { @@ -34,33 +25,19 @@ function ButtonComponent( "bg-[transparent] text-primary border border-[transparent] hover:border-primary": variant === "text", "bg-[transparent] text-secondary border-secondary border hover:bg-secondary hover:text-primary": variant === "tertiary", - "bg-thunder-cloud text-dark-thunder-cloud hover:text-dark-thunder-cloud": disabled, + "bg-thunder-cloud text-dark-thunder-cloud hover:text-dark-thunder-cloud": componentProps.disabled, "cursor-pointer": as === "a", }, className ); - if (as === "a") { - const { children, ...linkProps } = props as ButtonAsLink; - - return ( - } className={styles} {...linkProps}> - {leftIcon &&
{leftIcon}
} - {children} - {rightIcon &&
{rightIcon}
} -
- ); - } - - const { children, ...buttonProps } = props as ButtonAsButton; - return ( - + ); -} +}; -export const Button = React.forwardRef(ButtonComponent); +export const Button: ButtonComponent = forwardRef(ButtonComponent); diff --git a/packages/shared-ui/components/Card/Card.tsx b/packages/shared-ui/components/Card/Card.tsx index b0710198..401245d3 100644 --- a/packages/shared-ui/components/Card/Card.tsx +++ b/packages/shared-ui/components/Card/Card.tsx @@ -1,31 +1,34 @@ import classNames from "classnames"; import { currencyFormatter } from "../../utils/currencyFormatter"; -import { UrlObject } from "url"; +import { PolymorphicComponentProps } from "../../uitls/polymorphicComponent"; -export interface CardProps { +export interface BaseProps { title: string; price?: number; - Link?: React.ElementType; Image?: React.ElementType; subTitle?: string; - to: string | UrlObject; - className?: string; imageContainerClass?: string; } -export const Card = ({ - to, +type CardProps = PolymorphicComponentProps; + +export const Card = ({ subTitle, title, price, - Link = "a", - className = "", + as, imageContainerClass, children, -}: React.PropsWithChildren) => { + ...linkProps +}: React.PropsWithChildren>) => { + const RenderedLink = as || "a"; + return ( - + {title} {price && {currencyFormatter.format(price)}} {subTitle && {subTitle}} - + ); }; diff --git a/packages/shared-ui/uitls/polymorphicComponent.ts b/packages/shared-ui/uitls/polymorphicComponent.ts new file mode 100644 index 00000000..c13059ac --- /dev/null +++ b/packages/shared-ui/uitls/polymorphicComponent.ts @@ -0,0 +1,57 @@ +// Taken from: https://www.benmvp.com/blog/forwarding-refs-polymorphic-react-component-typescript/ + +// A more precise version of just React.ComponentPropsWithoutRef on its own +export type PropsOf< + C extends keyof JSX.IntrinsicElements | React.JSXElementConstructor +> = JSX.LibraryManagedAttributes> + +type AsProp = { + /** + * An override of the default HTML tag. + * Can also be another React component. + */ + as?: C +} + +/** + * Allows for extending a set of props (`ExtendedProps`) by an overriding set of props + * (`OverrideProps`), ensuring that any duplicates are overridden by the overriding + * set of props. + */ +export type ExtendableProps< + ExtendedProps = {}, + OverrideProps = {} +> = OverrideProps & Omit + +/** + * Allows for inheriting the props from the specified element type so that + * props like children, className & style work, as well as element-specific + * attributes like aria roles. The component (`C`) must be passed in. + */ +export type InheritableElementProps< + C extends React.ElementType, + Props = {} +> = ExtendableProps, Props> + +export type PolymorphicComponentProps< + C extends React.ElementType, + Props = {} +> = InheritableElementProps> + +/** + * Utility type to extract the `ref` prop from a polymorphic component + */ +export type PolymorphicRef< + C extends React.ElementType +> + = React.ComponentPropsWithRef['ref'] + +/** + * A wrapper of `PolymorphicComponentProps` that also includes the `ref` + * prop for the polymorphic component + */ +export type PolymorphicComponentPropsWithRef< + C extends React.ElementType, + Props = {} +> + = PolymorphicComponentProps & { ref?: PolymorphicRef } \ No newline at end of file