From 27c4ab0470f65a1b21e00717781484ee83676ef3 Mon Sep 17 00:00:00 2001 From: p-sw Date: Fri, 24 May 2024 22:03:16 +0900 Subject: [PATCH] feat: add slot component in shared --- packages/react/{shared.ts => shared.tsx} | 86 ++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 6 deletions(-) rename packages/react/{shared.ts => shared.tsx} (68%) diff --git a/packages/react/shared.ts b/packages/react/shared.tsx similarity index 68% rename from packages/react/shared.ts rename to packages/react/shared.tsx index c889512..d8b05b8 100644 --- a/packages/react/shared.ts +++ b/packages/react/shared.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { twMerge } from "tailwind-merge"; /** @@ -88,7 +89,10 @@ type PresetType = { * @returns function (variantProps) -> class name, * @returns function (anyProps) -> [variantProps, otherProps] */ -export function vcn({ +export function vcn< + V extends VariantType, + N extends string /* Preset names */, +>({ base, variants, defaults, @@ -106,7 +110,7 @@ export function vcn( } ) => string, >( - anyProps: AnyPropBeforeResolve, + anyProps: AnyPropBeforeResolve ) => [ Partial>, Omit< @@ -158,9 +162,7 @@ export function vcn( return Object.entries(anyProps).reduce( ([variantProps, otherProps], [key, value]) => { - if ( - variantKeys.includes(key) - ) { + if (variantKeys.includes(key)) { return [{ ...variantProps, [key]: value }, otherProps]; } return [variantProps, { ...otherProps, [key]: value }]; @@ -168,7 +170,10 @@ export function vcn( [{}, {}] ) as [ Partial>, - Omit> | "preset" | "className">, + Omit< + typeof anyProps, + keyof Partial> | "preset" | "className" + >, ]; }, ]; @@ -193,3 +198,72 @@ export type VariantProps string> = F extends ( ) => string ? P : never; + +function mergeReactProps( + parentProps: Record, + childProps: Record +) { + const overrideProps = { ...childProps }; + + for (const propName in childProps) { + const parentPropValue = parentProps[propName]; + const childPropValue = childProps[propName]; + + const isHandler = /^on[A-Z]/.test(propName); + if (isHandler) { + if (childPropValue && parentPropValue) { + overrideProps[propName] = (...args: unknown[]) => { + childPropValue?.(...args); + parentPropValue?.(...args); + }; + } else if (parentPropValue) { + overrideProps[propName] = parentPropValue; + } + } else if (propName === "style") { + overrideProps[propName] = { ...parentPropValue, ...childPropValue }; + } else if (propName === "className") { + overrideProps[propName] = twMerge(parentPropValue, childPropValue); + } + } + + return { ...parentProps, ...overrideProps }; +} + +function combinedRef(refs: React.Ref[]) { + return (instance: I | null) => + refs.forEach((ref) => { + if (ref instanceof Function) { + ref(instance); + } else if (!!ref) { + (ref as React.MutableRefObject).current = instance; + } + }); +} + +interface SlotProps { + children?: Exclude> | string; +} +export const Slot = React.forwardRef((props, ref) => { + const { children, ...slotProps } = props; + if (!React.isValidElement(children)) { + return null; + } + return React.cloneElement(children, { + ...mergeReactProps(slotProps, children.props), + ref: combinedRef([ref, (children as any).ref]), + } as any); +}); + +export interface MustAsChild { + children: React.ReactElement< + unknown, + string | React.JSXElementConstructor + >; +} + +export interface OptionalAsChild { + children?: T extends true + ? React.ReactElement> + : React.ReactNode; + asChild?: T; +}