Skip to content

Commit cf0c535

Browse files
authored
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
1 parent 1c3f9a6 commit cf0c535

File tree

4 files changed

+131
-8
lines changed

4 files changed

+131
-8
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Add ability to render multiple `<Dialog />` components at once (without nesting them) ([#3242](https://github.com/tailwindlabs/headlessui/pull/3242))
1313
- Add CSS based transitions using `data-*` attributes ([#3273](https://github.com/tailwindlabs/headlessui/pull/3273), [#3285](https://github.com/tailwindlabs/headlessui/pull/3285))
14+
- Add a `transition` prop to `<Dialog />` component ([#3307](https://github.com/tailwindlabs/headlessui/pull/3307))
15+
- Re-introduce `<DialogBackdrop />` component ([#3307](https://github.com/tailwindlabs/headlessui/pull/3307))
1416

1517
### Fixed
1618

@@ -21,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2123
- Use `useId` instead of React internals (for React 19 compatibility) ([#3254](https://github.com/tailwindlabs/headlessui/pull/3254))
2224
- Ensure `ComboboxInput` does not sync with current value while typing ([#3259](https://github.com/tailwindlabs/headlessui/pull/3259))
2325
- Cancel outside click behavior on touch devices when scrolling ([#3266](https://github.com/tailwindlabs/headlessui/pull/3266))
24-
- Correctly apply conditional classses when using `<Transition />` and `<TransitionChild />` components ([#3303](https://github.com/tailwindlabs/headlessui/pull/3303))
26+
- Correctly apply conditional classes when using `<Transition />` and `<TransitionChild />` components ([#3303](https://github.com/tailwindlabs/headlessui/pull/3303))
2527
- Improve UX by freezing `ComboboxOptions` while closing ([#3304](https://github.com/tailwindlabs/headlessui/pull/3304))
2628

2729
### Changed

packages/@headlessui-react/src/components/dialog/dialog.tsx

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
// WAI-ARIA: https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/
44
import React, {
5+
Fragment,
56
createContext,
67
createRef,
78
useContext,
@@ -49,6 +50,9 @@ import {
4950
} from '../description/description'
5051
import { FocusTrap, FocusTrapFeatures } from '../focus-trap/focus-trap'
5152
import { Portal, PortalGroup, useNestedPortals } from '../portal/portal'
53+
import { Transition, TransitionChild } from '../transition/transition'
54+
55+
let WithTransitionWrapper = createContext(false)
5256

5357
enum DialogStates {
5458
Open,
@@ -126,6 +130,7 @@ export type DialogProps<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG> =
126130
role?: 'dialog' | 'alertdialog'
127131
autoFocus?: boolean
128132
__demoMode?: boolean
133+
transition?: boolean
129134
}
130135
>
131136

@@ -141,6 +146,7 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
141146
initialFocus,
142147
role = 'dialog',
143148
autoFocus = true,
149+
transition = false,
144150
__demoMode = false,
145151
...theirProps
146152
} = props
@@ -337,6 +343,17 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
337343
}
338344
}
339345

346+
if (transition) {
347+
let { transition: _transition, open, ...rest } = props
348+
return (
349+
<WithTransitionWrapper.Provider value={true}>
350+
<Transition show={open}>
351+
<Dialog ref={ref} {...rest} />
352+
</Transition>
353+
</WithTransitionWrapper.Provider>
354+
)
355+
}
356+
340357
return (
341358
<>
342359
<ForcePortalRoot force={true}>
@@ -416,13 +433,62 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
416433
onClick: handleClick,
417434
}
418435

419-
return render({
420-
ourProps,
421-
theirProps,
422-
slot,
423-
defaultTag: DEFAULT_PANEL_TAG,
424-
name: 'Dialog.Panel',
425-
})
436+
let Wrapper = useContext(WithTransitionWrapper) ? TransitionChild : Fragment
437+
438+
return (
439+
<WithTransitionWrapper.Provider value={false}>
440+
<Wrapper>
441+
{render({
442+
ourProps,
443+
theirProps,
444+
slot,
445+
defaultTag: DEFAULT_PANEL_TAG,
446+
name: 'Dialog.Panel',
447+
})}
448+
</Wrapper>
449+
</WithTransitionWrapper.Provider>
450+
)
451+
}
452+
453+
// ---
454+
455+
let DEFAULT_BACKDROP_TAG = 'div' as const
456+
type BackdropRenderPropArg = {
457+
open: boolean
458+
}
459+
460+
export type DialogBackdropProps<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG> = Props<
461+
TTag,
462+
BackdropRenderPropArg
463+
>
464+
465+
function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
466+
props: DialogBackdropProps<TTag>,
467+
ref: Ref<HTMLElement>
468+
) {
469+
let theirProps = props
470+
let [{ dialogState }] = useDialogContext('Dialog.Backdrop')
471+
472+
let slot = useMemo(
473+
() => ({ open: dialogState === DialogStates.Open }) satisfies BackdropRenderPropArg,
474+
[dialogState]
475+
)
476+
477+
let ourProps = { ref }
478+
479+
let Wrapper = useContext(WithTransitionWrapper) ? TransitionChild : Fragment
480+
481+
return (
482+
<Wrapper>
483+
{render({
484+
ourProps,
485+
theirProps,
486+
slot,
487+
defaultTag: DEFAULT_BACKDROP_TAG,
488+
name: 'Dialog.Backdrop',
489+
})}
490+
</Wrapper>
491+
)
426492
}
427493

428494
// ---
@@ -482,6 +548,12 @@ export interface _internal_ComponentDialogPanel extends HasDisplayName {
482548
): JSX.Element
483549
}
484550

551+
export interface _internal_ComponentDialogBackdrop extends HasDisplayName {
552+
<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
553+
props: DialogBackdropProps<TTag> & RefProp<typeof BackdropFn>
554+
): JSX.Element
555+
}
556+
485557
export interface _internal_ComponentDialogTitle extends HasDisplayName {
486558
<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
487559
props: DialogTitleProps<TTag> & RefProp<typeof TitleFn>
@@ -492,6 +564,7 @@ export interface _internal_ComponentDialogDescription extends _internal_Componen
492564

493565
let DialogRoot = forwardRefWithAs(DialogFn) as _internal_ComponentDialog
494566
export let DialogPanel = forwardRefWithAs(PanelFn) as _internal_ComponentDialogPanel
567+
export let DialogBackdrop = forwardRefWithAs(BackdropFn) as _internal_ComponentDialogBackdrop
495568
export let DialogTitle = forwardRefWithAs(TitleFn) as _internal_ComponentDialogTitle
496569
/** @deprecated use `<Description>` instead of `<DialogDescription>` */
497570
export let DialogDescription = Description as _internal_ComponentDialogDescription

packages/@headlessui-react/src/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ it('should expose the correct components', () => {
2424
'Description',
2525

2626
'Dialog',
27+
'DialogBackdrop',
2728
'DialogDescription',
2829
'DialogPanel',
2930
'DialogTitle',
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
2+
import { useState } from 'react'
3+
import { Button } from '../../components/button'
4+
import { classNames } from '../../utils/class-names'
5+
6+
export default function Home() {
7+
let [isOpen, setIsOpen] = useState(false)
8+
let [transition, setTransition] = useState(true)
9+
10+
return (
11+
<>
12+
<div className="flex gap-4 p-12">
13+
<Button onClick={() => setIsOpen((v) => !v)}>Toggle!</Button>
14+
<Button onClick={() => setTransition((v) => !v)}>
15+
<span>Toggle transition</span>
16+
<span
17+
className={classNames(
18+
'ml-2 inline-flex size-4 rounded-md',
19+
transition ? 'bg-green-500' : 'bg-red-500'
20+
)}
21+
></span>
22+
</Button>
23+
</div>
24+
25+
<Dialog
26+
open={isOpen}
27+
transition={transition}
28+
onClose={() => setIsOpen(false)}
29+
className="relative z-50"
30+
>
31+
<DialogBackdrop className="fixed inset-0 bg-black/30 duration-500 ease-out data-[closed]:opacity-0" />
32+
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
33+
<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">
34+
<h1 className="text-2xl font-bold">Dialog</h1>
35+
<p>
36+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed pulvinar, nunc nec
37+
vehicula fermentum, nunc sapien tristique ipsum, nec facilisis dolor sapien non dui.
38+
Nullam vel sapien ultrices, lacinia felis sit amet, fermentum odio. Nullam vel sapien
39+
ultrices, lacinia felis sit amet, fermentum odio.
40+
</p>
41+
<Button onClick={() => setIsOpen(false)}>Close</Button>
42+
</DialogPanel>
43+
</div>
44+
</Dialog>
45+
</>
46+
)
47+
}

0 commit comments

Comments
 (0)