Skip to content

Commit

Permalink
[Dialog] Modality changes
Browse files Browse the repository at this point in the history
  • Loading branch information
michaldudak committed Dec 9, 2024
1 parent 8b560c4 commit 9a01a54
Show file tree
Hide file tree
Showing 13 changed files with 251 additions and 34 deletions.
2 changes: 1 addition & 1 deletion docs/reference/generated/alert-dialog-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"description": "Determines whether the dialog is initally open.\nThis is an uncontrolled equivalent of the `open` prop."
},
"onOpenChange": {
"type": "(open, event) => void",
"type": "(open, event, reason) => void",
"description": "Callback invoked when the dialog is being opened or closed."
},
"open": {
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/generated/dialog-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"description": "Determines whether the dialog is modal."
},
"onOpenChange": {
"type": "(open, event) => void",
"type": "(open, event, reason) => void",
"description": "Callback invoked when the dialog is being opened or closed."
},
"open": {
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/overrides/alert-dialog-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "AlertDialogRoot",
"props": {
"onOpenChange": {
"type": "(open, event) => void"
"type": "(open, event, reason) => void"
}
}
}
2 changes: 1 addition & 1 deletion docs/reference/overrides/dialog-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "DialogRoot",
"props": {
"onOpenChange": {
"type": "(open, event) => void"
"type": "(open, event, reason) => void"
}
}
}
4 changes: 2 additions & 2 deletions packages/react/src/alert-dialog/close/AlertDialogClose.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const AlertDialogClose = React.forwardRef(function AlertDialogClose(
forwardedRef: React.ForwardedRef<HTMLButtonElement>,
) {
const { render, className, ...other } = props;
const { open, onOpenChange } = useAlertDialogRootContext();
const { getRootProps } = useDialogClose({ open, onOpenChange });
const { open, setOpen } = useAlertDialogRootContext();
const { getRootProps } = useDialogClose({ open, setOpen });

const { renderElement } = useComponentRenderer({
render: render ?? 'button',
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/alert-dialog/popup/AlertDialogPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const AlertDialogPopup = React.forwardRef(function AlertDialogPopup(
getPopupProps,
mounted,
nestedOpenDialogCount,
onOpenChange,
setOpen,
open,
openMethod,
popupRef,
Expand All @@ -78,7 +78,7 @@ const AlertDialogPopup = React.forwardRef(function AlertDialogPopup(
initialFocus,
modal: true,
mounted,
onOpenChange,
setOpen,
open,
openMethod,
ref: mergedRef,
Expand Down
73 changes: 73 additions & 0 deletions packages/react/src/alert-dialog/root/AlertDialogRoot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,85 @@ import { expect } from 'chai';
import { describeSkipIf, screen, waitFor } from '@mui/internal-test-utils';
import { AlertDialog } from '@base-ui-components/react/alert-dialog';
import { createRenderer } from '#test-utils';
import { spy } from 'sinon';

const isJSDOM = /jsdom/.test(window.navigator.userAgent);

describe('<AlertDialog.Root />', () => {
const { render } = createRenderer();

describe('prop: onOpenChange', () => {
it('calls onOpenChange with the new open state', async () => {
const handleOpenChange = spy();

const { user } = await render(
<AlertDialog.Root onOpenChange={handleOpenChange}>
<AlertDialog.Trigger>Open</AlertDialog.Trigger>
<AlertDialog.Popup>
<AlertDialog.Close>Close</AlertDialog.Close>
</AlertDialog.Popup>
</AlertDialog.Root>,
);

expect(handleOpenChange.callCount).to.equal(0);

const openButton = screen.getByText('Open');
await user.click(openButton);

expect(handleOpenChange.callCount).to.equal(1);
expect(handleOpenChange.firstCall.args[0]).to.equal(true);

const closeButton = screen.getByText('Close');
await user.click(closeButton);

expect(handleOpenChange.callCount).to.equal(2);
expect(handleOpenChange.secondCall.args[0]).to.equal(false);
});

it('calls onOpenChange with the reason for change when clicked on trigger and close button', async () => {
const handleOpenChange = spy();

const { user } = await render(
<AlertDialog.Root onOpenChange={handleOpenChange}>
<AlertDialog.Trigger>Open</AlertDialog.Trigger>
<AlertDialog.Popup>
<AlertDialog.Close>Close</AlertDialog.Close>
</AlertDialog.Popup>
</AlertDialog.Root>,
);

const openButton = screen.getByText('Open');
await user.click(openButton);

expect(handleOpenChange.callCount).to.equal(1);
expect(handleOpenChange.firstCall.args[2]).to.equal('click');

const closeButton = screen.getByText('Close');
await user.click(closeButton);

expect(handleOpenChange.callCount).to.equal(2);
expect(handleOpenChange.secondCall.args[2]).to.equal('click');
});

it('calls onOpenChange with the reason for change when pressed Esc while the dialog is open', async () => {
const handleOpenChange = spy();

const { user } = await render(
<AlertDialog.Root defaultOpen onOpenChange={handleOpenChange}>
<AlertDialog.Trigger>Open</AlertDialog.Trigger>
<AlertDialog.Popup>
<AlertDialog.Close>Close</AlertDialog.Close>
</AlertDialog.Popup>
</AlertDialog.Root>,
);

await user.keyboard('[Escape]');

expect(handleOpenChange.callCount).to.equal(1);
expect(handleOpenChange.firstCall.args[2]).to.equal('escape-key');
});
});

describeSkipIf(isJSDOM)('modality', () => {
it('makes other interactive elements on the page inert when a modal dialog is open and restores them after the dialog is closed', async () => {
const { user } = await render(
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/dialog/close/DialogClose.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const DialogClose = React.forwardRef(function DialogClose(
forwardedRef: React.ForwardedRef<HTMLButtonElement>,
) {
const { render, className, ...other } = props;
const { open, onOpenChange } = useDialogRootContext();
const { getRootProps } = useDialogClose({ open, onOpenChange });
const { open, setOpen } = useDialogRootContext();
const { getRootProps } = useDialogClose({ open, setOpen });

const { renderElement } = useComponentRenderer({
render: render ?? 'button',
Expand Down
22 changes: 15 additions & 7 deletions packages/react/src/dialog/close/useDialogClose.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
'use client';
import * as React from 'react';
import { mergeReactProps } from '../../utils/mergeReactProps';
import { OpenChangeReason } from '../../utils/translateOpenChangeReason';

export function useDialogClose(params: useDialogClose.Parameters): useDialogClose.ReturnValue {
const { open, onOpenChange } = params;
const handleClick = React.useCallback(() => {
if (open) {
onOpenChange?.(false);
}
}, [open, onOpenChange]);
const { open, setOpen } = params;
const handleClick = React.useCallback(
(event: React.MouseEvent) => {
if (open) {
setOpen(false, event.nativeEvent, 'click');
}
},
[open, setOpen],
);

const getRootProps = (externalProps: React.HTMLAttributes<any>) =>
mergeReactProps(externalProps, { onClick: handleClick });
Expand All @@ -27,7 +31,11 @@ export namespace useDialogClose {
/**
* Callback invoked when the dialog is being opened or closed.
*/
onOpenChange: (open: boolean) => void;
setOpen: (
open: boolean,
event: Event | undefined,
reason: OpenChangeReason | undefined,
) => void;
}

export interface ReturnValue {
Expand Down
8 changes: 4 additions & 4 deletions packages/react/src/dialog/popup/DialogPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const DialogPopup = React.forwardRef(function DialogPopup(
modal,
mounted,
nestedOpenDialogCount,
onOpenChange,
setOpen,
open,
openMethod,
popupRef,
Expand All @@ -82,7 +82,7 @@ const DialogPopup = React.forwardRef(function DialogPopup(
initialFocus,
modal,
mounted,
onOpenChange,
setOpen,
open,
openMethod,
ref: mergedRef,
Expand Down Expand Up @@ -117,12 +117,12 @@ const DialogPopup = React.forwardRef(function DialogPopup(
<FloatingPortal root={container}>
<FloatingFocusManager
context={floatingContext}
modal={modal}
modal
disabled={!mounted}
closeOnFocusOut={dismissible}
initialFocus={resolvedInitialFocus}
returnFocus={finalFocus}
outsideElementsInert
outsideElementsInert={modal}
>
{renderElement()}
</FloatingFocusManager>
Expand Down
28 changes: 24 additions & 4 deletions packages/react/src/dialog/popup/useDialogPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
'use client';
import * as React from 'react';
import { type FloatingRootContext, useFloating, type FloatingContext } from '@floating-ui/react';
import {
type FloatingRootContext,
useFloating,
type FloatingContext,
type OpenChangeReason as FloatingUIOpenChangeReason,
} from '@floating-ui/react';
import { useBaseUiId } from '../../utils/useBaseUiId';
import { useForkRef } from '../../utils/useForkRef';
import { mergeReactProps } from '../../utils/mergeReactProps';
import { useScrollLock } from '../../utils/useScrollLock';
import { useEnhancedEffect } from '../../utils/useEnhancedEffect';
import { type InteractionType } from '../../utils/useEnhancedClickHandler';
import { GenericHTMLProps } from '../../utils/types';
import {
translateOpenChangeReason,
type OpenChangeReason,
} from '../../utils/translateOpenChangeReason';

export function useDialogPopup(parameters: useDialogPopup.Parameters): useDialogPopup.ReturnValue {
const {
Expand All @@ -18,7 +27,7 @@ export function useDialogPopup(parameters: useDialogPopup.Parameters): useDialog
initialFocus,
modal,
mounted,
onOpenChange,
setOpen,
open,
openMethod,
ref,
Expand All @@ -27,9 +36,16 @@ export function useDialogPopup(parameters: useDialogPopup.Parameters): useDialog
titleElementId,
} = parameters;

const handleFloatingUIOpenChange = React.useCallback(
(isOpen: boolean, event: Event | undefined, reason: FloatingUIOpenChangeReason | undefined) => {
setOpen(isOpen, event, translateOpenChangeReason(reason));
},
[setOpen],
);

const { context, elements } = useFloating({
open,
onOpenChange,
onOpenChange: handleFloatingUIOpenChange,
rootContext: floatingRootContext,
});

Expand Down Expand Up @@ -112,7 +128,11 @@ export namespace useDialogPopup {
/**
* Callback fired when the dialog is requested to be opened or closed.
*/
onOpenChange: (open: boolean, event?: Event) => void;
setOpen: (
open: boolean,
event: Event | undefined,
reason: OpenChangeReason | undefined,
) => void;
/**
* The id of the title element associated with the dialog.
*/
Expand Down
90 changes: 90 additions & 0 deletions packages/react/src/dialog/root/DialogRoot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,96 @@ describe('<Dialog.Root />', () => {
});
});

describe('prop: onOpenChange', () => {
it('calls onOpenChange with the new open state', async () => {
const handleOpenChange = spy();

const { user } = await render(
<Dialog.Root onOpenChange={handleOpenChange}>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Popup>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Popup>
</Dialog.Root>,
);

expect(handleOpenChange.callCount).to.equal(0);

const openButton = screen.getByText('Open');
await user.click(openButton);

expect(handleOpenChange.callCount).to.equal(1);
expect(handleOpenChange.firstCall.args[0]).to.equal(true);

const closeButton = screen.getByText('Close');
await user.click(closeButton);

expect(handleOpenChange.callCount).to.equal(2);
expect(handleOpenChange.secondCall.args[0]).to.equal(false);
});

it('calls onOpenChange with the reason for change when clicked on trigger and close button', async () => {
const handleOpenChange = spy();

const { user } = await render(
<Dialog.Root onOpenChange={handleOpenChange}>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Popup>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Popup>
</Dialog.Root>,
);

const openButton = screen.getByText('Open');
await user.click(openButton);

expect(handleOpenChange.callCount).to.equal(1);
expect(handleOpenChange.firstCall.args[2]).to.equal('click');

const closeButton = screen.getByText('Close');
await user.click(closeButton);

expect(handleOpenChange.callCount).to.equal(2);
expect(handleOpenChange.secondCall.args[2]).to.equal('click');
});

it('calls onOpenChange with the reason for change when pressed Esc while the dialog is open', async () => {
const handleOpenChange = spy();

const { user } = await render(
<Dialog.Root defaultOpen onOpenChange={handleOpenChange}>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Popup>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Popup>
</Dialog.Root>,
);

await user.keyboard('[Escape]');

expect(handleOpenChange.callCount).to.equal(1);
expect(handleOpenChange.firstCall.args[2]).to.equal('escape-key');
});

it('calls onOpenChange with the reason for change when user clicks outside while the dialog is open', async () => {
const handleOpenChange = spy();

const { user } = await render(
<Dialog.Root defaultOpen onOpenChange={handleOpenChange}>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Popup>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Popup>
</Dialog.Root>,
);

await user.click(document.body);

expect(handleOpenChange.callCount).to.equal(1);
expect(handleOpenChange.firstCall.args[2]).to.equal('outside-press');
});
});

describeSkipIf(isJSDOM)('prop: modal', () => {
it('makes other interactive elements on the page inert when a modal dialog is open and restores them after the dialog is closed', async () => {
const { user } = await render(
Expand Down
Loading

0 comments on commit 9a01a54

Please sign in to comment.