diff --git a/packages/radix-vue/src/Primitive/Primitive.test.ts b/packages/radix-vue/src/Primitive/Primitive.test.ts new file mode 100644 index 000000000..2180145a4 --- /dev/null +++ b/packages/radix-vue/src/Primitive/Primitive.test.ts @@ -0,0 +1,141 @@ +import { mount } from "@vue/test-utils"; +import { PrimitiveDiv } from "./"; +import { describe, expect, it } from "vitest"; + +describe("test Primitive functionalities", () => { + it("should render div element correctly", () => { + const wrapper = mount(PrimitiveDiv); + expect(wrapper.find("div").exists()).toBe(true); + }); + + it("should renders div element with custom attribute", () => { + const wrapper = mount(PrimitiveDiv, { + attrs: { + type: "button", + }, + }); + + const element = wrapper.find("div"); + + expect(element.attributes("type")).toBe("button"); + }); + + it("should renders multiple child elements", () => { + const wrapper = mount(PrimitiveDiv, { + slots: { + default: "
1
2
3
", + }, + }); + + const element = wrapper.find("div"); + expect(element.findAll("div").length).toBe(3); + }); + + // ref: https://vitest.dev/api/expect.html#tothrowerror + describe("asChild", () => { + it("should throw error when multiple child elements exists", () => { + const wrapper = () => + mount(PrimitiveDiv, { + props: { + asChild: true, + }, + slots: { + default: "
1
2
3
", + }, + }); + + expect(() => wrapper()).toThrowError(/invalid children/); + }); + + it("should merge child's class together", () => { + const wrapper = mount(PrimitiveDiv, { + props: { + asChild: true, + }, + attrs: { + class: "parent-class", + }, + slots: { + default: + '
Child class
', + }, + }); + + const element = wrapper.find("div"); + expect(element.attributes("class")).toBe( + "parent-class child-class more-child-class" + ); + }); + + it("should render the child class element tag", () => { + const wrapper = mount(PrimitiveDiv, { + props: { + asChild: true, + }, + + slots: { + default: "Child class", + }, + }); + + const element = wrapper.find("a"); + expect(element.exists()).toBeTruthy(); + }); + + it("should render the child component", () => { + const ChildComponent = { + template: '
Hello world
', + }; + const RootComponent = { + components: { ChildComponent, PrimitiveDiv }, + template: "", + }; + + const wrapper = mount(RootComponent, { + props: { + asChild: true, + }, + }); + + const element = wrapper.find("div"); + expect(element.html()).toBe('
Hello world
'); + }); + + it("should inherit parent attributes and the child attributes", () => { + const wrapper = mount(PrimitiveDiv, { + props: { + asChild: true, + }, + attrs: { + "data-parent-attr": "", + }, + slots: { + default: "
Child class
", + }, + }); + + const element = wrapper.find("div"); + expect(element.attributes("data-parent-attr")).toBe(""); + expect(element.attributes("data-child-attr")).toBe(""); + }); + + it("should replace parent attributes with child's attributes", () => { + const wrapper = mount(PrimitiveDiv, { + props: { + asChild: true, + }, + attrs: { + id: "parent", + "data-type": "button", + }, + slots: { + default: '
Child class
', + }, + }); + + const element = wrapper.find("div"); + expect(element.attributes("data-type")).toBe("primary"); + expect(element.attributes("id")).toBe("child"); + }); + }); +}); diff --git a/packages/radix-vue/src/Primitive/Primitive.vue b/packages/radix-vue/src/Primitive/Primitive.vue index e3356ab90..4ff1dc1c7 100644 --- a/packages/radix-vue/src/Primitive/Primitive.vue +++ b/packages/radix-vue/src/Primitive/Primitive.vue @@ -6,6 +6,7 @@ import { getCurrentInstance, mergeProps, cloneVNode, + type ComponentInternalInstance, } from "vue"; import { renderSlotFragments, isValidVNodeElement } from "@/shared"; @@ -28,8 +29,30 @@ const NODES = [ "ul", ] as const; +const throwError = (instance: ComponentInternalInstance | null) => { + const componentName = instance?.parent?.type.name + ? `<${instance.parent.type.name} />` + : "component"; + + throw new Error( + [ + `Detected an invalid children for \`${componentName}\` with \`asChild\` prop.`, + "", + "Note: All components accepting `asChild` expect only one direct child of valid VNode type.", + "You can apply a few solutions:", + [ + "Provide a single child element so that we can forward the props onto that element.", + "Ensure the first child is an actual element instead of a raw text node or comment node.", + ] + .map((line) => ` - ${line}`) + .join("\n"), + ].join("\n") + ); +}; + const createComponent = (node: (typeof NODES)[number]) => defineComponent({ + inheritAttrs: false, props: { asChild: { type: Boolean, @@ -54,28 +77,16 @@ const createComponent = (node: (typeof NODES)[number]) => if (Object.keys(attrs).length > 0) { const [firstChild, ...otherChildren] = children; if (!isValidVNodeElement(firstChild) || otherChildren.length > 0) { - const componentName = instance?.parent?.type.name - ? `<${instance.parent.type.name} />` - : "component"; - throw new Error( - [ - `Detected an invalid children for \`${componentName}\` with \`asChild\` prop.`, - "", - "Note: All components accepting `asChild` expect only one direct child of valid VNode type.", - "You can apply a few solutions:", - [ - "Provide a single child element so that we can forward the props onto that element.", - "Ensure the first child is an actual element instead of a raw text node or comment node.", - ] - .map((line) => ` - ${line}`) - .join("\n"), - ].join("\n") - ); + throwError(instance); } // remove props ref from being inferred delete firstChild.props?.ref; - const mergedProps = mergeProps(firstChild.props ?? {}, attrs); + + const mergedProps = mergeProps(attrs, firstChild.props ?? {}); + // remove class to prevent duplicated + delete firstChild.props?.class; + const cloned = cloneVNode(firstChild, mergedProps); // Explicitly override props starting with `on`. // It seems cloneVNode from Vue doesn't like overriding `onXXX` props. So @@ -87,6 +98,8 @@ const createComponent = (node: (typeof NODES)[number]) => } } return cloned; + } else if (Array.isArray(children) && children.length > 1) { + throwError(instance); } else if (Array.isArray(children) && children.length === 1) { // No props to inherit return children[0];