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];