tailwindlabs / headlessui

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

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

Open blackjak231 opened 2 months ago

blackjak231 commented 2 months 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 2 months 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 months 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 months 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 months ago

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

mtzrmzia commented 2 months ago

Is there any news on this issue?

LePtiDev commented 2 months ago

Same issue for me

ixycej12 commented 2 months ago

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

ixycej12 commented 2 months ago

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

blackjak231 commented 2 months 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 2 months 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 2 months 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 2 months ago

The current temporary solution, but there will be bugs.

graphem commented 2 months ago

Thanks! That worked for me too!

HakwonChile commented 2 months ago

Any fixed version?

heartz66 commented 1 month ago

Most recent Vue fixed the issue for me.

ashab20zakaraev commented 3 weeks ago

the problem is still relevant