Remove deprecated DialogBackdrop and DialogOverlay components (#3171)

* remove `DialogBackdrop` and `DialogOverlay`

We deprecated those components in v1.6, since they are no longer
documented and this is a major release, we can safely get rid of it.

* update changelog

* migrate playground example to use `Dialog.Panel`
This commit is contained in:
Robin Malfait
2024-05-03 16:22:39 +02:00
committed by GitHub
parent 1ae1af72ab
commit a45cb6ff6a
8 changed files with 9 additions and 467 deletions
+1
View File
@@ -53,6 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Deprecate the `entered` prop on the `Transition` component ([#3089](https://github.com/tailwindlabs/headlessui/pull/3089))
- Deprecate dot notation for components ([#2887](https://github.com/tailwindlabs/headlessui/pull/2887), [#3170](https://github.com/tailwindlabs/headlessui/pull/3170))
- Add frozen value to `ComboboxOptions` component ([#3126](https://github.com/tailwindlabs/headlessui/pull/3126))
- Remove deprecated `DialogBackdrop` and `DialogOverlay` components ([#3171](https://github.com/tailwindlabs/headlessui/pull/3171))
## [1.7.19] - 2024-04-15
@@ -1,3 +0,0 @@
// Next.js barrel file improvements (GENERATED FILE)
export type * from '../dialog/dialog'
export { DialogBackdrop } from '../dialog/dialog'
@@ -1,3 +0,0 @@
// Next.js barrel file improvements (GENERATED FILE)
export type * from '../dialog/dialog'
export { DialogOverlay } from '../dialog/dialog'
@@ -6,15 +6,11 @@ import {
assertActiveElement,
assertDialog,
assertDialogDescription,
assertDialogOverlay,
assertDialogTitle,
assertPopoverPanel,
DialogState,
getByText,
getDialog,
getDialogBackdrop,
getDialogOverlay,
getDialogOverlays,
getDialogs,
getPopoverButton,
PopoverState,
@@ -46,9 +42,7 @@ function TabSentinel(props: PropsOf<'button'>) {
describe('Safe guards', () => {
it.each([
['Dialog.Overlay', Dialog.Overlay],
['Dialog.Title', Dialog.Title],
['Dialog.Backdrop', Dialog.Backdrop],
['Dialog.Panel', Dialog.Panel],
])(
'should error when we are using a <%s /> without a parent <Dialog />',
@@ -66,7 +60,6 @@ describe('Safe guards', () => {
render(
<Dialog autoFocus={false} open={false} onClose={console.log}>
<button>Trigger</button>
<Dialog.Overlay />
<Dialog.Title />
<p>Contents</p>
<Dialog.Description />
@@ -553,148 +546,6 @@ describe('Rendering', () => {
)
})
describe('Dialog.Overlay', () => {
it(
'should be possible to render Dialog.Overlay using a render prop',
suppressConsoleLogs(async () => {
let overlay = jest.fn().mockReturnValue(null)
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger
</button>
<Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
<Dialog.Overlay>{overlay}</Dialog.Overlay>
<TabSentinel />
</Dialog>
</>
)
}
render(<Example />)
assertDialogOverlay({
state: DialogState.InvisibleUnmounted,
attributes: { id: 'headlessui-dialog-overlay-2' },
})
await click(document.getElementById('trigger'))
assertDialogOverlay({
state: DialogState.Visible,
attributes: { id: 'headlessui-dialog-overlay-2' },
})
expect(overlay).toHaveBeenCalledWith({ open: true })
})
)
})
describe('Dialog.Backdrop', () => {
it(
'should throw an error if a Dialog.Backdrop is used without a Dialog.Panel',
suppressConsoleLogs(async () => {
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger
</button>
<Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
<Dialog.Backdrop />
<TabSentinel />
</Dialog>
</>
)
}
render(<Example />)
try {
await click(document.getElementById('trigger'))
expect(true).toBe(false)
} catch (e: unknown) {
expect((e as Error).message).toBe(
'A <Dialog.Backdrop /> component is being used, but a <Dialog.Panel /> component is missing.'
)
}
})
)
it(
'should not throw an error if a Dialog.Backdrop is used with a Dialog.Panel',
suppressConsoleLogs(async () => {
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger
</button>
<Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
<Dialog.Backdrop />
<Dialog.Panel>
<TabSentinel />
</Dialog.Panel>
</Dialog>
</>
)
}
render(<Example />)
await click(document.getElementById('trigger'))
})
)
it(
'should portal the Dialog.Backdrop',
suppressConsoleLogs(async () => {
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger
</button>
<Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
<Dialog.Backdrop />
<Dialog.Panel>
<TabSentinel />
</Dialog.Panel>
</Dialog>
</>
)
}
render(<Example />)
await click(document.getElementById('trigger'))
let dialog = getDialog()
let backdrop = getDialogBackdrop()
expect(dialog).not.toBe(null)
dialog = dialog as HTMLElement
expect(backdrop).not.toBe(null)
backdrop = backdrop as HTMLElement
// It should not be nested
let position = dialog.compareDocumentPosition(backdrop)
expect(position & Node.DOCUMENT_POSITION_CONTAINED_BY).not.toBe(
Node.DOCUMENT_POSITION_CONTAINED_BY
)
// It should be a sibling
expect(position & Node.DOCUMENT_POSITION_FOLLOWING).toBe(Node.DOCUMENT_POSITION_FOLLOWING)
})
)
})
describe('Dialog.Title', () => {
it(
'should be possible to render Dialog.Title using a render prop',
@@ -1134,76 +985,6 @@ describe('Keyboard interactions', () => {
})
describe('Mouse interactions', () => {
it(
'should be possible to close a Dialog using a click on the Dialog.Overlay',
suppressConsoleLogs(async () => {
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger
</button>
<Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
<Dialog.Overlay />
Contents
<TabSentinel />
</Dialog>
</>
)
}
render(<Example />)
// Open dialog
await click(document.getElementById('trigger'))
// Verify it is open
assertDialog({ state: DialogState.Visible })
// Click to close
await click(getDialogOverlay())
// Verify it is closed
assertDialog({ state: DialogState.InvisibleUnmounted })
})
)
it(
'should not close the Dialog when clicking on contents of the Dialog.Overlay',
suppressConsoleLogs(async () => {
function Example() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger
</button>
<Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
<Dialog.Overlay>
<button>hi</button>
</Dialog.Overlay>
Contents
<TabSentinel />
</Dialog>
</>
)
}
render(<Example />)
// Open dialog
await click(document.getElementById('trigger'))
// Verify it is open
assertDialog({ state: DialogState.Visible })
// Click on an element inside the overlay
await click(getByText('hi'))
// Verify it is still open
assertDialog({ state: DialogState.Visible })
})
)
it(
'should be possible to close the dialog, and re-focus the button when we click outside on the body element',
suppressConsoleLogs(async () => {
@@ -1273,44 +1054,6 @@ describe('Mouse interactions', () => {
})
)
it(
'should stop propagating click events when clicking on the Dialog.Overlay',
suppressConsoleLogs(async () => {
let wrapperFn = jest.fn()
function Example() {
let [isOpen, setIsOpen] = useState(true)
return (
<div onClick={wrapperFn}>
<Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
Contents
<Dialog.Overlay />
<TabSentinel />
</Dialog>
</div>
)
}
render(<Example />)
await nextFrame()
// Verify it is open
assertDialog({ state: DialogState.Visible })
// Verify that the wrapper function has not been called yet
expect(wrapperFn).toHaveBeenCalledTimes(0)
// Click the Dialog.Overlay to close the Dialog
await click(getDialogOverlay())
// Verify it is closed
assertDialog({ state: DialogState.InvisibleUnmounted })
// Verify that the wrapper function has not been called yet
expect(wrapperFn).toHaveBeenCalledTimes(0)
})
)
it(
'should be possible to submit a form inside a Dialog',
suppressConsoleLogs(async () => {
@@ -1667,7 +1410,6 @@ describe('Mouse interactions', () => {
Trigger
</button>
<Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
<Dialog.Backdrop />
<Dialog.Panel>
<TabSentinel />
</Dialog.Panel>
@@ -1700,7 +1442,6 @@ describe('Mouse interactions', () => {
Trigger
</button>
<Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
<Dialog.Backdrop />
<Dialog.Panel>
<button id="inside">Inside</button>
<TabSentinel />
@@ -1733,7 +1474,6 @@ describe('Mouse interactions', () => {
Trigger
</button>
<Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
<Dialog.Backdrop />
<Dialog.Panel>
<button id="inside">Inside</button>
<TabSentinel />
@@ -1767,7 +1507,6 @@ describe('Mouse interactions', () => {
</button>
<div id="i-am-outside">this thing</div>
<Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
<Dialog.Backdrop />
<Dialog.Panel>
<button id="inside">Inside</button>
<TabSentinel />
@@ -1816,8 +1555,6 @@ describe('Nesting', () => {
return (
<Dialog autoFocus={false} open={open} onClose={onClose}>
<Dialog.Overlay />
<div>
<p>Level: {level}</p>
<button onClick={() => setShowChild(true)}>Open {level + 1} a</button>
@@ -1850,12 +1587,11 @@ describe('Nesting', () => {
}
it.each`
strategy | when | action
${'with `Escape`'} | ${'mounted'} | ${() => press(Keys.Escape)}
${'with `Outside Click`'} | ${'mounted'} | ${() => click(document.body)}
${'with `Click on Dialog.Overlay`'} | ${'mounted'} | ${() => click(getDialogOverlays().pop()!)}
${'with `Escape`'} | ${'always'} | ${() => press(Keys.Escape)}
${'with `Outside Click`'} | ${'always'} | ${() => click(document.body)}
strategy | when | action
${'with `Escape`'} | ${'mounted'} | ${() => press(Keys.Escape)}
${'with `Outside Click`'} | ${'mounted'} | ${() => click(document.body)}
${'with `Escape`'} | ${'always'} | ${() => press(Keys.Escape)}
${'with `Outside Click`'} | ${'always'} | ${() => click(document.body)}
`(
'should be possible to open nested Dialog components (visible when $when) and close them $strategy',
async ({ when, action }) => {
@@ -35,7 +35,6 @@ import { State, useOpenClosed } from '../../internal/open-closed'
import { ForcePortalRoot } from '../../internal/portal-force-root'
import { StackMessage, StackProvider } from '../../internal/stack-context'
import type { Props } from '../../types'
import { isDisabledReactIssue7711 } from '../../utils/bugs'
import { match } from '../../utils/match'
import {
RenderFeatures,
@@ -426,115 +425,6 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
// ---
let DEFAULT_OVERLAY_TAG = 'div' as const
type OverlayRenderPropArg = {
open: boolean
}
type OverlayPropsWeControl = 'aria-hidden'
export type DialogOverlayProps<TTag extends ElementType = typeof DEFAULT_OVERLAY_TAG> = Props<
TTag,
OverlayRenderPropArg,
OverlayPropsWeControl
>
function OverlayFn<TTag extends ElementType = typeof DEFAULT_OVERLAY_TAG>(
props: DialogOverlayProps<TTag>,
ref: Ref<HTMLElement>
) {
let internalId = useId()
let { id = `headlessui-dialog-overlay-${internalId}`, ...theirProps } = props
let [{ dialogState, close }] = useDialogContext('Dialog.Overlay')
let overlayRef = useSyncRefs(ref)
let handleClick = useEvent((event: ReactMouseEvent) => {
if (event.target !== event.currentTarget) return
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
event.preventDefault()
event.stopPropagation()
close()
})
let slot = useMemo(
() => ({ open: dialogState === DialogStates.Open }) satisfies OverlayRenderPropArg,
[dialogState]
)
let ourProps = {
ref: overlayRef,
id,
'aria-hidden': true,
onClick: handleClick,
}
return render({
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_OVERLAY_TAG,
name: 'Dialog.Overlay',
})
}
// ---
let DEFAULT_BACKDROP_TAG = 'div' as const
type BackdropRenderPropArg = {
open: boolean
}
type BackdropPropsWeControl = 'aria-hidden'
export type DialogBackdropProps<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG> = Props<
TTag,
BackdropRenderPropArg,
BackdropPropsWeControl
>
function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
props: DialogBackdropProps<TTag>,
ref: Ref<HTMLElement>
) {
let internalId = useId()
let { id = `headlessui-dialog-backdrop-${internalId}`, ...theirProps } = props
let [{ dialogState }, state] = useDialogContext('Dialog.Backdrop')
let backdropRef = useSyncRefs(ref)
useEffect(() => {
if (state.panelRef.current === null) {
throw new Error(
`A <Dialog.Backdrop /> component is being used, but a <Dialog.Panel /> component is missing.`
)
}
}, [state.panelRef])
let slot = useMemo(
() => ({ open: dialogState === DialogStates.Open }) satisfies BackdropRenderPropArg,
[dialogState]
)
let ourProps = {
ref: backdropRef,
id,
'aria-hidden': true,
}
return (
<ForcePortalRoot force>
<Portal>
{render({
ourProps,
theirProps,
slot,
defaultTag: DEFAULT_BACKDROP_TAG,
name: 'Dialog.Backdrop',
})}
</Portal>
</ForcePortalRoot>
)
}
// ---
let DEFAULT_PANEL_TAG = 'div' as const
type PanelRenderPropArg = {
open: boolean
@@ -631,24 +521,12 @@ export interface _internal_ComponentDialog 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_ComponentDialogPanel extends HasDisplayName {
<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
props: DialogPanelProps<TTag> & RefProp<typeof PanelFn>
): JSX.Element
}
export interface _internal_ComponentDialogOverlay extends HasDisplayName {
<TTag extends ElementType = typeof DEFAULT_OVERLAY_TAG>(
props: DialogOverlayProps<TTag> & RefProp<typeof OverlayFn>
): JSX.Element
}
export interface _internal_ComponentDialogTitle extends HasDisplayName {
<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
props: DialogTitleProps<TTag> & RefProp<typeof TitleFn>
@@ -659,21 +537,15 @@ export interface _internal_ComponentDialogDescription extends _internal_Componen
let DialogRoot = forwardRefWithAs(DialogFn) as _internal_ComponentDialog
/** @deprecated use a plain `<div>` instead of `<DialogBackdrop>` */
export let DialogBackdrop = forwardRefWithAs(BackdropFn) as _internal_ComponentDialogBackdrop
export let DialogPanel = forwardRefWithAs(PanelFn) as _internal_ComponentDialogPanel
/** @deprecated use a plain `<div>` instead of `<DialogOverlay>` */
export let DialogOverlay = forwardRefWithAs(OverlayFn) as _internal_ComponentDialogOverlay
export let DialogTitle = forwardRefWithAs(TitleFn) as _internal_ComponentDialogTitle
/** @deprecated use `<Description>` instead of `<DialogDescription>` */
export let DialogDescription = Description as _internal_ComponentDialogDescription
export let Dialog = Object.assign(DialogRoot, {
/** @deprecated use a plain `<div>` instead of `<Dialog.Backdrop>` */
Backdrop: DialogBackdrop,
/** @deprecated use `<DialogPanel>` instead of `<Dialog.Panel>` */
Panel: DialogPanel,
/** @deprecated use a plain `<div>` instead of `<Dialog.Overlay>` */
Overlay: DialogOverlay,
/** @deprecated use `<DialogTitle>` instead of `<Dialog.Title>` */
Title: DialogTitle,
/** @deprecated use `<Description>` instead of `<Dialog.Description>` */
@@ -24,9 +24,7 @@ it('should expose the correct components', () => {
'Description',
'Dialog',
'DialogBackdrop',
'DialogDescription',
'DialogOverlay',
'DialogPanel',
'DialogTitle',
@@ -1490,18 +1490,6 @@ export function getDialogDescription(): HTMLElement | null {
return document.querySelector('[id^="headlessui-description-"]')
}
export function getDialogOverlay(): HTMLElement | null {
return document.querySelector('[id^="headlessui-dialog-overlay-"]')
}
export function getDialogBackdrop(): HTMLElement | null {
return document.querySelector('[id^="headlessui-dialog-backdrop-"]')
}
export function getDialogOverlays(): HTMLElement[] {
return Array.from(document.querySelectorAll('[id^="headlessui-dialog-overlay-"]'))
}
// ---
export enum DialogState {
@@ -1682,53 +1670,6 @@ export function assertDialogDescription(
}
}
export function assertDialogOverlay(
options: {
attributes?: Record<string, string | null>
textContent?: string
state: DialogState
},
overlay = getDialogOverlay()
) {
try {
switch (options.state) {
case DialogState.InvisibleHidden:
if (overlay === null) return expect(overlay).not.toBe(null)
assertHidden(overlay)
if (options.textContent) expect(overlay).toHaveTextContent(options.textContent)
for (let attributeName in options.attributes) {
expect(overlay).toHaveAttribute(attributeName, options.attributes[attributeName])
}
break
case DialogState.Visible:
if (overlay === null) return expect(overlay).not.toBe(null)
assertVisible(overlay)
if (options.textContent) expect(overlay).toHaveTextContent(options.textContent)
for (let attributeName in options.attributes) {
expect(overlay).toHaveAttribute(attributeName, options.attributes[attributeName])
}
break
case DialogState.InvisibleUnmounted:
expect(overlay).toBe(null)
break
default:
assertNever(options.state)
}
} catch (err) {
if (err instanceof Error) Error.captureStackTrace(err, assertDialogOverlay)
throw err
}
}
// ---
export function getRadioGroup(): HTMLElement | null {
+3 -3
View File
@@ -21,8 +21,8 @@ function Nested({ onClose, level = 0 }) {
return (
<>
<Dialog open={true} onClose={onClose} className="fixed inset-0 z-10">
<Dialog.Overlay className="fixed inset-0 bg-gray-500 opacity-25" />
<div
<div className="fixed inset-0 bg-gray-500 opacity-25" />
<Dialog.Panel
className="fixed left-12 top-24 z-10 w-96 bg-white p-4"
style={{
transform: `translate(calc(50px * ${level}), calc(50px * ${level}))`,
@@ -34,7 +34,7 @@ function Nested({ onClose, level = 0 }) {
<Button onClick={() => setShowChild(true)}>Open (2)</Button>
<Button onClick={() => setShowChild(true)}>Open (3)</Button>
</div>
</div>
</Dialog.Panel>
{showChild && <Nested onClose={() => setShowChild(false)} level={level + 1} />}
</Dialog>
</>