Change default tag from div to Fragment on Transition components (#3110)

* use `Fragment` as the default element for `Transition` components

* update tests to reflect default tag change

* only error on missing `ref` if it's actually required

If the `<Transition />` component "root" is used as a root placeholder
(for state management) and not making actual transitions itself, then we
don't require a `ref` element.

* add test to ensure we don't error on missing `ref` when not required

+ add `className="…"` to some places to indicate that we _do_ want to
  perform a transition and thus have to fail if the `ref` is missing.

* improve `requiresRef` check

Also ensure that a ref is required if the `as` prop is provided and
it's not a `Fragment`

* add `shouldForwardRef` helper

* fix broken tests

These tests were rendering a `Debug` element that didn't render any DOM
nodes. Adding `as="div"` ensures that we are forwarding the ref
correctly.

* update changelog

* update playgrounds to reflect tag change

* Tweak changelog

---------

Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
This commit is contained in:
Robin Malfait
2024-04-19 16:15:11 +02:00
committed by GitHub
parent 83cda0aa75
commit 8fa5caf0dc
22 changed files with 182 additions and 85 deletions
@@ -1,13 +1,12 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
import { useState } from 'react'
function MyDialog({ open, close }) {
return (
<>
<Transition show={open} as={Fragment}>
<Transition show={open}>
<Dialog onClose={close} className="relative z-50">
<Transition.Child
as={Fragment}
enter="transition duration-500 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
+2 -3
View File
@@ -1,5 +1,5 @@
import { Dialog, Menu, Portal, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
import { useState } from 'react'
import Flatpickr from 'react-flatpickr'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
@@ -64,7 +64,6 @@ export default function Home() {
<Transition
data-debug="Dialog"
show={isOpen}
as={Fragment}
beforeEnter={() => console.log('[Transition] Before enter')}
afterEnter={() => console.log('[Transition] After enter')}
beforeLeave={() => console.log('[Transition] Before leave')}
@@ -79,7 +78,6 @@ export default function Home() {
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-75"
@@ -96,6 +94,7 @@ export default function Home() {
</Transition.Child>
<Transition.Child
as="div"
enter="ease-out transform duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
@@ -1,6 +1,6 @@
import { Dialog, Transition } from '@headlessui/react'
import { ExclamationIcon } from '@heroicons/react/outline'
import { Fragment, useRef, useState } from 'react'
import { useRef, useState } from 'react'
export default function Example() {
const [open, setOpen] = useState(false)
@@ -18,10 +18,9 @@ export default function Example() {
Open Dialog
</button>
</div>
<Transition.Root show={open} as={Fragment}>
<Transition.Root show={open}>
<Dialog as="div" className="relative z-10" initialFocus={cancelButtonRef} onClose={setOpen}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
@@ -35,7 +34,6 @@ export default function Example() {
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
@@ -1,5 +1,5 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
import { useState } from 'react'
export default function Home() {
let [isOpen, setIsOpen] = useState(false)
@@ -37,7 +37,6 @@ export default function Home() {
<Transition
data-debug="Dialog"
show={isOpen}
as={Fragment}
beforeEnter={() => console.log('[Transition] Before enter')}
afterEnter={() => console.log('[Transition] After enter')}
beforeLeave={() => console.log('[Transition] Before leave')}
@@ -52,7 +51,6 @@ export default function Home() {
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-75"
@@ -69,6 +67,7 @@ export default function Home() {
</Transition.Child>
<Transition.Child
as="div"
enter="ease-out transform duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
@@ -1,5 +1,5 @@
import { Listbox, Transition } from '@headlessui/react'
import { Fragment, useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
let people = [
'Wade Cooper',
@@ -60,7 +60,6 @@ export default function Home() {
</span>
<Transition
as={Fragment}
enter="transition duration-500 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
@@ -1,5 +1,4 @@
import { Menu, Transition } from '@headlessui/react'
import { Fragment } from 'react'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
@@ -30,7 +29,6 @@ export default function Home() {
</span>
<Transition
as={Fragment}
enter="transition duration-500 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
+1 -2
View File
@@ -1,5 +1,5 @@
import { Popover, Portal, Transition } from '@headlessui/react'
import React, { Fragment, forwardRef } from 'react'
import React, { forwardRef } from 'react'
import { usePopper } from '../../utils/hooks/use-popper'
let Button = forwardRef(
@@ -33,7 +33,6 @@ export default function Home() {
<Popover.Group as="nav" aria-label="Mythical University" className="flex space-x-3">
<Popover as="div" className="relative">
<Transition
as={Fragment}
enter="transition ease-out duration-300 transform"
enterFrom="opacity-0"
enterTo="opacity-100"
@@ -18,6 +18,7 @@ export default function AppearExample() {
<span className="mb-2">Initial render</span>
<div className="grid max-w-6xl grid-cols-4 gap-4">
<Transition
as="div"
show={show}
appear={true}
unmount={true}
@@ -50,6 +51,7 @@ export default function AppearExample() {
</Transition>
<Transition
as="div"
show={show}
appear={false}
unmount={true}
@@ -82,6 +84,7 @@ export default function AppearExample() {
</Transition>
<Transition
as="div"
show={show}
appear={true}
unmount={false}
@@ -114,6 +117,7 @@ export default function AppearExample() {
</Transition>
<Transition
as="div"
show={show}
appear={false}
unmount={false}
@@ -152,6 +156,7 @@ export default function AppearExample() {
<span className="mb-2">Not on the initial render</span>
<div className="grid max-w-6xl grid-cols-4 gap-4">
<Transition
as="div"
show={show}
appear={true}
unmount={true}
@@ -184,6 +189,7 @@ export default function AppearExample() {
</Transition>
<Transition
as="div"
show={show}
appear={false}
unmount={true}
@@ -216,6 +222,7 @@ export default function AppearExample() {
</Transition>
<Transition
as="div"
show={show}
appear={true}
unmount={false}
@@ -248,6 +255,7 @@ export default function AppearExample() {
</Transition>
<Transition
as="div"
show={show}
appear={false}
unmount={false}
@@ -44,6 +44,7 @@ function Dropdown() {
</div>
<Transition
as="div"
show={isOpen}
enter="transition ease-out duration-75"
enterFrom="transform opacity-0 scale-95"
@@ -41,6 +41,7 @@ export default function Home() {
</div>
<Transition
as="div"
show={isOpen}
className="fixed inset-0 z-10 overflow-y-auto"
beforeEnter={() => {
@@ -78,6 +79,7 @@ export default function Home() {
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:h-screen sm:align-middle"></span>&#8203;
<Transition.Child
as="div"
className="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle"
role="dialog"
aria-modal="true"
@@ -18,7 +18,7 @@ export default function Home() {
</button>
</span>
<Transition show={isOpen} unmount={false}>
<Transition as="div" show={isOpen} unmount={false}>
<Box>
<Box>
<Box>
@@ -18,7 +18,7 @@ export default function Home() {
</button>
</span>
<Transition show={isOpen} unmount={true}>
<Transition as="div" show={isOpen} unmount={true}>
<Box>
<Box>
<Box>
@@ -43,6 +43,7 @@ export default function Home() {
function Box({ children }: { children?: ReactNode }) {
return (
<Transition.Child
as="div"
unmount={true}
enter="transition translate duration-300"
enterFrom="transform -translate-x-full"
@@ -19,6 +19,7 @@ export default function Home() {
</span>
<Transition
as="div"
show={isOpen}
appear={false}
beforeEnter={() => console.log('beforeEnter')}
@@ -160,6 +160,7 @@ function FullPageTransition() {
<div className="relative h-96 overflow-hidden rounded-lg">
{pages.map((page, i) => (
<Transition
as="div"
appear={false}
key={page}
show={activePage === i}
@@ -26,9 +26,10 @@ export default function App() {
<div className="bg-cool-gray-100 flex h-screen overflow-hidden">
{/* Off-canvas menu for mobile */}
<Transition show={mobileOpen} unmount={false} className="fixed inset-0 z-40 flex">
<Transition as="div" show={mobileOpen} unmount={false} className="fixed inset-0 z-40 flex">
{/* Off-canvas menu overlay, show/hide based on off-canvas menu state. */}
<Transition.Child
as="div"
unmount={false}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
@@ -49,6 +50,7 @@ export default function App() {
{/* Off-canvas menu, show/hide based on off-canvas menu state. */}
<Transition.Child
as="div"
unmount={false}
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
@@ -7,6 +7,7 @@ const TailwindToaster = () => {
{(t) => (
<Transition
appear
as="div"
show={t.visible}
className="flex transform rounded bg-white p-4 shadow-lg"
enter="transition-all duration-500"