ycs77 / headlessui-float

Easily use Headless UI with Floating UI to position floating elements.
https://headlessui-float.vercel.app
MIT License
348 stars 13 forks source link

Vue refs not working inside `<Float>`? #105

Closed vincerubinetti closed 5 months ago

vincerubinetti commented 7 months ago

Versions "@headlessui-float/vue": "^0.13.2", "@headlessui/vue": "^1.7.19", "vue": "^3.4.21",

Describe the bug

Regular Vue refs to raw elements seem to break when inside the <Float> component.

To Reproduce

An example, trimmed down to just the relevant parts:

<template>
  <Combobox>
    <ComboboxLabel>{{ label }}</ComboboxLabel>
    <Float>
      <!-- if i move this button above <Float>, the ref works fine -->
      <button ref="button">test</button>
    </Float>
  </Combobox>
</template>

<script setup lang="ts">
const button = ref<HTMLButtonElement>();

// prints undefined
window.setInterval(() => console.log(button.value), 1000)
</script>
vincerubinetti commented 7 months ago

Nevermind, I restarted my dev server and the problem was gone, sorry.

xak2000 commented 6 months ago

I have the same problem. Restrting dev server is not helping. An element ref is always null when inside <Float> and works fine if moved outside <Float>.

xak2000 commented 6 months ago

I also tried to use the function ref syntax to check if the ref setter is ever called.

<script lang="ts" setup>
const outsideInputRef = ref<HTMLInputElement | null>(null)
function setOutsideInputRef(input: HTMLInputElement | null) {
  console.log('setOutsideInputRef', input)
  outsideInputRef.value = input
}

const floatRefInputRef = ref<HTMLInputElement | null>(null)
function setFloatRefInputRef(input: HTMLInputElement | null) {
  console.log('setFloatRefInputRef', input)
  floatRefInputRef.value = input
}
</script>

<template>
  <div>
    <input
      :ref="setOutsideInputRef"
      type="text"
      value="Outside input"
    />
    <Float>
      <input
        :ref="setFloatRefInputRef"
        type="text"
        value="Float ref input"
      />
      <div>Float div</div>
    </Float>
  </div>
</template>

When I tried it in a Vue playground on Stackblitz, it worked fine.

But when I tried it in the Nuxt playground (with @headlessui-float/nuxt module), the second setter (setFloatRefInputRef) is never called.

@ycs77 Could you please re-open this issue to investigate this problem?

here is the reproducible example.

xak2000 commented 5 months ago

A workaround:

Instead of:

    <Float>
      <input :ref="setFloatRefInputRef" type="text" value="Float ref input" />
      <div>Float div</div>
    </Float>

do this:

    <Float>
      <div>
        <input :ref="setFloatRefInputRef" type="text" value="Float ref input" />
      </div>
      <div>Float div</div>
    </Float>

I.e. wrap Float reference into div. This way :ref="setFloatRefInputRef" starts to work. setFloatRefInputRef is called 3-30 times for some reason, but it is better than 0 times. :)

vincerubinetti commented 5 months ago

I wonder if the as property could help you here? Like <Float as="template">.

Perhaps the <Float> component should pass down a ref slot prop that you can merge with your own ref (like you would in react, not actually sure that's necessary or possible in Vue).

xak2000 commented 5 months ago

I tried <Float as="template">. Unfortunately it doesn't help.

What I wonder is why it works fine in "pure Vue" environment, but doesn't work or behaves strange (if wrapped in div) in Nuxt environment.

I tested the "pure Vue" version and it works both wrapped in div and unwrapped. Moreover, even when wrapped then setFloatRefInputRef is still called only once.

In Nuxt sample wrapped version works, but setFloatRefInputRef is called 3 times on initial render and then 2 times more every time when the browser window is resized. In my local project setFloatRefInputRef is called 25-30 times on initial render. 😨

With "pure Vue" example even resizing the browser window doesn't lead to calling setFloatRefInputRef function again. It is called exactly 1 time: on the initial render of the referenced element.

Anyway, it looks like the problem appears only when headlessui-float is used with Nuxt, or at least I didn't able to reproduce the problem with pure Vue setup. But, unfortunately, I can't find the root of the problem. Tried to inspect the source code of the headlessui-float library, but it is too much to me at the moment. :) I'm too unfamiliar with the low level manipulation of component children that this library does.

At least, the problem is perfectly reproduced and I linked the example, so I hope @ycs77 will take a look when have a time.

ycs77 commented 5 months ago

Hi @xak2000

First is your pure Vue example missing the import { Float } from '@headlessui-float/vue' to import the <Float> component, after added you will see the Vue example has the same problem and Nuxt example. And the Nuxt will work with no import component because the Headless UI Float's Nuxt module will auto-import the component.

Then the solution is still this:

<Float>
  <div>
    <input :ref="setFloatRefInputRef" type="text" />
  </div>
  <div>Float div</div>
</Float>

When mentioning the reason, we need to explain the working principle of the <Float> component. The <Float> component gets the 2 child elements in the slot, the reference element and the floating element, and binds the ref of 2 child elements to bring the elements dom, then can pass the 2 child dom elements for Floating UI to positioning.

The Vue can't merge refs like React. Therefore, it cannot be re-bind externally when it has been bind once internally (in the <Float> component). So you will wrap an element like <div> for the reference element and the floating element, this will solve it.

xak2000 commented 5 months ago

Hi @ycs77

Thank you very much for the explanation. This does make sense.

Do you think this trait (inability to use ref on direct children of the Float) should be documented? I spent literaly 2 days scratching my head and experimenting (shame on me that I didn't try the easiest solution with a wrapper earlier).

Probably it will be good to left a note in the documentation with description of limitations (ref doesn't work, maybe something else too?). It would prevent long hours of head-scratching for other folks. :)

mreduar commented 4 months ago

Do you think this trait (inability to use ref on direct children of the Float) should be documented? I spent literaly 2 days scratching my head and experimenting (shame on me that I didn't try the easiest solution with a wrapper earlier).

Probably it will be good to left a note in the documentation with description of limitations (ref doesn't work, maybe something else too?). It would prevent long hours of head-scratching for other folks. :)

This would be great, and give an explanation of how to solve the real problem. I am trying to solve this same problem but on the floating element side. I place a ref ListboxOptions for example but when mounting the component always gives null I tried to place a div wrapping the ref as the solution that you propose but still gives null. So it seems that it does not apply the same solution with the floating element.

ycs77 commented 4 months ago

Do you think this trait (inability to use ref on direct children of the Float) should be documented? I spent literaly 2 days scratching my head and experimenting (shame on me that I didn't try the easiest solution with a wrapper earlier).

Probably it will be good to left a note in the documentation with description of limitations (ref doesn't work, maybe something else too?). It would prevent long hours of head-scratching for other folks. :)

@xak2000 I think it should be possible. I've added it https://headlessui-float.vercel.app/vue/render-wrapper.html#notice-of-template-ref

ycs77 commented 4 months ago

Hi @mreduar, if you has any problem, please open a new issue and provide enough information and a minimal reproducible example.