diff --git a/packages/core/src/polymorphic/polymorphic.test.tsx b/packages/core/src/polymorphic/polymorphic.test.tsx
index f5bdc133..56dd9892 100644
--- a/packages/core/src/polymorphic/polymorphic.test.tsx
+++ b/packages/core/src/polymorphic/polymorphic.test.tsx
@@ -6,459 +6,36 @@
* https://github.com/radix-ui/primitives/blob/b14ac1fff0cdaf45d1ea3e65c28c320ac0f743f2/packages/react/slot/src/Slot.test.tsx
*/
-import { fireEvent, render } from "@solidjs/testing-library";
-import { ComponentProps, JSX, splitProps } from "solid-js";
-import { vi } from "vitest";
+import { render } from "@solidjs/testing-library";
+import { expect } from "vitest";
-import { As, AsChildProp, Polymorphic } from "./polymorphic";
+import { Polymorphic } from "./polymorphic";
-type ButtonExampleProps = ComponentProps<"button"> & {
- leftIcon?: JSX.Element;
- rightIcon?: JSX.Element;
-};
+describe("Polymorphic", () => {
+ it("should render the 'as' string prop", () => {
+ const { getByTestId } = render(() => (
+
+ Button
+
+ ));
-function ButtonExample(props: ButtonExampleProps & AsChildProp) {
- const [local, others] = splitProps(props, [
- "leftIcon",
- "rightIcon",
- "children",
- ]);
+ const polymorphic = getByTestId("polymorphic");
- return (
-
- {local.leftIcon}
- {local.children}
- {local.rightIcon}
-
- );
-}
-
-// Skipped: error with vitest implementation
-describe.skip("Polymorphic", () => {
- describe("render", () => {
- it("should render the fallback if no 'asChild' prop", () => {
- const { getByTestId } = render(() => (
-
- Button
-
- ));
-
- const polymorphic = getByTestId("polymorphic");
-
- expect(polymorphic).toBeInstanceOf(HTMLButtonElement);
- });
-
- it("should render the component from 'As' when 'asChild' prop is true and the only direct child is 'As'", () => {
- const { getByTestId } = render(() => (
-
- Link
-
- ));
-
- const polymorphic = getByTestId("polymorphic");
-
- expect(polymorphic).toBeInstanceOf(HTMLAnchorElement);
- });
-
- it("should render the component from 'As' when 'asChild' prop is true and one of the direct children is 'As'", () => {
- const { getByTestId } = render(() => (
-
- before
- Link
- after
-
- ));
-
- const polymorphic = getByTestId("polymorphic");
-
- expect(polymorphic).toBeInstanceOf(HTMLAnchorElement);
- });
- });
-
- describe("style", () => {
- it("should apply Polymorphic string 'style' on child", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle("background-color:red");
- });
-
- it("should apply Polymorphic string 'style' on child when child's 'style' is undefined", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle("background-color:red");
- });
-
- it("should apply child's string 'style' on child", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle("background-color:red");
- });
-
- it("should apply child's string 'style' on child when Polymorphic 'style' is undefined", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle("background-color:red");
- });
-
- it("should apply both Polymorphic and child's string 'style' on child", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle(
- "background-color:red;color:white",
- );
- });
-
- it("support overriding same style attribute by child when using string 'sytle'", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle("background-color:blue");
- });
-
- it("should apply Polymorphic object 'style' on child", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle("background-color:red");
- });
-
- it("should apply Polymorphic object 'style' on child when child's style is undefined", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle("background-color:red");
- });
-
- it("should apply child's object 'style' on child", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle("background-color:red");
- });
-
- it("should apply child's object 'style' on child when Polymorphic 'style' is undefined", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle("background-color:red");
- });
-
- it("should apply both Polymorphic and child's object 'style' on child", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle(
- "background-color:red;color:white",
- );
- });
-
- it("support overriding same style attribute by child when using object 'sytle'", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle("background-color:blue");
- });
-
- it("support mixing object and string 'style'", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveStyle(
- "background-color:blue;padding:14px;font-size:18px",
- );
- });
+ expect(polymorphic).toBeInstanceOf(HTMLDivElement);
});
- describe("class", () => {
- it("should apply Polymorphic 'class' on child", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveClass("foo");
- });
-
- it("should apply Polymorphic 'class' on child when child's 'class' is undefined", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveClass("foo");
- });
-
- it("should apply child's 'class' on child", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveClass("foo");
- });
-
- it("should apply child's 'class' on child when Polymorphic 'class' is undefined", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- expect(getByRole("button")).toHaveClass("foo");
- });
-
- it("should apply both Polymorphic and child's 'class' on child", () => {
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- const button = getByRole("button");
-
- expect(button).toHaveClass("foo");
- expect(button).toHaveClass("bar");
- });
- });
-
- describe("handlers", () => {
- it("should call the 'onClick' passed to the Polymorphic", () => {
- const onPolymorphicClickSpy = vi.fn();
-
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- fireEvent.click(getByRole("button"));
-
- expect(onPolymorphicClickSpy).toBeCalledTimes(1);
- });
-
- it("should call the child's 'onClick'", () => {
- const onChildClickSpy = vi.fn();
-
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- fireEvent.click(getByRole("button"));
-
- expect(onChildClickSpy).toBeCalledTimes(1);
- });
-
- it("should call both the Polymorphic and child's 'onClick' when provided", () => {
- const onPolymorphicClickSpy = vi.fn();
- const onChildClickSpy = vi.fn();
-
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- fireEvent.click(getByRole("button"));
-
- expect(onChildClickSpy).toBeCalledTimes(1);
- expect(onPolymorphicClickSpy).toBeCalledTimes(1);
- });
-
- it("should call the Polymorphic 'onClick' even if child's 'onClick' is undefined", () => {
- const onPolymorphicClickSpy = vi.fn();
-
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- fireEvent.click(getByRole("button"));
-
- expect(onPolymorphicClickSpy).toBeCalledTimes(1);
- });
-
- it("should call the child's 'onClick' even if Polymorphic 'onClick' is undefined", () => {
- const onChildClickSpy = vi.fn();
-
- const { getByRole } = render(() => (
-
-
- Click me
-
-
- ));
-
- fireEvent.click(getByRole("button"));
-
- expect(onChildClickSpy).toBeCalledTimes(1);
- });
- });
-
- describe("With slottable content", () => {
- it("should render a button with icon on the left/right when no 'asChild' prop", () => {
- const { getByRole } = render(() => (
- left}
- rightIcon={right}
- >
- Button text
-
- ));
-
- const button = getByRole("button");
-
- expect(button).toBeInstanceOf(HTMLButtonElement);
- expect(button).toContainHTML(
- "leftButton textright",
- );
- });
+ it("should render the 'as' custom component prop", () => {
+ const CustomButton = (props: any) => ;
- it("should render a link with icon on the left/right when 'asChild' prop is true and content is 'As'", () => {
- const { getByRole } = render(() => (
- left}
- rightIcon={right}
- asChild
- >
-
- Button text
-
-
- ));
+ const { getByTestId } = render(() => (
+
+ Button
+
+ ));
- const link = getByRole("link");
+ const polymorphic = getByTestId("polymorphic");
- expect(link).toBeInstanceOf(HTMLAnchorElement);
- expect(link).toHaveAttribute("href", "https://kobalte.dev");
- expect(link).toContainHTML(
- "leftButton textright",
- );
- });
+ expect(polymorphic).toBeInstanceOf(HTMLButtonElement);
+ expect(polymorphic).toHaveAttribute("id", "custom");
});
});
diff --git a/packages/core/src/polymorphic/polymorphic.tsx b/packages/core/src/polymorphic/polymorphic.tsx
index bf185a32..f0bb6952 100644
--- a/packages/core/src/polymorphic/polymorphic.tsx
+++ b/packages/core/src/polymorphic/polymorphic.tsx
@@ -1,127 +1,53 @@
/* @refresh reload */
-/*!
- * Portions of this file are based on code from radix-ui-primitives.
- * MIT Licensed, Copyright (c) 2022 WorkOS.
- *
- * Credits to the Radix UI team:
- * https://github.com/radix-ui/primitives/blob/b14ac1fff0cdaf45d1ea3e65c28c320ac0f743f2/packages/react/slot/src/Slot.tsx
- */
-
-import { combineProps as baseCombineProps, isArray } from "@kobalte/utils";
-import {
- Accessor,
- ComponentProps,
- For,
- JSX,
- Show,
- ValidComponent,
- children,
- splitProps,
-} from "solid-js";
-import { Dynamic, DynamicProps } from "solid-js/web";
+import { ComponentProps, JSX, ValidComponent, splitProps } from "solid-js";
+import { Dynamic } from "solid-js/web";
/* -------------------------------------------------------------------------------------------------
* Polymorphic
* -----------------------------------------------------------------------------------------------*/
-export interface AsChildProp {
- /** Whether the component should render as its direct `As` child component. */
- asChild?: boolean;
+// Combine T and P by overriding T
+export type OverrideProps = Omit & P;
- /** The component to render when `children` doesn't contain any `` component as direct child. */
- as?: ValidComponent;
+/**
+ * Polymorphic attribute.
+ */
+export interface PolymorphicAttributes {
+ as?: T | keyof JSX.HTMLElementTags;
}
+/**
+ * Props used by a polymorphic component.
+ */
export type PolymorphicProps<
T extends ValidComponent,
- P = ComponentProps,
-> = {
- [K in keyof P]: P[K];
-} & AsChildProp;
+ Props extends {} = {},
+> = OverrideProps<
+ ComponentProps, // Override props from custom/tag component with our own
+ Props & // Accept custom props of our own component
+ PolymorphicAttributes
+>;
/**
- * A utility component that render either a direct `` child or its `as` prop.
+ * Helper type to get the exact props in Polymnorphic `as` callback.
*/
-export function Polymorphic(
- props: PolymorphicProps,
-) {
- const [local, others] = splitProps(
- props as PolymorphicProps,
- ["asChild", "as", "children"],
- );
-
- // Prevent the extra computation below when "as child" polymorphism is not needed.
- if (!local.asChild) {
- return (
-
- {local.children}
-
- );
- }
-
- const resolvedChildren = children(() => local.children) as Accessor;
-
- // Single child is `As`.
- if (isAs(resolvedChildren())) {
- const combinedProps = combineProps(others, resolvedChildren()?.props ?? {});
- return ;
- }
-
- // Multiple children, find an `As` if any.
- if (isArray(resolvedChildren())) {
- const newElement = resolvedChildren().find(isAs);
-
- if (newElement) {
- // because the new element will be the one rendered, we are only interested
- // in grabbing its children (`newElement.props.children`)
- const newChildren = () => (
-
- {(child: any) => (
-
- {newElement.props.children}
-
- )}
-
- );
-
- const combinedProps = combineProps(others, newElement?.props ?? {});
-
- return {newChildren};
- }
- }
-
- throw new Error(
- "[kobalte]: Component is expected to render `asChild` but no children `As` component was found.",
- );
-}
-
-/* -------------------------------------------------------------------------------------------------
- * As
- * -----------------------------------------------------------------------------------------------*/
-
-const AS_COMPONENT_SYMBOL = Symbol("$$KobalteAsComponent");
+export type PolymorphicCallbackProps<
+ CustomProps extends {},
+ BaseProps extends {},
+ RenderProps extends {},
+> = Omit & RenderProps;
/**
- * A utility component used to delegate rendering of its `Polymorphic` parent component.
+ * A utility component that render its `as` prop.
*/
-export function As(props: DynamicProps) {
- return {
- [AS_COMPONENT_SYMBOL]: true,
- props,
- } as unknown as JSX.Element;
-}
-
-/* -------------------------------------------------------------------------------------------------
- * Utils
- * -----------------------------------------------------------------------------------------------*/
-
-function isAs(component: any): boolean {
- return component?.[AS_COMPONENT_SYMBOL] === true;
-}
-
-function combineProps(baseProps: any, overrideProps: any) {
- return baseCombineProps([baseProps, overrideProps], {
- reverseEventHandlers: true,
- }) as any;
+export function Polymorphic(
+ props: RenderProps & PolymorphicAttributes,
+): JSX.Element {
+ const [local, others] = splitProps(props, ["as"]);
+
+ return (
+ // @ts-ignore: Props are valid but not worth calculating
+
+ );
}