Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: polymorphic component #211

Merged
merged 2 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
nlkluth marked this conversation as resolved.
Show resolved Hide resolved
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 {
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>
) {
export const ButtonComponent = <T extends React.ElementType = "button">(
{ as, variant, className, children, leftIcon, rightIcon, ...props }: CombinedProps<T>,
ref: PolymorphicRef<T>
nlkluth marked this conversation as resolved.
Show resolved Hide resolved
) => {
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": props.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} {...props}>
{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
scottrippey marked this conversation as resolved.
Show resolved Hide resolved
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/
scottrippey marked this conversation as resolved.
Show resolved Hide resolved

// 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> }
Loading