tailwindlabs / headlessui

Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.
https://headlessui.com
MIT License
25.81k stars 1.07k forks source link

[Dialog/Transitions] Enter transitions no longer happening #3456

Open blackjak231 opened 3 weeks ago

blackjak231 commented 3 weeks ago

What package within Headless UI are you using?

@headlessui/vue

What version of that package are you using?

1.7.22

What browser are you using?

Chrome and Safari (mobile versions as well)

Reproduction URL

Codepen example here

Describe your issue

After upgrading to vue 3.5.x from 3.4.38, there are no longer any transition animations on enter. Well, instead of saying "no animations" it seems to be related to the transition duration (from very random tests I've done).

In the codepen provided, sometimes, for no apparent reason, the dialog will show just fine, but 99% of the time, there are no enter transition (or an extremely brief one).

From what I can see, Vue has changed some behaviour with the native transition and teleport. Maybe it has something to do with it ? https://github.com/vuejs/core/blob/main/CHANGELOG.md

Only solution for now : downgrade to vue 3.4.x.

Hopefully you can find and fix this issue !

edeustua commented 3 weeks ago

+1

Also, the transition seems to be working fine with 1.7.16 plus vue 3.5.3. From 1.7.17 on, the enter transition breaks.

shengslogar commented 2 weeks ago

seems to be working fine with 1.7.16 plus vue 3.5.3

Also able to repro this on my end. Otherwise, 1.7.23 (latest at writing) + locking Vue to 3.4.38 does the trick as already stated.

There have been a number of transition-related changes in Vue core over the past month. Not sure where to start on this. https://github.com/vuejs/core/issues?q=transition+created%3A%3E%3D2024-08-01

shengslogar commented 2 weeks ago

GH diff link won't jump to file automatically, but was able to isolate this issue to changes made to packages/@headlessui-vue/src/components/portal/portal.ts in @headlessui/vue@v1.7.17 by pinning v1.7.17 and swapping out a built version of portal.ts@v1.7.16 locally.

Compare tailwindlabs/headlessui/vue@v1.7.16...17

v1.7.16 node_modules/@headlessui/vue/dist/components/portal/portal.js

``` import{Teleport as x,computed as C,defineComponent as p,h as H,inject as m,onMounted as M,onUnmounted as c,provide as g,reactive as L,ref as s,watchEffect as j}from"vue";import{render as T}from'../../utils/render.js';import{usePortalRoot as b}from'../../internal/portal-force-root.js';import{getOwnerDocument as y}from'../../utils/owner.js';import{dom as w}from'../../utils/dom.js';function E(t){let e=y(t);if(!e){if(t===null)return null;throw new Error(`[Headless UI]: Cannot find ownerDocument for contextElement: ${t}`)}let u=e.getElementById("headlessui-portal-root");if(u)return u;let r=e.createElement("div");return r.setAttribute("id","headlessui-portal-root"),e.body.appendChild(r)}let U=p({name:"Portal",props:{as:{type:[Object,String],default:"div"}},setup(t,{slots:e,attrs:u}){let r=s(null),i=C(()=>y(r)),l=b(),n=m(h,null),o=s(l===!0||n==null?E(r.value):n.resolveTarget());j(()=>{l||n!=null&&(o.value=n.resolveTarget())});let d=m(f,null);return M(()=>{let a=w(r);a&&d&&c(d.register(a))}),c(()=>{var v,P;let a=(v=i.value)==null?void 0:v.getElementById("headlessui-portal-root");a&&o.value===a&&o.value.children.length<=0&&((P=o.value.parentElement)==null||P.removeChild(o.value))}),()=>{if(o.value===null)return null;let a={ref:r,"data-headlessui-portal":""};return H(x,{to:o.value},T({ourProps:a,theirProps:t,slot:{},attrs:u,slots:e,name:"Portal"}))}}}),f=Symbol("PortalParentContext");function V(){let t=m(f,null),e=s([]);function u(l){return e.value.push(l),t&&t.register(l),()=>r(l)}function r(l){let n=e.value.indexOf(l);n!==-1&&e.value.splice(n,1),t&&t.unregister(l)}let i={register:u,unregister:r,portals:e};return[e,p({name:"PortalWrapper",setup(l,{slots:n}){return g(f,i),()=>{var o;return(o=n.default)==null?void 0:o.call(n)}}})]}let h=Symbol("PortalGroupContext"),_=p({name:"PortalGroup",props:{as:{type:[Object,String],default:"template"},target:{type:Object,default:null}},setup(t,{attrs:e,slots:u}){let r=L({resolveTarget(){return t.target}});return g(h,r),()=>{let{target:i,...l}=t;return T({theirProps:l,ourProps:{},slot:{},attrs:e,slots:u,name:"PortalGroup"})}}});export{U as Portal,_ as PortalGroup,V as useNestedPortals}; ```

v1.7.17 node_modules/@headlessui/vue/dist/components/portal/portal.js

``` import{computed as M,defineComponent as s,getCurrentInstance as L,h as j,inject as f,onMounted as w,onUnmounted as y,provide as T,reactive as I,ref as p,Teleport as b,watch as R,watchEffect as G}from"vue";import{usePortalRoot as O}from'../../internal/portal-force-root.js';import{dom as D}from'../../utils/dom.js';import{getOwnerDocument as E}from'../../utils/owner.js';import{render as h}from'../../utils/render.js';function x(r){let e=E(r);if(!e){if(r===null)return null;throw new Error(`[Headless UI]: Cannot find ownerDocument for contextElement: ${r}`)}let u=e.getElementById("headlessui-portal-root");if(u)return u;let t=e.createElement("div");return t.setAttribute("id","headlessui-portal-root"),e.body.appendChild(t)}let _=s({name:"Portal",props:{as:{type:[Object,String],default:"div"}},setup(r,{slots:e,attrs:u}){let t=p(null),i=M(()=>E(t)),l=O(),n=f(C,null),o=p(l===!0||n==null?x(t.value):n.resolveTarget()),d=p(!1);w(()=>{d.value=!0}),G(()=>{l||n!=null&&(o.value=n.resolveTarget())});let c=f(m,null),v=!1,H=L();return R(t,()=>{if(v||!c)return;let a=D(t);a&&(y(c.register(a),H),v=!0)}),y(()=>{var g,P;let a=(g=i.value)==null?void 0:g.getElementById("headlessui-portal-root");a&&o.value===a&&o.value.children.length<=0&&((P=o.value.parentElement)==null||P.removeChild(o.value))}),()=>{if(!d.value||o.value===null)return null;let a={ref:t,"data-headlessui-portal":""};return j(b,{to:o.value},h({ourProps:a,theirProps:r,slot:{},attrs:u,slots:e,name:"Portal"}))}}}),m=Symbol("PortalParentContext");function A(){let r=f(m,null),e=p([]);function u(l){return e.value.push(l),r&&r.register(l),()=>t(l)}function t(l){let n=e.value.indexOf(l);n!==-1&&e.value.splice(n,1),r&&r.unregister(l)}let i={register:u,unregister:t,portals:e};return[e,s({name:"PortalWrapper",setup(l,{slots:n}){return T(m,i),()=>{var o;return(o=n.default)==null?void 0:o.call(n)}}})]}let C=Symbol("PortalGroupContext"),N=s({name:"PortalGroup",props:{as:{type:[Object,String],default:"template"},target:{type:Object,default:null}},setup(r,{attrs:e,slots:u}){let t=I({resolveTarget(){return r.target}});return T(C,t),()=>{let{target:i,...l}=r;return h({theirProps:l,ourProps:{},slot:{},attrs:e,slots:u,name:"PortalGroup"})}}});export{_ as Portal,N as PortalGroup,A as useNestedPortals}; ```

edeustua commented 2 weeks ago

I would guess that an { immediate: true } added to the watcher might fix it (??).

mtzrmzia commented 2 weeks ago

Is there any news on this issue?

LePtiDev commented 1 week ago

Same issue for me

ixycej12 commented 1 week ago

I will add the style of class="${enterFrom}" to TransitionChild

ixycej12 commented 1 week ago

If you don’t add it, there will be no entry animation.

blackjak231 commented 1 week ago

I will add the style of class="${enterFrom}" to TransitionChild

This is actually working great in my case. It's clearly a temporary hack to get it working over a fix, but allows me to keep Vue up to date. Thanks for the tip @ixycej12

marcos-c-vega commented 1 week ago

I will add the style of class="${enterFrom}" to TransitionChild

Sorry, I'm not finding the correct way to add this, I tried the following but didn't work:

<TransitionChild
  as="template"
  class="${enterFrom}"
  enter="ease-in-out duration-500"
  enter-from="opacity-0"
  enter-to="opacity-100"
  leave="ease-in-out duration-500"
  leave-from="opacity-100"
  leave-to="opacity-0"
>

Do I have to include enterFrom somehow @ixycej12 @blackjak231 ?

marcos-c-vega commented 1 week ago

Ahhh I just found it was a reference to the class you are passing into enter-from attribute, so in my case enterFrom = opacity-0 so I placed it like:

<TransitionChild
  as="template"
  class="opacity-0"
  enter="ease-in-out duration-500"
  enter-from="opacity-0"
  enter-to="opacity-100"
  leave="ease-in-out duration-500"
  leave-from="opacity-100"
  leave-to="opacity-0"
>

Worked like a charm!

ixycej12 commented 1 week ago

The current temporary solution, but there will be bugs.

graphem commented 1 week ago

Thanks! That worked for me too!

HakwonChile commented 6 days ago

Any fixed version?