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 ( - - {title} - {(variant === 'error' || variant === 'warning') && ( - - )} - - {children} - - {cancelLabel && ( - + + + {title} + {(variant === 'error' || variant === 'warning') && ( + )} - {secondaryActionLabel && ( + + {children} + + {cancelLabel && ( + + )} + {secondaryActionLabel && ( + + )} - )} - - - + + + ); }); 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); + }); });