From a45cb6ff6abbaa11bc0437aaaa1c40bb0ab26daf Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 3 May 2024 16:22:39 +0200 Subject: [PATCH] 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` --- packages/@headlessui-react/CHANGELOG.md | 1 + .../dialog-backdrop/dialog-backdrop.tsx | 3 - .../dialog-overlay/dialog-overlay.tsx | 3 - .../src/components/dialog/dialog.test.tsx | 274 +----------------- .../src/components/dialog/dialog.tsx | 128 -------- packages/@headlessui-react/src/index.test.ts | 2 - .../test-utils/accessibility-assertions.ts | 59 ---- playgrounds/react/pages/dialog/dialog.tsx | 6 +- 8 files changed, 9 insertions(+), 467 deletions(-) delete mode 100644 packages/@headlessui-react/src/components/dialog-backdrop/dialog-backdrop.tsx delete mode 100644 packages/@headlessui-react/src/components/dialog-overlay/dialog-overlay.tsx diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 88a2ebe..8e8f683 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -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 diff --git a/packages/@headlessui-react/src/components/dialog-backdrop/dialog-backdrop.tsx b/packages/@headlessui-react/src/components/dialog-backdrop/dialog-backdrop.tsx deleted file mode 100644 index 52624fc..0000000 --- a/packages/@headlessui-react/src/components/dialog-backdrop/dialog-backdrop.tsx +++ /dev/null @@ -1,3 +0,0 @@ -// Next.js barrel file improvements (GENERATED FILE) -export type * from '../dialog/dialog' -export { DialogBackdrop } from '../dialog/dialog' diff --git a/packages/@headlessui-react/src/components/dialog-overlay/dialog-overlay.tsx b/packages/@headlessui-react/src/components/dialog-overlay/dialog-overlay.tsx deleted file mode 100644 index 87a5ea5..0000000 --- a/packages/@headlessui-react/src/components/dialog-overlay/dialog-overlay.tsx +++ /dev/null @@ -1,3 +0,0 @@ -// Next.js barrel file improvements (GENERATED FILE) -export type * from '../dialog/dialog' -export { DialogOverlay } from '../dialog/dialog' diff --git a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx index 7b112c3..99df8e6 100644 --- a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx +++ b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx @@ -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 ', @@ -66,7 +60,6 @@ describe('Safe guards', () => { render( -

Contents

@@ -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 ( - <> - - - {overlay} - - - - ) - } - - render() - - 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 ( - <> - - - - - - - ) - } - - render() - - try { - await click(document.getElementById('trigger')) - - expect(true).toBe(false) - } catch (e: unknown) { - expect((e as Error).message).toBe( - 'A component is being used, but a 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 ( - <> - - - - - - - - - ) - } - - render() - - await click(document.getElementById('trigger')) - }) - ) - - it( - 'should portal the Dialog.Backdrop', - suppressConsoleLogs(async () => { - function Example() { - let [isOpen, setIsOpen] = useState(false) - return ( - <> - - - - - - - - - ) - } - - render() - - 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 ( - <> - - - - Contents - - - - ) - } - render() - - // 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 ( - <> - - - - - - Contents - - - - ) - } - render() - - // 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 ( -
- - Contents - - - -
- ) - } - - render() - - 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 - @@ -1700,7 +1442,6 @@ describe('Mouse interactions', () => { Trigger - @@ -1733,7 +1474,6 @@ describe('Mouse interactions', () => { Trigger - @@ -1767,7 +1507,6 @@ describe('Mouse interactions', () => {
this thing
- @@ -1816,8 +1555,6 @@ describe('Nesting', () => { return ( - -

Level: {level}

@@ -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 }) => { diff --git a/packages/@headlessui-react/src/components/dialog/dialog.tsx b/packages/@headlessui-react/src/components/dialog/dialog.tsx index 9b1330d..db93e77 100644 --- a/packages/@headlessui-react/src/components/dialog/dialog.tsx +++ b/packages/@headlessui-react/src/components/dialog/dialog.tsx @@ -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( // --- -let DEFAULT_OVERLAY_TAG = 'div' as const -type OverlayRenderPropArg = { - open: boolean -} -type OverlayPropsWeControl = 'aria-hidden' - -export type DialogOverlayProps = Props< - TTag, - OverlayRenderPropArg, - OverlayPropsWeControl -> - -function OverlayFn( - props: DialogOverlayProps, - ref: Ref -) { - 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 = Props< - TTag, - BackdropRenderPropArg, - BackdropPropsWeControl -> - -function BackdropFn( - props: DialogBackdropProps, - ref: Ref -) { - 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 component is being used, but a component is missing.` - ) - } - }, [state.panelRef]) - - let slot = useMemo( - () => ({ open: dialogState === DialogStates.Open }) satisfies BackdropRenderPropArg, - [dialogState] - ) - - let ourProps = { - ref: backdropRef, - id, - 'aria-hidden': true, - } - - return ( - - - {render({ - ourProps, - theirProps, - slot, - defaultTag: DEFAULT_BACKDROP_TAG, - name: 'Dialog.Backdrop', - })} - - - ) -} - -// --- - 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 { - ( - props: DialogBackdropProps & RefProp - ): JSX.Element -} - export interface _internal_ComponentDialogPanel extends HasDisplayName { ( props: DialogPanelProps & RefProp ): JSX.Element } -export interface _internal_ComponentDialogOverlay extends HasDisplayName { - ( - props: DialogOverlayProps & RefProp - ): JSX.Element -} - export interface _internal_ComponentDialogTitle extends HasDisplayName { ( props: DialogTitleProps & RefProp @@ -659,21 +537,15 @@ export interface _internal_ComponentDialogDescription extends _internal_Componen let DialogRoot = forwardRefWithAs(DialogFn) as _internal_ComponentDialog /** @deprecated use a plain `
` instead of `` */ -export let DialogBackdrop = forwardRefWithAs(BackdropFn) as _internal_ComponentDialogBackdrop export let DialogPanel = forwardRefWithAs(PanelFn) as _internal_ComponentDialogPanel /** @deprecated use a plain `
` instead of `` */ -export let DialogOverlay = forwardRefWithAs(OverlayFn) as _internal_ComponentDialogOverlay export let DialogTitle = forwardRefWithAs(TitleFn) as _internal_ComponentDialogTitle /** @deprecated use `` instead of `` */ export let DialogDescription = Description as _internal_ComponentDialogDescription export let Dialog = Object.assign(DialogRoot, { - /** @deprecated use a plain `
` instead of `` */ - Backdrop: DialogBackdrop, /** @deprecated use `` instead of `` */ Panel: DialogPanel, - /** @deprecated use a plain `
` instead of `` */ - Overlay: DialogOverlay, /** @deprecated use `` instead of `` */ Title: DialogTitle, /** @deprecated use `` instead of `` */ diff --git a/packages/@headlessui-react/src/index.test.ts b/packages/@headlessui-react/src/index.test.ts index 02082ce..ea86956 100644 --- a/packages/@headlessui-react/src/index.test.ts +++ b/packages/@headlessui-react/src/index.test.ts @@ -24,9 +24,7 @@ it('should expose the correct components', () => { 'Description', 'Dialog', - 'DialogBackdrop', 'DialogDescription', - 'DialogOverlay', 'DialogPanel', 'DialogTitle', diff --git a/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts b/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts index f48e303..c364e72 100644 --- a/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts +++ b/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts @@ -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 - 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 { diff --git a/playgrounds/react/pages/dialog/dialog.tsx b/playgrounds/react/pages/dialog/dialog.tsx index c6a5f2f..3ceb653 100644 --- a/playgrounds/react/pages/dialog/dialog.tsx +++ b/playgrounds/react/pages/dialog/dialog.tsx @@ -21,8 +21,8 @@ function Nested({ onClose, level = 0 }) { return ( <> - -
+ setShowChild(true)}>Open (2)
-
+
{showChild && setShowChild(false)} level={level + 1} />}