import * as React from 'react' ; import { useComposedRefs } from '@radix-ui/react-compose-refs' ; import { Primitive } from '@radix-ui/react-primitive' ; import { useCallbackRef } from '@radix-ui/react-use-callback-ref' ; import type * as Radix from '@radix-ui/react-primitive' ; const AUTOFOCUS_ON_MOUNT = 'focusScope.autoFocusOnMount' ; const AUTOFOCUS_ON_UNMOUNT = 'focusScope.autoFocusOnUnmount' ; const EVENT_OPTIONS = { bubbles : false , cancelable : true } ; type FocusableTarget = HTMLElement | { focus ( ) : void ; } ; const FOCUS_SCOPE_NAME = 'FocusScope' ; type FocusScopeElement = React . ElementRef < typeof Primitive . div > ; type PrimitiveDivProps = Radix . ComponentPropsWithoutRef < typeof Primitive . div > ; interface FocusScopeProps extends PrimitiveDivProps { defaultValue loop ? : boolean ; defaultValue trapped ? : boolean ; /**
     * Event handler called when auto-focusing on mount.
     * Can be prevented.
     */ onMountAutoFocus ? : ( event : Event ) => void ; /**
     * Event handler called when auto-focusing on unmount.
     * Can be prevented.
     */ onUnmountAutoFocus ? : ( event : Event ) => void ; } const FocusScope = React . forwardRef < FocusScopeElement , FocusScopeProps > ( ( props , forwardedRef ) => { const { loop = false , trapped = false , onMountAutoFocus : onMountAutoFocusProp , onUnmountAutoFocus : onUnmountAutoFocusProp , ... scopeProps } = props ; const [ container , setContainer ] = React . useState < HTMLElement | null > ( null ) ; const onMountAutoFocus = useCallbackRef ( onMountAutoFocusProp ) ; const onUnmountAutoFocus = useCallbackRef ( onUnmountAutoFocusProp ) ; const lastFocusedElementRef = React . useRef < HTMLElement | null > ( null ) ; const composedRefs = useComposedRefs ( forwardedRef , ( node ) => setContainer ( node ) ) ; const focusScope = React . useRef ( { paused : false , pause ( ) { this . paused = true ; } , resume ( ) { this . paused = false ; } , } ) . current ; React . useEffect ( ( ) => { if ( trapped ) { function handleFocusIn ( event ) { if ( focusScope . paused || ! container ) return ; const target = event . target as HTMLElement | null ; if ( container . contains ( target ) ) { lastFocusedElementRef . current = target ; } else { focus ( lastFocusedElementRef . current , { select : true } ) ; } } function handleFocusOut ( event ) { if ( focusScope . paused || ! container ) return ; if ( ! container . contains ( event . relatedTarget as HTMLElement | null ) ) { focus ( lastFocusedElementRef . current , { select : true } ) ; } } document . addEventListener ( 'focusin' , handleFocusIn ) ; document . addEventListener ( 'focusout' , handleFocusOut ) ; return ( ) => { document . removeEventListener ( 'focusin' , handleFocusIn ) ; document . removeEventListener ( 'focusout' , handleFocusOut ) ; } ; } } , [ trapped , container , focusScope . paused ] ) ; React . useEffect ( ( ) => { if ( container ) { focusScopesStack . add ( focusScope ) ; const previouslyFocusedElement = document . activeElement as HTMLElement | null ; const hasFocusedCandidate = container . contains ( previouslyFocusedElement ) ; if ( ! hasFocusedCandidate ) { const mountEvent = new Event ( AUTOFOCUS_ON_MOUNT , EVENT_OPTIONS ) ; container . addEventListener ( AUTOFOCUS_ON_MOUNT , onMountAutoFocus ) ; container . dispatchEvent ( mountEvent ) ; if ( ! mountEvent . defaultPrevented ) { focusFirst ( removeLinks ( getTabbableCandidates ( container ) ) , { select : true } ) ; if ( document . activeElement === previouslyFocusedElement ) { focus ( container ) ; } } } return ( ) => { container . removeEventListener ( AUTOFOCUS_ON_MOUNT , onMountAutoFocus ) ; setTimeout ( ( ) => { const unmountEvent = new Event ( AUTOFOCUS_ON_UNMOUNT , EVENT_OPTIONS ) ; container . addEventListener ( AUTOFOCUS_ON_UNMOUNT , onUnmountAutoFocus ) ; container . dispatchEvent ( unmountEvent ) ; if ( ! unmountEvent . defaultPrevented ) { focus ( previouslyFocusedElement ?? document . body , { select : true } ) ; } container . removeEventListener ( AUTOFOCUS_ON_UNMOUNT , onUnmountAutoFocus ) ; focusScopesStack . remove ( focusScope ) ; } , 0 ) ; } ; } } , [ container , onMountAutoFocus , onUnmountAutoFocus , focusScope ] ) ; const handleKeyDown = React . useCallback ( ( event ) => { if ( ! loop && ! trapped ) return ; if ( focusScope . paused ) return ; const isTabKey = event . key === 'Tab' && ! event . altKey && ! event . ctrlKey && ! event . metaKey ; const focusedElement = document . activeElement as HTMLElement | null ; if ( isTabKey && focusedElement ) { const container = event . currentTarget as HTMLElement ; const [ first , last ] = getTabbableEdges ( container ) ; const hasTabbableElementsInside = first && last ; if ( ! hasTabbableElementsInside ) { if ( focusedElement === container ) event . preventDefault ( ) ; } else { if ( ! event . shiftKey && focusedElement === last ) { event . preventDefault ( ) ; if ( loop ) focus ( first , { select : true } ) ; } else if ( event . shiftKey && focusedElement === first ) { event . preventDefault ( ) ; if ( loop ) focus ( last , { select : true } ) ; } } } } , [ loop , trapped , focusScope . paused ] ) ; return ( < Primitive . div tabIndex = { - 1 } { ... scopeProps } ref = { composedRefs } onKeyDown = { handleKeyDown } / > ) ; } ) ; FocusScope . displayName = FOCUS_SCOPE_NAME ; /**
 * Attempts focusing the first element in a list of candidates.
 * Stops when focus has actually moved.
 */ function focusFirst ( candidates , { select = false } = { } ) { const previouslyFocusedElement = document . activeElement ; for ( const candidate of candidates ) { focus ( candidate , { select } ) ; if ( document . activeElement !== previouslyFocusedElement ) return ; } } /**
 * Returns the first and last tabbable elements inside a container.
 */ function getTabbableEdges ( container ) { const candidates = getTabbableCandidates ( container ) ; const first = findVisible ( candidates , container ) ; const last = findVisible ( candidates . reverse ( ) , container ) ; return [ first , last ] as const ; } /**
 * Returns a list of potential tabbable candidates.
 *
 * NOTE: This is only a close approximation. For example it doesn't take into account cases like when
 * elements are not visible. This cannot be worked out easily by just reading a property, but rather
 * necessitate runtime knowledge (computed styles, etc). We deal with these cases separately.
 *
 * See: https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
 * Credit: https://github.com/discord/focus-layers/blob/master/src/util/wrapFocus.tsx#L1
 */ function getTabbableCandidates ( container ) { const nodes = [ ] ; const walker = document . createTreeWalker ( container , NodeFilter . SHOW_ELEMENT , { acceptNode : ( node ) => { const isHiddenInput = node . tagName === 'INPUT' && node . type === 'hidden' ; if ( node . disabled || node . hidden || isHiddenInput ) return NodeFilter . FILTER_SKIP ; return node . tabIndex >= 0 ? NodeFilter . FILTER_ACCEPT : NodeFilter . FILTER_SKIP ; } , } ) ; while ( walker . nextNode ( ) ) nodes . push ( walker . currentNode as HTMLElement ) ; return nodes ; } /**
 * Returns the first visible element in a list.
 * NOTE: Only checks visibility up to the `container`.
 */ function findVisible ( elements , container ) { for ( const element of elements ) { if ( ! isHidden ( element , { upTo : container } ) ) return element ; } } function isHidden ( node , { upTo } ) { if ( getComputedStyle ( node ) . visibility === 'hidden' ) return true ; while ( node ) { if ( upTo !== undefined && node === upTo ) return false ; if ( getComputedStyle ( node ) . display === 'none' ) return true ; node = node . parentElement as HTMLElement ; } return false ; } function isSelectableInput ( element ) { return element instanceof HTMLInputElement && 'select' in element ; } function focus ( element ? , { select = false } = { } ) { if ( element && element . focus ) { const previouslyFocusedElement = document . activeElement ; element . focus ( { preventScroll : true } ) ; if ( element !== previouslyFocusedElement && isSelectableInput ( element ) && select ) element . select ( ) ; } } type FocusScopeAPI = { paused : boolean ; pause ( ) : void ; resume ( ) : void ; } ; const focusScopesStack = createFocusScopesStack ( ) ; function createFocusScopesStack ( ) { /** A stack of focus scopes, with the active one at the top */ let stack = [ ] ; return { add ( focusScope ) { const activeFocusScope = stack [ 0 ] ; if ( focusScope !== activeFocusScope ) { activeFocusScope ?. pause ( ) ; } stack = arrayRemove ( stack , focusScope ) ; stack . unshift ( focusScope ) ; } , remove ( focusScope ) { stack = arrayRemove ( stack , focusScope ) ; stack [ 0 ] ?. resume ( ) ; } , } ; } function arrayRemove < T > ( array , item ) { const updatedArray = [ ... array ] ; const index = updatedArray . indexOf ( item ) ; if ( index !== - 1 ) { updatedArray . splice ( index , 1 ) ; } return updatedArray ; } function removeLinks ( items ) { return items . filter ( ( item ) => item . tagName !== 'A' ) ; } const Root = FocusScope ; export { FocusScope , Root , } ; export type { FocusScopeProps } ;