Skip to content

Commit

Permalink
refactor: polymorphic component (#211)
Browse files Browse the repository at this point in the history
* refactor: polymorphic component

* cleanup
  • Loading branch information
nlkluth authored Nov 9, 2023
1 parent e2730b0 commit 28f0271
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 54 deletions.
6 changes: 3 additions & 3 deletions packages/nextjs/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ export const Card = ({
<BaseCard
title={title}
subTitle={subTitle}
to={to}
price={price}
className={className}
imageContainerClass={containerClassName}
Link={Link}
as={Link}
href={to}
className={className}
>
<Image layout="fill" src={imageProps.src} alt={imageProps.alt} objectFit="cover" objectPosition="center" />
</BaseCard>
Expand Down
55 changes: 16 additions & 39 deletions packages/shared-ui/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -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<ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps> & {
as?: "button";
};
type CombinedProps<T extends React.ElementType> = PolymorphicComponentPropsWithRef<T, ButtonProps>;
type ButtonComponent = <C extends React.ElementType = "button">(props: CombinedProps<C>) => React.ReactElement | null;

type ButtonAsLink = BaseProps &
Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof BaseProps> & {
as: "a";
};

type ButtonProps = ButtonAsButton | ButtonAsLink;

function ButtonComponent(
{ as = "button", variant, disabled, className, leftIcon, rightIcon, ...props }: ButtonProps,
ref: ForwardedRef<HTMLButtonElement | HTMLAnchorElement>
) {
const ButtonComponent = <T extends React.ElementType = "button">(
{ as, variant, className, children, leftIcon, rightIcon, ...componentProps }: CombinedProps<T>,
ref: PolymorphicRef<T>
) => {
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",
{
Expand All @@ -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 (
<a ref={ref as ForwardedRef<HTMLAnchorElement>} className={styles} {...linkProps}>
{leftIcon && <div className="inline pr-2">{leftIcon}</div>}
{children}
{rightIcon && <div className="inline pl-2">{rightIcon}</div>}
</a>
);
}

const { children, ...buttonProps } = props as ButtonAsButton;

return (
<button ref={ref as ForwardedRef<HTMLButtonElement>} disabled={disabled} className={styles} {...buttonProps}>
<Component ref={ref} className={styles} {...componentProps}>
{leftIcon && <div className="inline pr-2">{leftIcon}</div>}
{children}
{rightIcon && <div className="inline pl-2">{rightIcon}</div>}
</button>
</Component>
);
}
};

export const Button = React.forwardRef(ButtonComponent);
export const Button: ButtonComponent = forwardRef(ButtonComponent);
27 changes: 15 additions & 12 deletions packages/shared-ui/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -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<AsComponent extends React.ElementType> = PolymorphicComponentProps<AsComponent, BaseProps>;

export const Card = <T extends React.ElementType>({
subTitle,
title,
price,
Link = "a",
className = "",
as,
imageContainerClass,
children,
}: React.PropsWithChildren<CardProps>) => {
...linkProps
}: React.PropsWithChildren<CardProps<T>>) => {
const RenderedLink = as || "a";

return (
<Link href={to} className={`flex flex-col justify-center text-primary group w-full ${className}`}>
<RenderedLink
{...linkProps}
className={`flex flex-col justify-center text-primary group w-full ${linkProps.className}`}
>
<span
className={classNames(
"rounded-xl group-hover:shadow-lg transition-shadow duration-150 overflow-hidden relative",
Expand All @@ -37,6 +40,6 @@ export const Card = ({
<h2 className="text-h5 font-medium mt-4 mb-1">{title}</h2>
{price && <span className="text-eyebrow font-bold">{currencyFormatter.format(price)}</span>}
{subTitle && <span className="text-eyebrow">{subTitle}</span>}
</Link>
</RenderedLink>
);
};
57 changes: 57 additions & 0 deletions packages/shared-ui/uitls/polymorphicComponent.ts
Original file line number Diff line number Diff line change
@@ -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<any>
> = JSX.LibraryManagedAttributes<C, React.ComponentPropsWithoutRef<C>>

type AsProp<C extends React.ElementType> = {
/**
* 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<ExtendedProps, keyof OverrideProps>

/**
* 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<PropsOf<C>, Props>

export type PolymorphicComponentProps<
C extends React.ElementType,
Props = {}
> = InheritableElementProps<C, Props & AsProp<C>>

/**
* Utility type to extract the `ref` prop from a polymorphic component
*/
export type PolymorphicRef<
C extends React.ElementType
>
= React.ComponentPropsWithRef<C>['ref']

/**
* A wrapper of `PolymorphicComponentProps` that also includes the `ref`
* prop for the polymorphic component
*/
export type PolymorphicComponentPropsWithRef<
C extends React.ElementType,
Props = {}
>
= PolymorphicComponentProps<C, Props> & { ref?: PolymorphicRef<C> }

1 comment on commit 28f0271

@vercel
Copy link

@vercel vercel bot commented on 28f0271 Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.