diff --git a/packages/@adobe/react-spectrum/src/dialog/AlertDialog.tsx b/packages/@adobe/react-spectrum/src/dialog/AlertDialog.tsx
index e93f0ae8325..54ac621e20a 100644
--- a/packages/@adobe/react-spectrum/src/dialog/AlertDialog.tsx
+++ b/packages/@adobe/react-spectrum/src/dialog/AlertDialog.tsx
@@ -65,7 +65,8 @@ export const AlertDialog = forwardRef(function AlertDialog(
props: SpectrumAlertDialogProps,
ref: DOMRef
) {
- let {onClose = () => {}} = useContext(DialogContext) || ({} as DialogContextValue);
+ let outerContext = useContext(DialogContext) || ({} as DialogContextValue);
+ let {onClose = () => {}} = outerContext;
let {
variant,
@@ -94,54 +95,70 @@ export const AlertDialog = forwardRef(function AlertDialog(
}
}
+ // Provide a context that forwards the onCancel callback when Escape dismisses the dialog.
+ // The Escape key is handled by the overlay (Modal/Tray), which calls state.close() directly
+ // and bypasses the cancel button's onPress handler. By injecting an onKeyDown into the
+ // dialog's context we ensure onCancel fires for keyboard-initiated dismissals too.
+ let contextWithEscapeCancel: DialogContextValue = {
+ ...outerContext,
+ onKeyDown: e => {
+ if (e.key === 'Escape' && props.onCancel) {
+ props.onCancel();
+ }
+ outerContext.onKeyDown?.(e);
+ }
+ };
+
return (
-
+
);
});
diff --git a/packages/@adobe/react-spectrum/src/dialog/Dialog.tsx b/packages/@adobe/react-spectrum/src/dialog/Dialog.tsx
index 4bdcc4f298c..b6f0392e458 100644
--- a/packages/@adobe/react-spectrum/src/dialog/Dialog.tsx
+++ b/packages/@adobe/react-spectrum/src/dialog/Dialog.tsx
@@ -55,7 +55,11 @@ let sizeMap = {
*/
export const Dialog = React.forwardRef(function Dialog(props: SpectrumDialogProps, ref: DOMRef) {
props = useSlotProps(props, 'dialog');
- let {type = 'modal', ...contextProps} = useContext(DialogContext) || ({} as DialogContextValue);
+ let {
+ type = 'modal',
+ onKeyDown: contextOnKeyDown,
+ ...contextProps
+ } = useContext(DialogContext) || ({} as DialogContextValue);
let {
children,
isDismissable = contextProps.isDismissable,
@@ -72,6 +76,9 @@ export const Dialog = React.forwardRef(function Dialog(props: SpectrumDialogProp
let gridRef = useRef(null);
let sizeVariant = sizeMap[type] || sizeMap[size];
let {dialogProps, titleProps} = useDialog(mergeProps(contextProps, props), domRef);
+ if (contextOnKeyDown) {
+ dialogProps = mergeProps(dialogProps, {onKeyDown: contextOnKeyDown});
+ }
let hasHeader = useHasChild(`.${styles['spectrum-Dialog-header']}`, unwrapDOMRef(gridRef));
let hasHeading = useHasChild(`.${styles['spectrum-Dialog-heading']}`, unwrapDOMRef(gridRef));
diff --git a/packages/@adobe/react-spectrum/test/dialog/AlertDialog.test.js b/packages/@adobe/react-spectrum/test/dialog/AlertDialog.test.js
index 9a85f56405a..27c4e2ec5d3 100644
--- a/packages/@adobe/react-spectrum/test/dialog/AlertDialog.test.js
+++ b/packages/@adobe/react-spectrum/test/dialog/AlertDialog.test.js
@@ -246,4 +246,50 @@ describe('AlertDialog', function () {
let primaryBtn = getByTestId('rsp-AlertDialog-confirmButton');
expect(primaryBtn).toBeDefined();
});
+
+ it('fires onCancel when Escape key is pressed', async function () {
+ let user = userEvent.setup({delay: null, pointerMap});
+ let onCancelSpy = jest.fn();
+ let {getByRole} = render(
+
+
+ Content body
+
+
+ );
+
+ let dialog = getByRole('alertdialog');
+ expect(document.activeElement).toBe(dialog);
+
+ await user.keyboard('{Escape}');
+ expect(onCancelSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not fire onCancel on Escape when onCancel prop is not provided', async function () {
+ let user = userEvent.setup({delay: null, pointerMap});
+ let onPrimaryAction = jest.fn();
+ let {getByRole} = render(
+
+
+ Content body
+
+
+ );
+
+ let dialog = getByRole('alertdialog');
+ expect(document.activeElement).toBe(dialog);
+
+ // Should not throw or call anything unexpected
+ await user.keyboard('{Escape}');
+ expect(onPrimaryAction).toHaveBeenCalledTimes(0);
+ });
});