From c052f22cf9425b1b6f8dac5613d6406bc89c04de Mon Sep 17 00:00:00 2001
From: Felissi <112175009+felissi@users.noreply.github.com>
Date: Wed, 19 Feb 2025 13:25:03 +0800
Subject: [PATCH] fix(useForwardExpose): forward native element (#1632)
---
.../src/shared/useForwardExpose.test.ts | 165 ++++++++++++++++++
.../radix-vue/src/shared/useForwardExpose.ts | 4 +-
2 files changed, 167 insertions(+), 2 deletions(-)
create mode 100644 packages/radix-vue/src/shared/useForwardExpose.test.ts
diff --git a/packages/radix-vue/src/shared/useForwardExpose.test.ts b/packages/radix-vue/src/shared/useForwardExpose.test.ts
new file mode 100644
index 000000000..423cb7986
--- /dev/null
+++ b/packages/radix-vue/src/shared/useForwardExpose.test.ts
@@ -0,0 +1,165 @@
+import { flushPromises, mount } from '@vue/test-utils'
+import { describe, expect, it } from 'vitest'
+import { Fragment, computed, defineComponent, h, ref } from 'vue'
+
+import { useForwardExpose } from './useForwardExpose'
+
+describe('useForwardRef', async () => {
+ it('should forward plain DOM element ref', async () => {
+ const comp = defineComponent({
+ setup() {
+ const { forwardRef } = useForwardExpose()
+ return { forwardRef }
+ },
+ template: `
+
+ inner element
+
+ `,
+ })
+ const wrapper = mount(
+ defineComponent(() => {
+ const el = ref()
+ const forwardedRef = computed(() => el.value?.$el)
+ return () =>
+ h('div', { test: forwardedRef.value }, [h(comp, { ref: el })])
+ }),
+ )
+ await flushPromises()
+ expect(wrapper.attributes('test')).toBe('[object HTMLSpanElement]')
+ })
+ it('should forward plain DOM element ref - 2', async () => {
+ const comp = defineComponent({
+ setup() {
+ const { forwardRef } = useForwardExpose()
+ return { forwardRef }
+ },
+ template: `
+
+ inner element
+
+ `,
+ })
+ const wrapper = mount(
+ defineComponent(() => {
+ const el = ref()
+ const forwardedRef = computed(() => el.value?.$el)
+ return () =>
+ h('div', { test: forwardedRef.value }, [h(comp, { ref: el })])
+ }),
+ )
+ await flushPromises()
+ expect(wrapper.attributes('test')).toBe('[object HTMLSpanElement]')
+ })
+ it('should forward plain DOM element ref - fragment', async () => {
+ const comp = defineComponent({
+ setup() {
+ const { forwardRef } = useForwardExpose()
+ return { forwardRef }
+ },
+ template: `
+ multiple node root
+
+ inner element
+
+ `,
+ })
+ const wrapper = mount(
+ defineComponent(() => {
+ const el = ref()
+ const forwardedRef = computed(() => el.value?.$el)
+ return () =>
+ h('div', { test: forwardedRef.value }, [h(comp, { ref: el })])
+ }),
+ )
+ await flushPromises()
+ expect(wrapper.attributes('test')).toBe('[object HTMLSpanElement]')
+ })
+ it('should forward plain DOM element ref - fragment - 2', async () => {
+ const Frag = defineComponent(
+ (props, { slots }) =>
+ () =>
+ h(Fragment, {}, [slots.default?.()]),
+ )
+ const comp = defineComponent({
+ components: { Frag },
+ setup() {
+ const { forwardRef } = useForwardExpose()
+ return { forwardRef }
+ },
+ template: `
+
+
+
+ inner element
+
+
+
+ `,
+ })
+ const wrapper = mount(
+ defineComponent(() => {
+ const el = ref()
+ const forwardedRef = computed(() => el.value?.$el)
+ return () =>
+ h('div', { test: forwardedRef.value }, [h(comp, { ref: el })])
+ }),
+ )
+ await flushPromises()
+ expect(wrapper.attributes('test')).toBe('[object HTMLSpanElement]')
+ })
+ it('should forward plain DOM element ref - fragment - 3', async () => {
+ const comp = defineComponent({
+ setup() {
+ const { forwardRef } = useForwardExpose()
+ return { forwardRef }
+ },
+ template: `
+
+
+
+ inner element
+
+
+
+ `,
+ })
+ const wrapper = mount(
+ defineComponent(() => {
+ const el = ref()
+ const forwardedRef = computed(() => el.value?.$el)
+ return () =>
+ h('div', { test: forwardedRef.value }, [h(comp, { ref: el })])
+ }),
+ )
+ await flushPromises()
+ expect(wrapper.attributes('test')).toBe('[object HTMLSpanElement]')
+ })
+ it('should forward component instance for component', async () => {
+ const InnerComp = defineComponent(() => {
+ return () => h('span', {}, 'inner component')
+ })
+ const comp = defineComponent({
+ setup() {
+ const { forwardRef } = useForwardExpose()
+ return { forwardRef }
+ },
+ components: { InnerComp },
+ template: `
+
+
+
+ `,
+ })
+ const wrapper = mount(
+ defineComponent(() => {
+ const el = ref()
+ const forwardedRef = computed(() => el.value?.$el)
+ return () =>
+ h('div', { test: forwardedRef.value }, [h(comp, { ref: el })])
+ }),
+ )
+ await flushPromises()
+ expect(wrapper.attributes('test')).toBe('[object HTMLSpanElement]')
+ })
+})
diff --git a/packages/radix-vue/src/shared/useForwardExpose.ts b/packages/radix-vue/src/shared/useForwardExpose.ts
index 44b14f563..e6cd48884 100644
--- a/packages/radix-vue/src/shared/useForwardExpose.ts
+++ b/packages/radix-vue/src/shared/useForwardExpose.ts
@@ -48,14 +48,14 @@ export function useForwardExpose() {
function forwardRef(ref: Element | ComponentPublicInstance | null) {
currentRef.value = ref
- if (ref instanceof Element || !ref)
+ if (!ref)
return
// retrieve the forwarded element
Object.defineProperty(ret, '$el', {
enumerable: true,
configurable: true,
- get: () => ref.$el,
+ get: () => (ref instanceof Element ? ref : ref.$el),
})
instance.exposed = ret