@@ -144,27 +144,6 @@ describe('ComposedModal', () => {
144144 ) ;
145145 } ) ;
146146
147- it ( 'should prevent close on click outside' , async ( ) => {
148- render (
149- < >
150- < button type = "button" > Click me</ button >
151- < ComposedModal open preventCloseOnClickOutside >
152- < ModalHeader > Modal header</ ModalHeader >
153- < ModalBody > This is the modal body content</ ModalBody >
154- </ ComposedModal >
155- </ >
156- ) ;
157- expect ( screen . getByRole ( 'presentation' , { hidden : true } ) ) . toHaveClass (
158- 'is-visible'
159- ) ;
160-
161- await userEvent . click ( screen . getByText ( 'Click me' ) ) ;
162-
163- expect ( screen . getByRole ( 'presentation' , { hidden : true } ) ) . toHaveClass (
164- 'is-visible'
165- ) ;
166- } ) ;
167-
168147 it ( 'should focus selector on open' , async ( ) => {
169148 function ComposedModalExample ( ) {
170149 const [ isOpen , setIsOpen ] = React . useState ( false ) ;
@@ -432,38 +411,6 @@ describe('ComposedModal', () => {
432411 expect ( onClick ) . toHaveBeenCalled ( ) ;
433412 } ) ;
434413
435- it ( 'should close when clicking outside passive composed modal' , async ( ) => {
436- const onClose = jest . fn ( ) ;
437- render (
438- < ComposedModal open onClose = { onClose } >
439- < ModalBody > This is the modal body content</ ModalBody >
440- </ ComposedModal >
441- ) ;
442- const backgroundLayer = screen . getByRole ( 'presentation' ) ;
443- await userEvent . click ( backgroundLayer ) ;
444- expect ( onClose ) . toHaveBeenCalled ( ) ;
445- } ) ;
446-
447- it ( 'should not close when clicking outside non-passive composed modal' , async ( ) => {
448- const onClose = jest . fn ( ) ;
449- render (
450- < >
451- < button data-testid = "outside-button" > ☀️</ button >
452- < ComposedModal open onClose = { onClose } >
453- < ModalHeader > Header</ ModalHeader >
454- < ModalBody > Body</ ModalBody >
455- < ModalFooter
456- primaryButtonText = "Confirm"
457- secondaryButtonText = "Cancel"
458- />
459- </ ComposedModal >
460- </ >
461- ) ;
462-
463- await userEvent . click ( screen . getByTestId ( 'outside-button' ) ) ;
464- expect ( onClose ) . not . toHaveBeenCalled ( ) ;
465- } ) ;
466-
467414 it ( 'should NOT close when clicked inside dialog window, dragged outside and released mouse button' , async ( ) => {
468415 const onClose = jest . fn ( ) ;
469416 render (
@@ -483,6 +430,167 @@ describe('ComposedModal', () => {
483430 expect ( onClose ) . not . toHaveBeenCalled ( ) ;
484431 } ) ;
485432
433+ describe ( 'close behavior for clicks outside the modal' , ( ) => {
434+ describe ( 'passive' , ( ) => {
435+ it ( 'should close on outside click by default' , async ( ) => {
436+ const onClose = jest . fn ( ) ;
437+ render (
438+ < ComposedModal open onClose = { onClose } >
439+ < ModalHeader > ModalHeader content</ ModalHeader >
440+ < ModalBody > ModalBody content</ ModalBody >
441+ { /* ModalFooter is omitted, this is what makes it passive */ }
442+ </ ComposedModal >
443+ ) ;
444+
445+ // The background layer is used here instead of a button outside the
446+ // modal because a real user cannot interact with a button. The
447+ // backround layer is in the way.
448+ const backgroundLayer = screen . getByRole ( 'presentation' , {
449+ hidden : true ,
450+ } ) ;
451+ expect ( backgroundLayer ) . toHaveClass ( 'is-visible' ) ;
452+
453+ await userEvent . click ( backgroundLayer ) ;
454+ expect ( backgroundLayer ) . not . toHaveClass ( 'is-visible' ) ;
455+ expect ( onClose ) . toHaveBeenCalled ( ) ;
456+ } ) ;
457+ it ( 'should not close on outside click when preventCloseOnClickOutside' , async ( ) => {
458+ const onClose = jest . fn ( ) ;
459+ render (
460+ < ComposedModal open onClose = { onClose } preventCloseOnClickOutside >
461+ < ModalHeader > ModalHeader content</ ModalHeader >
462+ < ModalBody > ModalBody content</ ModalBody >
463+ { /* ModalFooter is omitted, this is what makes it passive */ }
464+ </ ComposedModal >
465+ ) ;
466+
467+ // The background layer is used here instead of a button outside the
468+ // modal because a real user cannot interact with a button. The
469+ // backround layer is in the way.
470+ const backgroundLayer = screen . getByRole ( 'presentation' , {
471+ hidden : true ,
472+ } ) ;
473+ expect ( backgroundLayer ) . toHaveClass ( 'is-visible' ) ;
474+
475+ await userEvent . click ( backgroundLayer ) ;
476+ expect ( backgroundLayer ) . toHaveClass ( 'is-visible' ) ;
477+ expect ( onClose ) . not . toHaveBeenCalled ( ) ;
478+ } ) ;
479+ it ( 'should close on outside click when preventCloseOnClickOutside is explicitly false' , async ( ) => {
480+ const onClose = jest . fn ( ) ;
481+ render (
482+ < ComposedModal
483+ open
484+ onClose = { onClose }
485+ preventCloseOnClickOutside = { false } >
486+ < ModalHeader > ModalHeader content</ ModalHeader >
487+ < ModalBody > ModalBody content</ ModalBody >
488+ { /* ModalFooter is omitted, this is what makes it passive */ }
489+ </ ComposedModal >
490+ ) ;
491+
492+ // The background layer is used here instead of a button outside the
493+ // modal because a real user cannot interact with a button. The
494+ // backround layer is in the way.
495+ const backgroundLayer = screen . getByRole ( 'presentation' , {
496+ hidden : true ,
497+ } ) ;
498+ expect ( backgroundLayer ) . toHaveClass ( 'is-visible' ) ;
499+
500+ await userEvent . click ( backgroundLayer ) ;
501+ expect ( backgroundLayer ) . not . toHaveClass ( 'is-visible' ) ;
502+ expect ( onClose ) . toHaveBeenCalled ( ) ;
503+ } ) ;
504+ } ) ;
505+ describe ( 'non-passive' , ( ) => {
506+ it ( 'should not close on outside click by default' , async ( ) => {
507+ const onClose = jest . fn ( ) ;
508+ render (
509+ < ComposedModal open onClose = { onClose } >
510+ < ModalHeader > ModalHeader content</ ModalHeader >
511+ < ModalBody > ModalBody content</ ModalBody >
512+ < ModalFooter
513+ primaryButtonText = "Confirm"
514+ secondaryButtonText = "Cancel"
515+ />
516+ </ ComposedModal >
517+ ) ;
518+
519+ // The background layer is used here instead of a button outside the
520+ // modal because a real user cannot interact with a button. The
521+ // backround layer is in the way.
522+ const backgroundLayer = screen . getByRole ( 'presentation' , {
523+ hidden : true ,
524+ } ) ;
525+ expect ( backgroundLayer ) . toHaveClass ( 'is-visible' ) ;
526+
527+ await userEvent . click ( backgroundLayer ) ;
528+ expect ( backgroundLayer ) . toHaveClass ( 'is-visible' ) ;
529+ expect ( onClose ) . not . toHaveBeenCalled ( ) ;
530+ } ) ;
531+ it ( 'should not close on outside click when preventCloseOnClickOutside' , async ( ) => {
532+ const onClose = jest . fn ( ) ;
533+ render (
534+ < ComposedModal open onClose = { onClose } preventCloseOnClickOutside >
535+ < ModalHeader > ModalHeader content</ ModalHeader >
536+ < ModalBody > ModalBody content</ ModalBody >
537+ < ModalFooter
538+ primaryButtonText = "Confirm"
539+ secondaryButtonText = "Cancel"
540+ />
541+ </ ComposedModal >
542+ ) ;
543+
544+ // The background layer is used here instead of a button outside the
545+ // modal because a real user cannot interact with a button. The
546+ // backround layer is in the way.
547+ const backgroundLayer = screen . getByRole ( 'presentation' , {
548+ hidden : true ,
549+ } ) ;
550+ expect ( backgroundLayer ) . toHaveClass ( 'is-visible' ) ;
551+
552+ await userEvent . click ( backgroundLayer ) ;
553+ expect ( backgroundLayer ) . toHaveClass ( 'is-visible' ) ;
554+ expect ( onClose ) . not . toHaveBeenCalled ( ) ;
555+ } ) ;
556+ it ( 'should close on outside click when preventCloseOnClickOutside is explicitly false' , async ( ) => {
557+ const spy = jest . spyOn ( console , 'warn' ) . mockImplementation ( ( ) => { } ) ;
558+ const onClose = jest . fn ( ) ;
559+
560+ render (
561+ < ComposedModal
562+ open
563+ onClose = { onClose }
564+ preventCloseOnClickOutside = { false } >
565+ < ModalHeader > ModalHeader content</ ModalHeader >
566+ < ModalBody > ModalBody content</ ModalBody >
567+ < ModalFooter
568+ primaryButtonText = "Confirm"
569+ secondaryButtonText = "Cancel"
570+ />
571+ </ ComposedModal >
572+ ) ;
573+
574+ // The background layer is used here instead of a button outside the
575+ // modal because a real user cannot interact with a button. The
576+ // backround layer is in the way.
577+ const backgroundLayer = screen . getByRole ( 'presentation' , {
578+ hidden : true ,
579+ } ) ;
580+ expect ( backgroundLayer ) . toHaveClass ( 'is-visible' ) ;
581+
582+ await userEvent . click ( backgroundLayer ) ;
583+ expect ( backgroundLayer ) . not . toHaveClass ( 'is-visible' ) ;
584+ expect ( onClose ) . toHaveBeenCalled ( ) ;
585+ expect ( spy ) . toHaveBeenCalledWith (
586+ 'Warning: `<ComposedModal>` prop `preventCloseOnClickOutside` should not be `false` when `<ModalFooter>` is present. Transactional, non-passive Modals should not be dissmissable by clicking outside. See: https://carbondesignsystem.com/components/modal/usage/#transactional-modal'
587+ ) ;
588+
589+ spy . mockRestore ( ) ;
590+ } ) ;
591+ } ) ;
592+ } ) ;
593+
486594 it ( 'should focus on launcherButtonRef element on close when defined' , async ( ) => {
487595 const ComposedModalExample = ( ) => {
488596 const [ open , setOpen ] = useState ( true ) ;
0 commit comments