import { forwardRef } from "react";
import type * as React from "react";
import { hasOwn } from "@chance/utils";
// Most of the types and abstractions here are derived from types in Ariakit.
// https://github.com/ariakit/ariakit/
// MIT License Copyright (c) Diego Haz
// Creating a polymorphic component:
//
// const useCool = createComponentHook<"button", { customProp: number }>(
// (props) => {
// let { ref, customProp = "whatever", ...rest } = props;
// let ctx = {
// val1: true,
// val2: false,
// };
// // let myRef = useRef(null);
// return [
// {
// "data-custom": customProp,
// ref,
// ...rest,
// },
// ctx,
// ];
// }
// );
//
//
// This could be abstracted further to reduce the boilerplate a tiny bit, but I
// want it to still look like a React component so it's easier to read.
//
// const Cool = createPolymorphicComponent<"button", { customProp: number }>(
// (props) => {
// let { render, as: Comp = "button" } = props;
// let [domProps, Provider] = useCool(props);
// return typeof render === "function" ? (
// render(props)
// ) : (
//
//
//
// );
// }
// );
// Cool.displayName = "Cool";
export function createComponentHook<
DOMType extends As,
Props extends {} = {}
// Ctx extends never = never
>(
useProps: (
props: PolymorphicProps>
) => [DOMProps: HTMLProps>]
): ComponentHook>;
export function createComponentHook<
DOMType extends As,
Props extends {} = {},
Ctx = any
>(
useProps: (
props: PolymorphicProps>
) => [DOMProps: HTMLProps>, ContextType: Ctx]
): ComponentHookWithContext, Ctx>;
export function createComponentHook<
DOMType extends As,
Props extends {} = {},
Ctx = any
>(
useProps: (
props: PolymorphicProps>
) => [DOMProps: HTMLProps>, ContextType?: Ctx]
):
| ComponentHook>
| ComponentHookWithContext, Ctx> {
type AllProps = Props & AsProps;
type P = PolymorphicProps;
function useCompProps(props: P = {} as P) {
let [htmlProps, ctx] = useProps(props);
let copy = {} as typeof htmlProps;
for (let prop in htmlProps) {
if (hasOwn(htmlProps, prop) && htmlProps[prop] !== undefined) {
(copy as any)[prop] = htmlProps[prop] as any;
}
}
return [copy, ctx];
}
return useCompProps as any;
}
export function createPolymorphicComponent<
DOMType extends As,
Props extends {} = {}
>(
render: (
props: PolymorphicProps>
) => React.ReactNode | null
) {
return forwardRef(function Component(
props: PolymorphicProps>,
ref: React.Ref
) {
return render({ ref, ...props }) as React.ReactElement;
}) as unknown as PolymorphicComponent>;
}
// Love/hate the `as` prop, but when looking at other options for polymorphic
// components it still feels like the most natural API despite the headaches
// with TypeScript. Thankfully other folks smarter than me have solved the types
// so I don't have to (thanks again Diego for all of your great work!)
type As = React.ElementType;
type AsProps> = { as?: DOMType };
type RequiredAs> = NonNullable;
type HTMLProps> = {
children?: React.ReactNode;
render?: (
props: React.HTMLAttributes & React.RefAttributes
) => React.ReactNode;
[index: `data-${string}`]: unknown;
} & Omit<
React.ComponentPropsWithRef>,
keyof OwnProps | "children"
>;
type PolymorphicProps> = OwnProps &
HTMLProps;
type PolymorphicComponent> = {
>(
props: Omit &
Omit>, keyof Props> &
Required>
): JSX.Element | null;
(props: PolymorphicProps): JSX.Element | null;
readonly $$typeof: symbol;
displayName?: string;
/** don't use propTypes or defaultProps internally! */
};
type ComponentHook> = {
= RequiredAs>(
props?: Omit &
Omit>, keyof Props> &
AsProps
): [DOMProps: Omit>, "render" | "children">];
displayName?: string;
};
type ComponentHookWithContext, Ctx> = {
= RequiredAs>(
props?: Omit &
Omit>, keyof Props> &
AsProps
): [
DOMProps: Omit>, "render" | "children">,
ContextType: Ctx
];
displayName?: string;
};
export type {
As,
AsProps,
ComponentHook,
ComponentHookWithContext,
PolymorphicComponent,
};