headlessui icon indicating copy to clipboard operation
headlessui copied to clipboard

[Dialog/Transitions] Enter transitions no longer happening

Open blackjak231 opened this issue 1 year ago • 19 comments

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 !

blackjak231 avatar Sep 05 '24 08:09 blackjak231

+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.

edeustua avatar Sep 09 '24 04:09 edeustua

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 avatar Sep 11 '24 21:09 shengslogar

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/[email protected] by pinning v1.7.17 and swapping out a built version of [email protected] locally.

Compare tailwindlabs/headlessui/[email protected]

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

shengslogar avatar Sep 11 '24 22:09 shengslogar

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

edeustua avatar Sep 11 '24 22:09 edeustua

Is there any news on this issue?

mtzrmzia avatar Sep 15 '24 18:09 mtzrmzia

Same issue for me

LePtiDev avatar Sep 17 '24 10:09 LePtiDev

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

ixycej12 avatar Sep 18 '24 16:09 ixycej12

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

ixycej12 avatar Sep 18 '24 16:09 ixycej12

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

blackjak231 avatar Sep 19 '24 16:09 blackjak231

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 avatar Sep 20 '24 15:09 marcos-c-vega

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!

marcos-c-vega avatar Sep 20 '24 15:09 marcos-c-vega

The current temporary solution, but there will be bugs.

ixycej12 avatar Sep 21 '24 10:09 ixycej12

Thanks! That worked for me too!

graphem avatar Sep 21 '24 16:09 graphem

Any fixed version?

HakwonChile avatar Sep 24 '24 04:09 HakwonChile

Most recent Vue fixed the issue for me.

heartz66 avatar Oct 05 '24 16:10 heartz66

the problem is still relevant

ashab20zakaraev avatar Oct 31 '24 19:10 ashab20zakaraev

Hey @RobinMalfait,

Is there any news on this ? The temporary fix was working but is now broken with other libraries such as Vueuse. I had to revert back to 1.7.16 as mentioned in #1503 .

Is there any news on HeadlessUI Vue generally speaking ? A lot of work seemed to have been put into the React version lately but Vue has been quiet for a while now.

Can we expect the same kind of "2.0" update for HeadlessUI Vue in the coming weeks/months ?

Thanks

blackjak231 avatar Dec 04 '24 08:12 blackjak231

I think that this issue is linked with mine https://github.com/tailwindlabs/headlessui/issues/3535

Archetipo95 avatar Jan 18 '25 14:01 Archetipo95

I think that this issue is linked with mine #3535

@Archetipo95 Yes it is. Only workarounds for now are :

  • Stay on version 1.7.16 of headlessui (which I recommend unless you absolutely need the fixes from > 1.7.16)
  • Stay on version 1.7.22 and apply the start from class to the element's classes (as mentionned by @ixycej12 here)

blackjak231 avatar Jan 20 '25 10:01 blackjak231