@@ -75,6 +75,54 @@ const renderStackedModals = ({
7575 </ ChatProvider > ,
7676 ) ;
7777
78+ const RemovableChildModalFixture = ( ) => {
79+ const [ showChild , setShowChild ] = React . useState ( true ) ;
80+
81+ return (
82+ < ChatProvider value = { mockChatContext ( { theme : 'messaging light' } ) } >
83+ < ComponentProvider value = { { NotificationList : NoopNotificationList } } >
84+ < ModalDialogManagerProvider >
85+ < GlobalModal aria-label = 'Parent modal' open >
86+ < ModalContent text = 'Parent content' />
87+ { showChild && (
88+ < GlobalModal aria-label = 'Child modal' open role = 'alertdialog' >
89+ < ModalContent text = 'Child content' >
90+ < button onClick = { ( ) => setShowChild ( false ) } type = 'button' >
91+ Remove child modal
92+ </ button >
93+ </ ModalContent >
94+ </ GlobalModal >
95+ ) }
96+ </ GlobalModal >
97+ </ ModalDialogManagerProvider >
98+ </ ComponentProvider >
99+ </ ChatProvider >
100+ ) ;
101+ } ;
102+
103+ const CloseChildModalFixture = ( ) => {
104+ const [ childOpen , setChildOpen ] = React . useState ( true ) ;
105+
106+ return (
107+ < ChatProvider value = { mockChatContext ( { theme : 'messaging light' } ) } >
108+ < ComponentProvider value = { { NotificationList : NoopNotificationList } } >
109+ < ModalDialogManagerProvider >
110+ < GlobalModal aria-label = 'Parent modal' open >
111+ < ModalContent text = 'Parent content' />
112+ < GlobalModal aria-label = 'Child modal' open = { childOpen } role = 'alertdialog' >
113+ < ModalContent text = 'Child content' >
114+ < button onClick = { ( ) => setChildOpen ( false ) } type = 'button' >
115+ Close child modal
116+ </ button >
117+ </ ModalContent >
118+ </ GlobalModal >
119+ </ GlobalModal >
120+ </ ModalDialogManagerProvider >
121+ </ ComponentProvider >
122+ </ ChatProvider >
123+ ) ;
124+ } ;
125+
78126const OverlayCloseButton = React . forwardRef <
79127 HTMLButtonElement ,
80128 React . ComponentProps < 'button' >
@@ -302,7 +350,7 @@ describe('GlobalModal', () => {
302350 const dialog = screen . getByRole ( 'dialog' ) ;
303351 expect ( dialog ) . toHaveClass ( 'str-chat__modal__dialog' ) ;
304352 expect ( dialog ) . toHaveAttribute ( 'aria-modal' , 'true' ) ;
305- expect ( dialog ) . toHaveAttribute ( 'tabindex' , '-1 ' ) ;
353+ expect ( dialog ) . toHaveAttribute ( 'tabindex' , '0 ' ) ;
306354 expect ( dialog ) . toHaveAttribute ( 'aria-labelledby' , 'modal-title' ) ;
307355 expect ( dialog ) . toHaveAttribute ( 'aria-describedby' , 'modal-description' ) ;
308356 } ) ;
@@ -390,9 +438,11 @@ describe('GlobalModal', () => {
390438 expect ( parentModal ) . toBeInTheDocument ( ) ;
391439 expect ( parentModal ) . not . toHaveAttribute ( 'aria-modal' ) ;
392440 expect ( parentModal ) . toHaveAttribute ( 'inert' ) ;
441+ expect ( parentModal ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
393442 expect ( childModal ) . toBeInTheDocument ( ) ;
394443 expect ( childModal ) . toHaveAttribute ( 'aria-modal' , 'true' ) ;
395444 expect ( childModal ) . not . toHaveAttribute ( 'inert' ) ;
445+ expect ( childModal ) . toHaveAttribute ( 'tabindex' , '0' ) ;
396446 } ) ;
397447
398448 it ( 'only closes the topmost modal on Escape' , ( ) => {
@@ -413,6 +463,59 @@ describe('GlobalModal', () => {
413463 expect ( parentOnClose ) . not . toHaveBeenCalled ( ) ;
414464 } ) ;
415465
466+ it ( 'restores interactivity to the underlying modal after the topmost modal closes' , ( ) => {
467+ const childOnClose = vi . fn ( ) ;
468+ const parentOnClose = vi . fn ( ) ;
469+
470+ renderStackedModals ( { childOnClose, parentOnClose } ) ;
471+
472+ const parentModal = screen . getByRole ( 'dialog' , { name : 'Parent modal' } ) ;
473+ const childModal = screen . getByRole ( 'alertdialog' , { name : 'Child modal' } ) ;
474+
475+ fireEvent . keyDown ( childModal , { key : 'Escape' } ) ;
476+
477+ expect ( childOnClose ) . toHaveBeenCalledTimes ( 1 ) ;
478+ expect ( parentModal ) . toHaveAttribute ( 'aria-modal' , 'true' ) ;
479+ expect ( parentModal ) . not . toHaveAttribute ( 'inert' ) ;
480+ expect ( parentModal ) . toHaveAttribute ( 'tabindex' , '0' ) ;
481+
482+ fireEvent . keyDown ( parentModal , { key : 'Escape' } ) ;
483+
484+ expect ( parentOnClose ) . toHaveBeenCalledTimes ( 1 ) ;
485+ } ) ;
486+
487+ it ( 'restores topmost state to the underlying modal after the topmost modal is removed' , ( ) => {
488+ render ( < RemovableChildModalFixture /> ) ;
489+
490+ const parentModal = screen . getByRole ( 'dialog' , { name : 'Parent modal' } ) ;
491+ expect ( parentModal ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
492+
493+ fireEvent . click ( screen . getByRole ( 'button' , { name : 'Remove child modal' } ) ) ;
494+
495+ expect (
496+ screen . queryByRole ( 'alertdialog' , { name : 'Child modal' } ) ,
497+ ) . not . toBeInTheDocument ( ) ;
498+ expect ( parentModal ) . toHaveAttribute ( 'aria-modal' , 'true' ) ;
499+ expect ( parentModal ) . not . toHaveAttribute ( 'inert' ) ;
500+ expect ( parentModal ) . toHaveAttribute ( 'tabindex' , '0' ) ;
501+ } ) ;
502+
503+ it ( 'restores topmost state to the underlying modal after the topmost modal open prop becomes false' , ( ) => {
504+ render ( < CloseChildModalFixture /> ) ;
505+
506+ const parentModal = screen . getByRole ( 'dialog' , { name : 'Parent modal' } ) ;
507+ expect ( parentModal ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
508+
509+ fireEvent . click ( screen . getByRole ( 'button' , { name : 'Close child modal' } ) ) ;
510+
511+ expect (
512+ screen . queryByRole ( 'alertdialog' , { name : 'Child modal' } ) ,
513+ ) . not . toBeInTheDocument ( ) ;
514+ expect ( parentModal ) . toHaveAttribute ( 'aria-modal' , 'true' ) ;
515+ expect ( parentModal ) . not . toHaveAttribute ( 'inert' ) ;
516+ expect ( parentModal ) . toHaveAttribute ( 'tabindex' , '0' ) ;
517+ } ) ;
518+
416519 it ( 'forwards alertdialog role when explicitly provided' , ( ) => {
417520 renderComponent ( {
418521 props : {
0 commit comments