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, };