diff --git a/e2e/components/Notifications/Notifications-test.avt.e2e.js b/e2e/components/Notifications/Notifications-test.avt.e2e.js index f1a76a6fd9fe..544f25fee4a6 100644 --- a/e2e/components/Notifications/Notifications-test.avt.e2e.js +++ b/e2e/components/Notifications/Notifications-test.avt.e2e.js @@ -41,7 +41,6 @@ test.describe('Notifications @avt', () => { await expect(actionButton).toBeVisible(); - await page.keyboard.press('Tab'); await expect(actionButton).toBeFocused(); await page.keyboard.press('Tab'); diff --git a/packages/react/src/components/Notification/Notification-test.js b/packages/react/src/components/Notification/Notification-test.js index 3dfc2d80f122..33f482a43a64 100644 --- a/packages/react/src/components/Notification/Notification-test.js +++ b/packages/react/src/components/Notification/Notification-test.js @@ -327,13 +327,7 @@ describe('ActionableNotification', () => { /> ); - // without focus being on/in the notification, it should not close via escape - await userEvent.keyboard('{Escape}'); - expect(onCloseButtonClick).toHaveBeenCalledTimes(0); - expect(onClose).toHaveBeenCalledTimes(0); - // after focus is placed, the notification should close via escape - await userEvent.tab(); await userEvent.keyboard('{Escape}'); expect(onCloseButtonClick).toHaveBeenCalledTimes(1); expect(onClose).toHaveBeenCalledTimes(1); diff --git a/packages/react/src/components/Notification/Notification.tsx b/packages/react/src/components/Notification/Notification.tsx index b6b2a4368ee3..2bc020e2cef2 100644 --- a/packages/react/src/components/Notification/Notification.tsx +++ b/packages/react/src/components/Notification/Notification.tsx @@ -37,6 +37,7 @@ import { keys, matches } from '../../internal/keyboard'; import { usePrefix } from '../../internal/usePrefix'; import { useId } from '../../internal/useId'; import { noopFn } from '../../internal/noopFn'; +import wrapFocus from '../../internal/wrapFocus'; /** * Conditionally call a callback when the escape key is pressed @@ -943,14 +944,38 @@ export function ActionableNotification({ [`${prefix}--actionable-notification--${kind}`]: kind, [`${prefix}--actionable-notification--hide-close-button`]: hideCloseButton, }); - + const innerModal = useRef(null); + const startTrap = useRef(null); + const endTrap = useRef(null); const ref = useRef(null); + useIsomorphicEffect(() => { - if (ref.current && hasFocus) { - ref.current.focus(); + if (hasFocus) { + const button = document.querySelector( + 'button.cds--actionable-notification__action-button' + ) as HTMLButtonElement; + button?.focus(); } }); + function handleBlur({ + target: oldActiveNode, + relatedTarget: currentActiveNode, + }) { + if (isOpen && currentActiveNode && oldActiveNode) { + const { current: bodyNode } = innerModal; + const { current: startTrapNode } = startTrap; + const { current: endTrapNode } = endTrap; + wrapFocus({ + bodyNode, + startTrapNode, + endTrapNode, + currentActiveNode, + oldActiveNode, + }); + } + } + const handleClose = (evt: MouseEvent) => { if (!onClose || onClose(evt) !== false) { setIsOpen(false); @@ -973,7 +998,16 @@ export function ActionableNotification({ ref={ref} role={role} className={containerClassName} - aria-labelledby={title ? id : subtitleId}> + aria-labelledby={title ? id : subtitleId} + onBlur={handleBlur}> + + Focus sentinel + +
+
+ {actionButtonLabel && ( + + {actionButtonLabel} + + )} - {actionButtonLabel && ( - - {actionButtonLabel} - - )} - - {!hideCloseButton && ( - - )} + {!hideCloseButton && ( + + )} +
+ + Focus sentinel + ); }