import * as React from 'react' ; import { composeEventHandlers } from '@radix-ui/primitive' ; import { createCollection } from '@radix-ui/react-collection' ; import { useComposedRefs } from '@radix-ui/react-compose-refs' ; import { createContextScope } from '@radix-ui/react-context' ; import { useId } from '@radix-ui/react-id' ; import { Primitive } from '@radix-ui/react-primitive' ; import { useCallbackRef } from '@radix-ui/react-use-callback-ref' ; import { useControllableState } from '@radix-ui/react-use-controllable-state' ; import type * as Radix from '@radix-ui/react-primitive' ; import type { Scope } from '@radix-ui/react-context' ; const ENTRY_FOCUS = 'rovingFocusGroup.onEntryFocus' ; const EVENT_OPTIONS = { bubbles : false , cancelable : true } ; const GROUP_NAME = 'RovingFocusGroup' ; type ItemData = { id : string ; focusable : boolean ; active : boolean ; } ; const [ Collection , useCollection , createCollectionScope ] = createCollection < HTMLSpanElement , ItemData > ( GROUP_NAME ) ; type ScopedProps < P > = P & { __scopeRovingFocusGroup ? : Scope ; } ; const [ createRovingFocusGroupContext , createRovingFocusGroupScope ] = createContextScope ( GROUP_NAME , [ createCollectionScope ] ) ; type Orientation = React . AriaAttributes [ 'aria-orientation' ] ; type Direction = 'ltr' | 'rtl' ; interface RovingFocusGroupOptions { /**
     * The orientation of the group.
     * Mainly so arrow navigation is done accordingly (left & right vs. up & down)
     */ orientation ? : Orientation ; defaultValue dir ? : Direction ; defaultValue loop ? : boolean ; } type RovingContextValue = RovingFocusGroupOptions & { currentTabStopId : string | null ; onItemFocus ( tabStopId ) : void ; onItemShiftTab ( ) : void ; } ; const [ RovingFocusProvider , useRovingFocusContext ] = createRovingFocusGroupContext < RovingContextValue > ( GROUP_NAME ) ; type RovingFocusGroupElement = RovingFocusGroupImplElement ; interface RovingFocusGroupProps extends RovingFocusGroupImplProps { } const RovingFocusGroup = React . forwardRef < RovingFocusGroupElement , RovingFocusGroupProps > ( ( props , forwardedRef ) => { return ( < Collection . Provider scope = { props . __scopeRovingFocusGroup } > < Collection . Slot scope = { props . __scopeRovingFocusGroup } > < RovingFocusGroupImpl { ... props } ref = { forwardedRef } / > < /Collection.Slot> < /Collection.Provider>); } ) ; RovingFocusGroup . displayName = GROUP_NAME ; type RovingFocusGroupImplElement = React . ElementRef < typeof Primitive . div > ; type PrimitiveDivProps = Radix . ComponentPropsWithoutRef < typeof Primitive . div > ; interface RovingFocusGroupImplProps extends Omit < PrimitiveDivProps , 'dir' > , RovingFocusGroupOptions { currentTabStopId ? : string | null ; defaultCurrentTabStopId ? : string ; onCurrentTabStopIdChange ? : ( tabStopId : string | null ) => void ; onEntryFocus ? : ( event : Event ) => void ; } const RovingFocusGroupImpl = React . forwardRef < RovingFocusGroupImplElement , RovingFocusGroupImplProps > ( ( props , forwardedRef ) => { const { __scopeRovingFocusGroup , orientation , dir = 'ltr' , loop = false , currentTabStopId : currentTabStopIdProp , defaultCurrentTabStopId , onCurrentTabStopIdChange , onEntryFocus , ... groupProps } = props ; const ref = React . useRef < RovingFocusGroupImplElement > ( null ) ; const composedRefs = useComposedRefs ( forwardedRef , ref ) ; const [ currentTabStopId = null , setCurrentTabStopId ] = useControllableState ( { prop : currentTabStopIdProp , defaultProp : defaultCurrentTabStopId , onChange : onCurrentTabStopIdChange , } ) ; const [ isTabbingBackOut , setIsTabbingBackOut ] = React . useState ( false ) ; const handleEntryFocus = useCallbackRef ( onEntryFocus ) ; const getItems = useCollection ( __scopeRovingFocusGroup ) ; const isClickFocusRef = React . useRef ( false ) ; React . useEffect ( ( ) => { const node = ref . current ; if ( node ) { node . addEventListener ( ENTRY_FOCUS , handleEntryFocus ) ; return ( ) => node . removeEventListener ( ENTRY_FOCUS , handleEntryFocus ) ; } } , [ handleEntryFocus ] ) ; return ( < RovingFocusProvider scope = { __scopeRovingFocusGroup } orientation = { orientation } dir = { dir } loop = { loop } currentTabStopId = { currentTabStopId } onItemFocus = { React . useCallback ( ( tabStopId ) => setCurrentTabStopId ( tabStopId ) , [ setCurrentTabStopId ] ) } onItemShiftTab = { React . useCallback ( ( ) => setIsTabbingBackOut ( true ) , [ ] ) } > < Primitive . div tabIndex = { isTabbingBackOut ? - 1 : 0 } data - orientation = { orientation } { ... groupProps } ref = { composedRefs } style = { { outline : 'none' , ... props . style } } onMouseDown = { composeEventHandlers ( props . onMouseDown , ( ) => { isClickFocusRef . current = true ; } ) } onFocus = { composeEventHandlers ( props . onFocus , ( event ) => { const isKeyboardFocus = ! isClickFocusRef . current ; if ( event . target === event . currentTarget && isKeyboardFocus && ! isTabbingBackOut ) { const entryFocusEvent = new Event ( ENTRY_FOCUS , EVENT_OPTIONS ) ; event . currentTarget . dispatchEvent ( entryFocusEvent ) ; if ( ! entryFocusEvent . defaultPrevented ) { const items = getItems ( ) . filter ( ( item ) => item . focusable ) ; const activeItem = items . find ( ( item ) => item . active ) ; const currentItem = items . find ( ( item ) => item . id === currentTabStopId ) ; const candidateItems = [ activeItem , currentItem , ... items ] . filter ( Boolean ) as typeof items ; const candidateNodes = candidateItems . map ( ( item ) => item . ref . current ! ) ; focusFirst ( candidateNodes ) ; } } isClickFocusRef . current = false ; } ) } onBlur = { composeEventHandlers ( props . onBlur , ( ) => setIsTabbingBackOut ( false ) ) } /> < /RovingFocusProvider>); } ) ; const ITEM_NAME = 'RovingFocusGroupItem' ; type RovingFocusItemElement = React . ElementRef < typeof Primitive . span > ; type PrimitiveSpanProps = Radix . ComponentPropsWithoutRef < typeof Primitive . span > ; interface RovingFocusItemProps extends PrimitiveSpanProps { focusable ? : boolean ; active ? : boolean ; } const RovingFocusGroupItem = React . forwardRef < RovingFocusItemElement , RovingFocusItemProps > ( ( props , forwardedRef ) => { const { __scopeRovingFocusGroup , focusable = true , active = false , ... itemProps } = props ; const id = useId ( ) ; const context = useRovingFocusContext ( ITEM_NAME , __scopeRovingFocusGroup ) ; const isCurrentTabStop = context . currentTabStopId === id ; const getItems = useCollection ( __scopeRovingFocusGroup ) ; return ( < Collection . ItemSlot scope = { __scopeRovingFocusGroup } id = { id } focusable = { focusable } active = { active } > < Primitive . span tabIndex = { isCurrentTabStop ? 0 : - 1 } data - orientation = { context . orientation } { ... itemProps } ref = { forwardedRef } onMouseDown = { composeEventHandlers ( props . onMouseDown , ( event ) => { if ( ! focusable ) event . preventDefault ( ) ; else context . onItemFocus ( id ) ; } ) } onFocus = { composeEventHandlers ( props . onFocus , ( ) => context . onItemFocus ( id ) ) } onKeyDown = { composeEventHandlers ( props . onKeyDown , ( event ) => { if ( event . key === 'Tab' && event . shiftKey ) { context . onItemShiftTab ( ) ; return ; } if ( event . target !== event . currentTarget ) return ; const focusIntent = getFocusIntent ( event , context . orientation , context . dir ) ; if ( focusIntent !== undefined ) { event . preventDefault ( ) ; const items = getItems ( ) . filter ( ( item ) => item . focusable ) ; let candidateNodes = items . map ( ( item ) => item . ref . current ! ) ; if ( focusIntent === 'last' ) candidateNodes . reverse ( ) ; else if ( focusIntent === 'prev' || focusIntent === 'next' ) { if ( focusIntent === 'prev' ) candidateNodes . reverse ( ) ; const currentIndex = candidateNodes . indexOf ( event . currentTarget ) ; candidateNodes = context . loop ? wrapArray ( candidateNodes , currentIndex + 1 ) : candidateNodes . slice ( currentIndex + 1 ) ; } /**
                 * Imperative focus during keydown is risky so we prevent React's batching updates
                 * to avoid potential bugs. See: https://github.com/facebook/react/issues/20332
                 */ setTimeout ( ( ) => focusFirst ( candidateNodes ) ) ; } } ) } /> < /Collection.ItemSlot>); } ) ; RovingFocusGroupItem . displayName = ITEM_NAME ; const MAP_KEY_TO_FOCUS_INTENT = { ArrowLeft : 'prev' , ArrowUp : 'prev' , ArrowRight : 'next' , ArrowDown : 'next' , PageUp : 'first' , Home : 'first' , PageDown : 'last' , End : 'last' , } ; function getDirectionAwareKey ( key , dir ? ) { if ( dir !== 'rtl' ) return key ; return key === 'ArrowLeft' ? 'ArrowRight' : key === 'ArrowRight' ? 'ArrowLeft' : key ; } type FocusIntent = 'first' | 'last' | 'prev' | 'next' ; function getFocusIntent ( event , orientation ? , dir ? ) { const key = getDirectionAwareKey ( event . key , dir ) ; if ( orientation === 'vertical' && [ 'ArrowLeft' , 'ArrowRight' ] . includes ( key ) ) return undefined ; if ( orientation === 'horizontal' && [ 'ArrowUp' , 'ArrowDown' ] . includes ( key ) ) return undefined ; return MAP_KEY_TO_FOCUS_INTENT [ key ] ; } function focusFirst ( candidates ) { const PREVIOUSLY_FOCUSED_ELEMENT = document . activeElement ; for ( const candidate of candidates ) { if ( candidate === PREVIOUSLY_FOCUSED_ELEMENT ) return ; candidate . focus ( ) ; if ( document . activeElement !== PREVIOUSLY_FOCUSED_ELEMENT ) return ; } } /**
 * Wraps an array around itself at a given start index
 * Example: `wrapArray(['a', 'b', 'c', 'd'], 2) === ['c', 'd', 'a', 'b']`
 */ function wrapArray < T > ( array , startIndex ) { return array . map ( ( _ , index ) => array [ ( startIndex + index ) % array . length ] ) ; } const Root = RovingFocusGroup ; const Item = RovingFocusGroupItem ; export { createRovingFocusGroupScope , RovingFocusGroup , RovingFocusGroupItem , Root , Item , } ; export type { RovingFocusGroupProps , RovingFocusItemProps } ;