tailwindlabs / headlessui

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

Error: A <Transition.Child /> is used but it is missing a parent <Transition /> or <Transition.Root />. #3316

Closed spacecat closed 6 days ago

spacecat commented 1 week ago

What package within Headless UI are you using?

@headlessui/react

What version of that package are you using?

"@headlessui/react": "2.1.0",

What browser are you using?

MacOS Sonoma Version 14.4.1 (23E224) Safari Version 17.4.1 (19618.1.15.11.14) Chrome Version 126.0.6478.62 (Official Build) (arm64)

Description:

I tried the new Dialog transition APIs but the transitions are not working for me.

I tried the code from here: https://github.com/tailwindlabs/headlessui/pull/3307 here: https://github.com/tailwindlabs/headlessui/pull/3309 and here: https://headlessui.com/react/dialog#adding-transitions

This is what I have in my own project:

<Dialog open={open} onClose={setOpen}>
      <DialogBackdrop transition className="fixed inset-0 bg-black/30 duration-1000 ease-in-out data-[closed]:scale-95 data-[closed]:opacity-0" />
      <div className="fixed inset-0 flex w-screen items-center justify-center p-4">
        <DialogPanel transition className="bg-white duration-300 ease-in-out data-[closed]:scale-95 data-[closed]:opacity-0">
          <p>
            Are you sure you want to deactivate your account? All of your data
            will be permanently removed.
          </p>
          <div className="flex gap-4"></div>
        </DialogPanel>
      </div>
    </Dialog>

And it's giving me the following error:

1 of 4 errors
Next.js (14.2.4)

Unhandled Runtime Error
Error: A <Transition.Child /> is used but it is missing a parent <Transition /> or <Transition.Root />.

Call Stack
Ne
node_modules/@headlessui/react/dist/components/transition/transition.js (1:1408)
Ne
node_modules/@headlessui/react/dist/components/transition/transition.js (1:3326)

I'm probably just missing something?

zce commented 1 week ago

I researched and found that this is caused by <OpenClosedProvider /> nesting.

like:

<Disclosure> {/* <=  OpenClosedProvider in parent context */}
  <Dialog />
</Disclosure>

https://github.com/tailwindlabs/headlessui/blob/d60ed6a6702284c84415f49e12739f2c448d17e1/packages/%40headlessui-react/src/components/dialog/dialog.tsx#L393

RobinMalfait commented 1 week ago

Hey!

Can you share a minimal reproduction repo because the code snippet you shared doesn't reproduce it. As @zce mentioned, incorrectly nesting certain components could cause this behavior, if you are putting your <Dialog /> in a <Disclosure /> make sure to either move it inside the <DisclosurePanel /> or moving it outside of the <Disclosure />

ThenTech commented 1 week ago

I tried to do the same after seeing the changes in v2.1.0, and got the same error. In my case, I had the static prop on <Dialog />, removing that seems to work fine now, event though I do intent to ignore the internally managed open/closed state.

Edit: After some more testing, there is definitely something wrong when using the <Dialog /> component and children with transition, while also using nested dialogs (which also got updated in v2.1.0). Closing a nested dialog now hides all dialogs, also the one that was open when opening the nested one. So for this, I need the static prop, which throws the error above, so then I still need to wrap <Dialog /> in a Transition as before v2.1.0. Then it works again.

zce commented 1 week ago

make sure to either move it inside the <DisclosurePanel /> or moving it outside of the <Disclosure />

This happens when I use the Disclosure component to implement a mobile navigation collapse scenario, and this navigation also contains a search dialog.

As you said, I can find ways to avoid this, but I don't think it's a very good development experience. I'm not sure if there are other built-in components that use transitionchild, if so, I think they will have similar issues.

I'm very sorry, I'm on my phone and can't provide a repro repo at the moment

spacecat commented 1 week ago

@RobinMalfait I will try to create a repro repo tomorrow. In the meantime; I'm checking my code and I am indeed nesting the dialog - I did not think about this before. Here is my code structure:

Please note that Dialog, DialogBody, DialogActions in this first code snippet (parent component) come from Catalyst.

import {
  Dialog,
  DialogActions,
  DialogBody,
} from "@/components/catalyst/dialog";
)

I'm using this version of Catalyst - not sure if it's the latest but pretty new:

# Changelog

## 2024-05-31

- Add Next.js demo app ([#1580](https://github.com/tailwindlabs/tailwindui-issues/issues/1580))
- Fix `Avatar` sizing and padding ([#1588](https://github.com/tailwindlabs/tailwindui-issues/issues/1588))

Parent component:

return (
    <Dialog open={isOpen} onClose={() => setIsOpen(false)}>
      <DialogBody>
             <!-- my JSX - just some HTML tags -->
      </DialogBody>
      <DialogActions>
        <button
          onClick={() => setIsOpen(false)}
          type="button"
        >
          Minimize
        </button>
        <button
          onClick={() => {
            setOpen(true);
          }}
          type="button"
        >
          <CompareIcon className="size-4" />
          Compare
        </button>
        <TableDialog
          open={open}
          setOpen={setOpen}
          selectedBrands={selectedBrands}
        />
      </DialogActions>
    </Dialog>
  );

and then in my <TableDialog> component I have this:

Child component:

return (
    <Transition show={open}>
      <Dialog className="relative z-[60]" onClose={() => setOpen(false)}>
        <TransitionChild
          as={Fragment}
          enter="transition-opacity ease-linear duration-300"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="transition-opacity ease-linear duration-300"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
          unmount={false}
        >
          <div className="fixed inset-0 bg-theme-sidebar-right-backdrop/10 backdrop-blur-[2px]" />
        </TransitionChild>
        <div className="fixed inset-0 z-[60] flex items-center justify-center">
          <TransitionChild
            enter="ease-out duration-300"
            enterFrom="opacity-0 translate-y-0 sm:scale-95"
            enterTo="opacity-100 translate-y-0 sm:scale-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100 translate-y-0 sm:scale-100"
            leaveTo="opacity-0 translate-y-0 sm:scale-95"
          >
            <DialogPanel className="flex h-full w-full flex-col overflow-hidden bg-theme-app-background sm:h-[90vh] sm:w-auto sm:rounded-lg lg:max-w-7xl">
              <!-- my own JSX - regular HTML tags -->
            </DialogPanel>
          </TransitionChild>
        </div>
      </Dialog>
    </Transition>
  );

and then I replaced the <Dialog> in <TableDialog> with what I wrote earlier (which is causing the error):

  <Dialog open={open} onClose={setOpen}>
      <DialogBackdrop transition className="fixed inset-0 bg-black/30 duration-1000 ease-in-out data-[closed]:scale-95 data-[closed]:opacity-0" />
      <div className="fixed inset-0 flex w-screen items-center justify-center p-4">
        <DialogPanel transition className="bg-white duration-300 ease-in-out data-[closed]:scale-95 data-[closed]:opacity-0">
          <p>
            Are you sure you want to deactivate your account? All of your data
            will be permanently removed.
          </p>
          <div className="flex gap-4"></div>
        </DialogPanel>
      </div>
    </Dialog>

I hope this helps debugging/troubleshooting.

RobinMalfait commented 6 days ago

I figured out a reproduction that seems to reproduce this. So this should be fixed by #3331, and will be available in the next release. If this does not fix your issue after testing, please open a new issue with a minimal reproduction GitHub repo attached.

You can already try it using:

spacecat commented 6 days ago

Thanks @RobinMalfait. Will try the insiders build asap.