Add transition prop to <Dialog /> component (#3307)
* add `transition` prop to `Dialog`
Internally this will make sure that the `Dialog` itself gets wrapped in a `<Transition />` component.
Next, the `<DialogPanel>` will also be wrapped in a `<TransitionChild />` component.
We also re-introduce the `DialogBackdrop` that will also be wrapped in a
`<TransitionChild />` component based on the `transition` prop of the
`Dialog`.
This simplifies the `<Dialog />` component, especially now that we can
use transitions with data attributes.
E.g.:
```tsx
<Transition show={open}>
<Dialog onClose={setOpen}>
<TransitionChild
enter="ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div />
</TransitionChild>
<TransitionChild
enter="ease-in-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<DialogPanel>
{/* … */}
</DialogPanel>
</TransitionChild>
</Dialog>
</Transition>
```
↓↓↓↓↓
```tsx
<Transition show={open}>
<Dialog onClose={setOpen}>
<TransitionChild>
<div className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95" />
</TransitionChild>
<TransitionChild>
<DialogPanel className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95 bg-white">
{/* … */}
</DialogPanel>
</TransitionChild>
</Dialog>
</Transition>
```
↓↓↓↓↓
```tsx
<Dialog transition open={open} onClose={setOpen}>
<DialogBackdrop className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95" />
<DialogPanel className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95 bg-white">
{/* … */}
</DialogPanel>
</Dialog>
```
* update test now that we expose `DialogBackdrop`
* add built-in `<Dialog transition />` playground example
* update changelog
This commit is contained in:
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Add ability to render multiple `<Dialog />` components at once (without nesting them) ([#3242](https://github.com/tailwindlabs/headlessui/pull/3242))
|
||||
- Add CSS based transitions using `data-*` attributes ([#3273](https://github.com/tailwindlabs/headlessui/pull/3273), [#3285](https://github.com/tailwindlabs/headlessui/pull/3285))
|
||||
- Add a `transition` prop to `<Dialog />` component ([#3307](https://github.com/tailwindlabs/headlessui/pull/3307))
|
||||
- Re-introduce `<DialogBackdrop />` component ([#3307](https://github.com/tailwindlabs/headlessui/pull/3307))
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -21,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Use `useId` instead of React internals (for React 19 compatibility) ([#3254](https://github.com/tailwindlabs/headlessui/pull/3254))
|
||||
- Ensure `ComboboxInput` does not sync with current value while typing ([#3259](https://github.com/tailwindlabs/headlessui/pull/3259))
|
||||
- Cancel outside click behavior on touch devices when scrolling ([#3266](https://github.com/tailwindlabs/headlessui/pull/3266))
|
||||
- Correctly apply conditional classses when using `<Transition />` and `<TransitionChild />` components ([#3303](https://github.com/tailwindlabs/headlessui/pull/3303))
|
||||
- Correctly apply conditional classes when using `<Transition />` and `<TransitionChild />` components ([#3303](https://github.com/tailwindlabs/headlessui/pull/3303))
|
||||
- Improve UX by freezing `ComboboxOptions` while closing ([#3304](https://github.com/tailwindlabs/headlessui/pull/3304))
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
// WAI-ARIA: https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/
|
||||
import React, {
|
||||
Fragment,
|
||||
createContext,
|
||||
createRef,
|
||||
useContext,
|
||||
@@ -49,6 +50,9 @@ import {
|
||||
} from '../description/description'
|
||||
import { FocusTrap, FocusTrapFeatures } from '../focus-trap/focus-trap'
|
||||
import { Portal, PortalGroup, useNestedPortals } from '../portal/portal'
|
||||
import { Transition, TransitionChild } from '../transition/transition'
|
||||
|
||||
let WithTransitionWrapper = createContext(false)
|
||||
|
||||
enum DialogStates {
|
||||
Open,
|
||||
@@ -126,6 +130,7 @@ export type DialogProps<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG> =
|
||||
role?: 'dialog' | 'alertdialog'
|
||||
autoFocus?: boolean
|
||||
__demoMode?: boolean
|
||||
transition?: boolean
|
||||
}
|
||||
>
|
||||
|
||||
@@ -141,6 +146,7 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
|
||||
initialFocus,
|
||||
role = 'dialog',
|
||||
autoFocus = true,
|
||||
transition = false,
|
||||
__demoMode = false,
|
||||
...theirProps
|
||||
} = props
|
||||
@@ -337,6 +343,17 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
|
||||
}
|
||||
}
|
||||
|
||||
if (transition) {
|
||||
let { transition: _transition, open, ...rest } = props
|
||||
return (
|
||||
<WithTransitionWrapper.Provider value={true}>
|
||||
<Transition show={open}>
|
||||
<Dialog ref={ref} {...rest} />
|
||||
</Transition>
|
||||
</WithTransitionWrapper.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ForcePortalRoot force={true}>
|
||||
@@ -416,13 +433,62 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
|
||||
onClick: handleClick,
|
||||
}
|
||||
|
||||
return render({
|
||||
ourProps,
|
||||
theirProps,
|
||||
slot,
|
||||
defaultTag: DEFAULT_PANEL_TAG,
|
||||
name: 'Dialog.Panel',
|
||||
})
|
||||
let Wrapper = useContext(WithTransitionWrapper) ? TransitionChild : Fragment
|
||||
|
||||
return (
|
||||
<WithTransitionWrapper.Provider value={false}>
|
||||
<Wrapper>
|
||||
{render({
|
||||
ourProps,
|
||||
theirProps,
|
||||
slot,
|
||||
defaultTag: DEFAULT_PANEL_TAG,
|
||||
name: 'Dialog.Panel',
|
||||
})}
|
||||
</Wrapper>
|
||||
</WithTransitionWrapper.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
let DEFAULT_BACKDROP_TAG = 'div' as const
|
||||
type BackdropRenderPropArg = {
|
||||
open: boolean
|
||||
}
|
||||
|
||||
export type DialogBackdropProps<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG> = Props<
|
||||
TTag,
|
||||
BackdropRenderPropArg
|
||||
>
|
||||
|
||||
function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
|
||||
props: DialogBackdropProps<TTag>,
|
||||
ref: Ref<HTMLElement>
|
||||
) {
|
||||
let theirProps = props
|
||||
let [{ dialogState }] = useDialogContext('Dialog.Backdrop')
|
||||
|
||||
let slot = useMemo(
|
||||
() => ({ open: dialogState === DialogStates.Open }) satisfies BackdropRenderPropArg,
|
||||
[dialogState]
|
||||
)
|
||||
|
||||
let ourProps = { ref }
|
||||
|
||||
let Wrapper = useContext(WithTransitionWrapper) ? TransitionChild : Fragment
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{render({
|
||||
ourProps,
|
||||
theirProps,
|
||||
slot,
|
||||
defaultTag: DEFAULT_BACKDROP_TAG,
|
||||
name: 'Dialog.Backdrop',
|
||||
})}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
// ---
|
||||
@@ -482,6 +548,12 @@ export interface _internal_ComponentDialogPanel extends HasDisplayName {
|
||||
): JSX.Element
|
||||
}
|
||||
|
||||
export interface _internal_ComponentDialogBackdrop extends HasDisplayName {
|
||||
<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
|
||||
props: DialogBackdropProps<TTag> & RefProp<typeof BackdropFn>
|
||||
): JSX.Element
|
||||
}
|
||||
|
||||
export interface _internal_ComponentDialogTitle extends HasDisplayName {
|
||||
<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
|
||||
props: DialogTitleProps<TTag> & RefProp<typeof TitleFn>
|
||||
@@ -492,6 +564,7 @@ export interface _internal_ComponentDialogDescription extends _internal_Componen
|
||||
|
||||
let DialogRoot = forwardRefWithAs(DialogFn) as _internal_ComponentDialog
|
||||
export let DialogPanel = forwardRefWithAs(PanelFn) as _internal_ComponentDialogPanel
|
||||
export let DialogBackdrop = forwardRefWithAs(BackdropFn) as _internal_ComponentDialogBackdrop
|
||||
export let DialogTitle = forwardRefWithAs(TitleFn) as _internal_ComponentDialogTitle
|
||||
/** @deprecated use `<Description>` instead of `<DialogDescription>` */
|
||||
export let DialogDescription = Description as _internal_ComponentDialogDescription
|
||||
|
||||
@@ -24,6 +24,7 @@ it('should expose the correct components', () => {
|
||||
'Description',
|
||||
|
||||
'Dialog',
|
||||
'DialogBackdrop',
|
||||
'DialogDescription',
|
||||
'DialogPanel',
|
||||
'DialogTitle',
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
|
||||
import { useState } from 'react'
|
||||
import { Button } from '../../components/button'
|
||||
import { classNames } from '../../utils/class-names'
|
||||
|
||||
export default function Home() {
|
||||
let [isOpen, setIsOpen] = useState(false)
|
||||
let [transition, setTransition] = useState(true)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-4 p-12">
|
||||
<Button onClick={() => setIsOpen((v) => !v)}>Toggle!</Button>
|
||||
<Button onClick={() => setTransition((v) => !v)}>
|
||||
<span>Toggle transition</span>
|
||||
<span
|
||||
className={classNames(
|
||||
'ml-2 inline-flex size-4 rounded-md',
|
||||
transition ? 'bg-green-500' : 'bg-red-500'
|
||||
)}
|
||||
></span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
transition={transition}
|
||||
onClose={() => setIsOpen(false)}
|
||||
className="relative z-50"
|
||||
>
|
||||
<DialogBackdrop className="fixed inset-0 bg-black/30 duration-500 ease-out data-[closed]:opacity-0" />
|
||||
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
|
||||
<DialogPanel className="w-full max-w-lg space-y-4 bg-white p-12 duration-500 ease-out data-[closed]:scale-95 data-[closed]:opacity-0">
|
||||
<h1 className="text-2xl font-bold">Dialog</h1>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed pulvinar, nunc nec
|
||||
vehicula fermentum, nunc sapien tristique ipsum, nec facilisis dolor sapien non dui.
|
||||
Nullam vel sapien ultrices, lacinia felis sit amet, fermentum odio. Nullam vel sapien
|
||||
ultrices, lacinia felis sit amet, fermentum odio.
|
||||
</p>
|
||||
<Button onClick={() => setIsOpen(false)}>Close</Button>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user