@@ -12,7 +12,6 @@ import {
1212} from 'react' ;
1313import * as React from 'react' ;
1414import ReactDOM from 'react-dom' ;
15- import useMounted from '@restart/hooks/useMounted' ;
1615import useWillUnmount from '@restart/hooks/useWillUnmount' ;
1716
1817import usePrevious from '@restart/hooks/usePrevious' ;
@@ -21,11 +20,13 @@ import ModalManager from './ModalManager.js';
2120import useWaitForDOMRef , { type DOMContainer } from './useWaitForDOMRef.js' ;
2221import type { TransitionCallbacks , TransitionComponent } from './types.js' ;
2322import useWindow from './useWindow.js' ;
23+ import { useFocusTrap } from './useFocusTrap.js' ;
2424import {
2525 renderTransition ,
2626 type TransitionHandler ,
2727} from './ImperativeTransition.js' ;
2828import { isEscKey } from './utils.js' ;
29+ import { getTabbableElementsOrSelf } from './tabbable.js' ;
2930
3031let manager : ModalManager ;
3132
@@ -298,11 +299,16 @@ const Modal: React.ForwardRefExoticComponent<
298299 const container = useWaitForDOMRef ( containerRef ) ;
299300 const modal = useModalManager ( providedManager ) ;
300301
301- const isMounted = useMounted ( ) ;
302302 const prevShow = usePrevious ( show ) ;
303303 const [ exited , setExited ] = useState ( ! show ) ;
304+ const removeKeydownListenerRef = useRef < ( ( ) => void ) | null > ( null ) ;
304305 const lastFocusRef = useRef < HTMLElement | null > ( null ) ;
305306
307+ const focusTrap = useFocusTrap ( {
308+ getContainer : ( ) => modal . dialog ,
309+ disabled : ( ) => ! enforceFocus || ! modal . isTopModal ( ) ,
310+ } ) ;
311+
306312 useImperativeHandle ( ref , ( ) => modal , [ modal ] ) ;
307313
308314 if ( canUseDOM && ! prevShow && show ) {
@@ -325,14 +331,7 @@ const Modal: React.ForwardRefExoticComponent<
325331 handleDocumentKeyDown ,
326332 ) ;
327333
328- removeFocusListenerRef . current = listen (
329- document as any ,
330- 'focus' ,
331- // the timeout is necessary b/c this will run before the new modal is mounted
332- // and so steals focus from it
333- ( ) => setTimeout ( handleEnforceFocus ) ,
334- true ,
335- ) ;
334+ focusTrap . start ( ) ;
336335
337336 if ( onShow ) {
338337 onShow ( ) ;
@@ -351,7 +350,8 @@ const Modal: React.ForwardRefExoticComponent<
351350 ! contains ( modal . dialog , currentActiveElement )
352351 ) {
353352 lastFocusRef . current = currentActiveElement ;
354- modal . dialog . focus ( ) ;
353+ const tabbables = getTabbableElementsOrSelf ( modal . dialog ) ;
354+ tabbables [ 0 ] ?. focus ( ) ;
355355 }
356356 }
357357 } ) ;
@@ -360,7 +360,7 @@ const Modal: React.ForwardRefExoticComponent<
360360 modal . remove ( ) ;
361361
362362 removeKeydownListenerRef . current ?.( ) ;
363- removeFocusListenerRef . current ?. ( ) ;
363+ focusTrap . stop ( ) ;
364364
365365 if ( restoreFocus ) {
366366 // Support: <=IE11 doesn't support `focus()` on svg elements (RB: #917)
@@ -394,22 +394,6 @@ const Modal: React.ForwardRefExoticComponent<
394394
395395 // --------------------------------
396396
397- const handleEnforceFocus = useEventCallback ( ( ) => {
398- if ( ! enforceFocus || ! isMounted ( ) || ! modal . isTopModal ( ) ) {
399- return ;
400- }
401-
402- const currentActiveElement = activeElement ( ownerWindow ?. document ) ;
403-
404- if (
405- modal . dialog &&
406- currentActiveElement &&
407- ! contains ( modal . dialog , currentActiveElement )
408- ) {
409- modal . dialog . focus ( ) ;
410- }
411- } ) ;
412-
413397 const handleBackdropClick = useEventCallback ( ( e : React . SyntheticEvent ) => {
414398 if ( e . target !== e . currentTarget ) {
415399 return ;
@@ -432,13 +416,6 @@ const Modal: React.ForwardRefExoticComponent<
432416 }
433417 } ) ;
434418
435- const removeFocusListenerRef = useRef < ReturnType < typeof listen > | null > (
436- null ,
437- ) ;
438- const removeKeydownListenerRef = useRef < ReturnType < typeof listen > | null > (
439- null ,
440- ) ;
441-
442419 const handleHidden : TransitionCallbacks [ 'onExited' ] = ( ...args ) => {
443420 setExited ( true ) ;
444421 onExited ?.( ...args ) ;
0 commit comments