@@ -10,6 +10,15 @@ import {
1010 CONFIRM_DELETE_DATA_BUTTON_TEST_ID ,
1111} from '@/constants/test-ids' ;
1212
13+ const FOCUSABLE_SELECTORS = [
14+ 'a[href]' ,
15+ 'button:not([disabled])' ,
16+ 'input:not([disabled])' ,
17+ 'select:not([disabled])' ,
18+ 'textarea:not([disabled])' ,
19+ '[tabindex]:not([tabindex="-1"])' ,
20+ ] . join ( ', ' ) ;
21+
1322interface ConfirmDialogProps {
1423 isOpen : boolean ;
1524 onConfirm : ( ) => void ;
@@ -39,34 +48,57 @@ export default function ConfirmDialog({
3948} : ConfirmDialogProps ) {
4049 const dialogRef = useRef < HTMLDivElement > ( null ) ;
4150 const confirmButtonRef = useRef < HTMLButtonElement > ( null ) ;
51+ const triggerRef = useRef < HTMLElement | null > ( null ) ;
4252
43- // Focus the confirm button when dialog opens
53+ // Capture the trigger element, move focus into dialog on open,
54+ // and restore focus to the trigger on close.
4455 useEffect ( ( ) => {
45- if ( isOpen && confirmButtonRef . current ) {
46- confirmButtonRef . current . focus ( ) ;
56+ if ( isOpen ) {
57+ triggerRef . current = document . activeElement as HTMLElement ;
58+ confirmButtonRef . current ?. focus ( ) ;
59+ } else {
60+ triggerRef . current ?. focus ( ) ;
61+ triggerRef . current = null ;
4762 }
4863 } , [ isOpen ] ) ;
4964
50- // Handle escape key
65+ // Focus trap + Escape key (only active while open)
5166 useEffect ( ( ) => {
52- const handleEscape = ( e : KeyboardEvent ) => {
53- if ( e . key === 'Escape' && isOpen ) {
67+ if ( ! isOpen ) return ;
68+
69+ const handleKeyDown = ( e : KeyboardEvent ) => {
70+ if ( e . key === 'Escape' ) {
5471 onCancel ( ) ;
72+ return ;
73+ }
74+
75+ if ( e . key !== 'Tab' || ! dialogRef . current ) return ;
76+
77+ const focusable = Array . from (
78+ dialogRef . current . querySelectorAll < HTMLElement > ( FOCUSABLE_SELECTORS ) ,
79+ ) ;
80+
81+ if ( focusable . length === 0 ) return ;
82+
83+ const first = focusable [ 0 ] ;
84+ const last = focusable [ focusable . length - 1 ] ;
85+
86+ if ( e . shiftKey && document . activeElement === first ) {
87+ e . preventDefault ( ) ;
88+ last . focus ( ) ;
89+ } else if ( ! e . shiftKey && document . activeElement === last ) {
90+ e . preventDefault ( ) ;
91+ first . focus ( ) ;
5592 }
5693 } ;
5794
58- document . addEventListener ( 'keydown' , handleEscape ) ;
59- return ( ) => document . removeEventListener ( 'keydown' , handleEscape ) ;
95+ document . addEventListener ( 'keydown' , handleKeyDown ) ;
96+ return ( ) => document . removeEventListener ( 'keydown' , handleKeyDown ) ;
6097 } , [ isOpen , onCancel ] ) ;
6198
6299 // Prevent body scroll when dialog is open
63100 useEffect ( ( ) => {
64- if ( isOpen ) {
65- document . body . style . overflow = 'hidden' ;
66- } else {
67- document . body . style . overflow = '' ;
68- }
69-
101+ document . body . style . overflow = isOpen ? 'hidden' : '' ;
70102 return ( ) => {
71103 document . body . style . overflow = '' ;
72104 } ;
@@ -101,6 +133,7 @@ export default function ConfirmDialog({
101133 type = "button"
102134 >
103135 < svg
136+ aria-hidden = "true"
104137 width = "24"
105138 height = "24"
106139 viewBox = "0 0 24 24"
0 commit comments