@@ -116,6 +116,10 @@ interface StateDefinition<T> {
116116
117117 isTyping : boolean
118118
119+ inputElement : HTMLInputElement | null
120+ buttonElement : HTMLButtonElement | null
121+ optionsElement : HTMLElement | null
122+
119123 __demoMode : boolean
120124}
121125
@@ -132,6 +136,10 @@ enum ActionTypes {
132136 SetActivationTrigger ,
133137
134138 UpdateVirtualConfiguration ,
139+
140+ SetInputElement ,
141+ SetButtonElement ,
142+ SetOptionsElement ,
135143}
136144
137145function adjustOrderedState < T > (
@@ -192,6 +200,9 @@ type Actions<T> =
192200 options : T [ ]
193201 disabled : ( ( value : any ) => boolean ) | null
194202 }
203+ | { type : ActionTypes . SetInputElement ; element : HTMLInputElement | null }
204+ | { type : ActionTypes . SetButtonElement ; element : HTMLButtonElement | null }
205+ | { type : ActionTypes . SetOptionsElement ; element : HTMLElement | null }
195206
196207let reducers : {
197208 [ P in ActionTypes ] : < T > (
@@ -245,7 +256,7 @@ let reducers: {
245256 [ ActionTypes . GoToOption ] ( state , action ) {
246257 if ( state . dataRef . current ?. disabled ) return state
247258 if (
248- state . dataRef . current ?. optionsRef . current &&
259+ state . optionsElement &&
249260 ! state . dataRef . current ?. optionsPropsRef . current . static &&
250261 state . comboboxState === ComboboxState . Closed
251262 ) {
@@ -419,6 +430,18 @@ let reducers: {
419430 virtual : { options : action . options , disabled : action . disabled ?? ( ( ) => false ) } ,
420431 }
421432 } ,
433+ [ ActionTypes . SetInputElement ] : ( state , action ) => {
434+ if ( state . inputElement === action . element ) return state
435+ return { ...state , inputElement : action . element }
436+ } ,
437+ [ ActionTypes . SetButtonElement ] : ( state , action ) => {
438+ if ( state . buttonElement === action . element ) return state
439+ return { ...state , buttonElement : action . element }
440+ } ,
441+ [ ActionTypes . SetOptionsElement ] : ( state , action ) => {
442+ if ( state . optionsElement === action . element ) return state
443+ return { ...state , optionsElement : action . element }
444+ } ,
422445}
423446
424447let ComboboxActionsContext = createContext < {
@@ -431,6 +454,10 @@ let ComboboxActionsContext = createContext<{
431454 selectActiveOption ( ) : void
432455 setActivationTrigger ( trigger : ActivationTrigger ) : void
433456 onChange ( value : unknown ) : void
457+
458+ setInputElement ( element : HTMLInputElement | null ) : void
459+ setButtonElement ( element : HTMLButtonElement | null ) : void
460+ setOptionsElement ( element : HTMLElement | null ) : void
434461} | null > ( null )
435462ComboboxActionsContext . displayName = 'ComboboxActionsContext'
436463
@@ -455,7 +482,7 @@ function VirtualProvider(props: {
455482 let { options } = data . virtual !
456483
457484 let [ paddingStart , paddingEnd ] = useMemo ( ( ) => {
458- let el = data . optionsRef . current
485+ let el = data . optionsElement
459486 if ( ! el ) return [ 0 , 0 ]
460487
461488 let styles = window . getComputedStyle ( el )
@@ -464,7 +491,7 @@ function VirtualProvider(props: {
464491 parseFloat ( styles . paddingBlockStart || styles . paddingTop ) ,
465492 parseFloat ( styles . paddingBlockEnd || styles . paddingBottom ) ,
466493 ]
467- } , [ data . optionsRef . current ] )
494+ } , [ data . optionsElement ] )
468495
469496 let virtualizer = useVirtualizer ( {
470497 enabled : options . length !== 0 ,
@@ -475,7 +502,7 @@ function VirtualProvider(props: {
475502 return 40
476503 } ,
477504 getScrollElement ( ) {
478- return ( data . optionsRef . current ?? null ) as HTMLElement | null
505+ return data . optionsElement
479506 } ,
480507 overscan : 12 ,
481508 } )
@@ -573,10 +600,6 @@ let ComboboxDataContext = createContext<
573600 static : boolean
574601 hold : boolean
575602 } >
576-
577- inputRef : MutableRefObject < HTMLInputElement | null >
578- buttonRef : MutableRefObject < HTMLButtonElement | null >
579- optionsRef : MutableRefObject < HTMLElement | null >
580603 } & Omit < StateDefinition < unknown > , 'dataRef' > )
581604 | null
582605> ( null )
@@ -688,17 +711,16 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
688711 : null ,
689712 activeOptionIndex : null ,
690713 activationTrigger : ActivationTrigger . Other ,
714+ inputElement : null ,
715+ buttonElement : null ,
716+ optionsElement : null ,
691717 __demoMode,
692718 } as StateDefinition < TValue > )
693719
694720 let defaultToFirstOption = useRef ( false )
695721
696722 let optionsPropsRef = useRef < _Data [ 'optionsPropsRef' ] [ 'current' ] > ( { static : false , hold : false } )
697723
698- let inputRef = useRef < _Data [ 'inputRef' ] [ 'current' ] > ( null )
699- let buttonRef = useRef < _Data [ 'buttonRef' ] [ 'current' ] > ( null )
700- let optionsRef = useRef < _Data [ 'optionsRef' ] [ 'current' ] > ( null )
701-
702724 type TActualValue = true extends typeof multiple ? EnsureArray < TValue > [ number ] : TValue
703725 let compare = useByComparator < TActualValue > ( by )
704726
@@ -733,9 +755,6 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
733755 ...state ,
734756 immediate,
735757 optionsPropsRef,
736- inputRef,
737- buttonRef,
738- optionsRef,
739758 value,
740759 defaultValue,
741760 disabled,
@@ -791,8 +810,10 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
791810
792811 // Handle outside click
793812 let outsideClickEnabled = data . comboboxState === ComboboxState . Open
794- useOutsideClick ( outsideClickEnabled , [ data . buttonRef , data . inputRef , data . optionsRef ] , ( ) =>
795- actions . closeCombobox ( )
813+ useOutsideClick (
814+ outsideClickEnabled ,
815+ [ data . buttonElement , data . inputElement , data . optionsElement ] ,
816+ ( ) => actions . closeCombobox ( )
796817 )
797818
798819 let slot = useMemo ( ( ) => {
@@ -896,6 +917,18 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
896917 dispatch ( { type : ActionTypes . SetActivationTrigger , trigger } )
897918 } )
898919
920+ let setInputElement = useEvent ( ( element : HTMLInputElement | null ) => {
921+ dispatch ( { type : ActionTypes . SetInputElement , element } )
922+ } )
923+
924+ let setButtonElement = useEvent ( ( element : HTMLButtonElement | null ) => {
925+ dispatch ( { type : ActionTypes . SetButtonElement , element } )
926+ } )
927+
928+ let setOptionsElement = useEvent ( ( element : HTMLElement | null ) => {
929+ dispatch ( { type : ActionTypes . SetOptionsElement , element } )
930+ } )
931+
899932 let actions = useMemo < _Actions > (
900933 ( ) => ( {
901934 onChange,
@@ -906,6 +939,9 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
906939 openCombobox,
907940 setActivationTrigger,
908941 selectActiveOption,
942+ setInputElement,
943+ setButtonElement,
944+ setOptionsElement,
909945 } ) ,
910946 [ ]
911947 )
@@ -923,7 +959,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
923959 < LabelProvider
924960 value = { labelledby }
925961 props = { {
926- htmlFor : data . inputRef . current ?. id ,
962+ htmlFor : data . inputElement ?. id ,
927963 } }
928964 slot = { {
929965 open : data . comboboxState === ComboboxState . Open ,
@@ -1019,15 +1055,16 @@ function InputFn<
10191055 ...theirProps
10201056 } = props
10211057
1022- let inputRef = useSyncRefs ( data . inputRef , ref , useFloatingReference ( ) )
1023- let ownerDocument = useOwnerDocument ( data . inputRef )
1058+ let internalInputRef = useRef < HTMLInputElement | null > ( null )
1059+ let inputRef = useSyncRefs ( internalInputRef , ref , useFloatingReference ( ) , actions . setInputElement )
1060+ let ownerDocument = useOwnerDocument ( data . inputElement )
10241061
10251062 let d = useDisposables ( )
10261063
10271064 let clear = useEvent ( ( ) => {
10281065 actions . onChange ( null )
1029- if ( data . optionsRef . current ) {
1030- data . optionsRef . current . scrollTop = 0
1066+ if ( data . optionsElement ) {
1067+ data . optionsElement . scrollTop = 0
10311068 }
10321069 actions . goToOption ( Focus . Nothing )
10331070 } )
@@ -1071,7 +1108,7 @@ function InputFn<
10711108 // using an IME, we don't want to mess with the input at all.
10721109 if ( data . isTyping ) return
10731110
1074- let input = data . inputRef . current
1111+ let input = internalInputRef . current
10751112 if ( ! input ) return
10761113
10771114 if ( oldState === ComboboxState . Open && state === ComboboxState . Closed ) {
@@ -1121,7 +1158,7 @@ function InputFn<
11211158 // using an IME, we don't want to mess with the input at all.
11221159 if ( data . isTyping ) return
11231160
1124- let input = data . inputRef . current
1161+ let input = internalInputRef . current
11251162 if ( ! input ) return
11261163
11271164 // Capture current state
@@ -1232,7 +1269,7 @@ function InputFn<
12321269 case Keys . Escape :
12331270 if ( data . comboboxState !== ComboboxState . Open ) return
12341271 event . preventDefault ( )
1235- if ( data . optionsRef . current && ! data . optionsPropsRef . current . static ) {
1272+ if ( data . optionsElement && ! data . optionsPropsRef . current . static ) {
12361273 event . stopPropagation ( )
12371274 }
12381275
@@ -1286,10 +1323,10 @@ function InputFn<
12861323 ( event . relatedTarget as HTMLElement ) ?? history . find ( ( x ) => x !== event . currentTarget )
12871324
12881325 // Focus is moved into the list, we don't want to close yet.
1289- if ( data . optionsRef . current ?. contains ( relatedTarget ) ) return
1326+ if ( data . optionsElement ?. contains ( relatedTarget ) ) return
12901327
12911328 // Focus is moved to the button, we don't want to close yet.
1292- if ( data . buttonRef . current ?. contains ( relatedTarget ) ) return
1329+ if ( data . buttonElement ?. contains ( relatedTarget ) ) return
12931330
12941331 // Focus is moved, but the combobox is not open. This can mean two things:
12951332 //
@@ -1316,8 +1353,8 @@ function InputFn<
13161353 let handleFocus = useEvent ( ( event : ReactFocusEvent ) => {
13171354 let relatedTarget =
13181355 ( event . relatedTarget as HTMLElement ) ?? history . find ( ( x ) => x !== event . currentTarget )
1319- if ( data . buttonRef . current ?. contains ( relatedTarget ) ) return
1320- if ( data . optionsRef . current ?. contains ( relatedTarget ) ) return
1356+ if ( data . buttonElement ?. contains ( relatedTarget ) ) return
1357+ if ( data . optionsElement ?. contains ( relatedTarget ) ) return
13211358 if ( data . disabled ) return
13221359
13231360 if ( ! data . immediate ) return
@@ -1378,7 +1415,7 @@ function InputFn<
13781415 id,
13791416 role : 'combobox' ,
13801417 type,
1381- 'aria-controls' : data . optionsRef . current ?. id ,
1418+ 'aria-controls' : data . optionsElement ?. id ,
13821419 'aria-expanded' : data . comboboxState === ComboboxState . Open ,
13831420 'aria-activedescendant' :
13841421 data . activeOptionIndex === null
@@ -1457,7 +1494,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
14571494) {
14581495 let data = useData ( 'Combobox.Button' )
14591496 let actions = useActions ( 'Combobox.Button' )
1460- let buttonRef = useSyncRefs ( data . buttonRef , ref )
1497+ let buttonRef = useSyncRefs ( ref , actions . setButtonElement )
1498+
14611499 let internalId = useId ( )
14621500 let {
14631501 id = `headlessui-combobox-button-${ internalId } ` ,
@@ -1466,7 +1504,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
14661504 ...theirProps
14671505 } = props
14681506
1469- let refocusInput = useRefocusableInput ( data . inputRef )
1507+ let refocusInput = useRefocusableInput ( data . inputElement )
14701508
14711509 let handleKeyDown = useEvent ( ( event : ReactKeyboardEvent < HTMLElement > ) => {
14721510 switch ( event . key ) {
@@ -1505,7 +1543,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
15051543 case Keys . Escape :
15061544 if ( data . comboboxState !== ComboboxState . Open ) return
15071545 event . preventDefault ( )
1508- if ( data . optionsRef . current && ! data . optionsPropsRef . current . static ) {
1546+ if ( data . optionsElement && ! data . optionsPropsRef . current . static ) {
15091547 event . stopPropagation ( )
15101548 }
15111549 flushSync ( ( ) => actions . closeCombobox ( ) )
@@ -1561,10 +1599,10 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
15611599 {
15621600 ref : buttonRef ,
15631601 id,
1564- type : useResolveButtonType ( props , data . buttonRef ) ,
1602+ type : useResolveButtonType ( props , data . buttonElement ) ,
15651603 tabIndex : - 1 ,
15661604 'aria-haspopup' : 'listbox' ,
1567- 'aria-controls' : data . optionsRef . current ?. id ,
1605+ 'aria-controls' : data . optionsElement ?. id ,
15681606 'aria-expanded' : data . comboboxState === ComboboxState . Open ,
15691607 'aria-labelledby' : labelledBy ,
15701608 disabled : disabled || undefined ,
@@ -1635,20 +1673,20 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
16351673
16361674 let [ floatingRef , style ] = useFloatingPanel ( anchor )
16371675 let getFloatingPanelProps = useFloatingPanelProps ( )
1638- let optionsRef = useSyncRefs ( data . optionsRef , ref , anchor ? floatingRef : null )
1639- let ownerDocument = useOwnerDocument ( data . optionsRef )
1676+ let optionsRef = useSyncRefs ( ref , anchor ? floatingRef : null , actions . setOptionsElement )
1677+ let ownerDocument = useOwnerDocument ( data . optionsElement )
16401678
16411679 let usesOpenClosedState = useOpenClosed ( )
16421680 let [ visible , transitionData ] = useTransition (
16431681 transition ,
1644- data . optionsRef ,
1682+ data . optionsElement ,
16451683 usesOpenClosedState !== null
16461684 ? ( usesOpenClosedState & State . Open ) === State . Open
16471685 : data . comboboxState === ComboboxState . Open
16481686 )
16491687
16501688 // Ensure we close the combobox as soon as the input becomes hidden
1651- useOnDisappear ( visible , data . inputRef , actions . closeCombobox )
1689+ useOnDisappear ( visible , data . inputElement , actions . closeCombobox )
16521690
16531691 // Enable scroll locking when the combobox is visible, and `modal` is enabled
16541692 let scrollLockEnabled = data . __demoMode
@@ -1661,11 +1699,10 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
16611699 ? false
16621700 : modal && data . comboboxState === ComboboxState . Open
16631701 useInertOthers ( inertOthersEnabled , {
1664- allowed : useEvent ( ( ) => [
1665- data . inputRef . current ,
1666- data . buttonRef . current ,
1667- data . optionsRef . current ,
1668- ] ) ,
1702+ allowed : useCallback (
1703+ ( ) => [ data . inputElement , data . buttonElement , data . optionsElement ] ,
1704+ [ data . inputElement , data . buttonElement , data . optionsElement ]
1705+ ) ,
16691706 } )
16701707
16711708 useIsoMorphicEffect ( ( ) => {
@@ -1676,7 +1713,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
16761713 } , [ data . optionsPropsRef , hold ] )
16771714
16781715 useTreeWalker ( data . comboboxState === ComboboxState . Open , {
1679- container : data . optionsRef . current ,
1716+ container : data . optionsElement ,
16801717 accept ( node ) {
16811718 if ( node . getAttribute ( 'role' ) === 'option' ) return NodeFilter . FILTER_REJECT
16821719 if ( node . hasAttribute ( 'role' ) ) return NodeFilter . FILTER_SKIP
@@ -1687,7 +1724,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
16871724 } ,
16881725 } )
16891726
1690- let labelledBy = useLabelledBy ( [ data . buttonRef . current ?. id ] )
1727+ let labelledBy = useLabelledBy ( [ data . buttonElement ?. id ] )
16911728
16921729 let slot = useMemo ( ( ) => {
16931730 return {
@@ -1728,8 +1765,8 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
17281765 style : {
17291766 ...theirProps . style ,
17301767 ...style ,
1731- '--input-width' : useElementSize ( data . inputRef , true ) . width ,
1732- '--button-width' : useElementSize ( data . buttonRef , true ) . width ,
1768+ '--input-width' : useElementSize ( data . inputElement , true ) . width ,
1769+ '--button-width' : useElementSize ( data . buttonElement , true ) . width ,
17331770 } as CSSProperties ,
17341771 onWheel : data . activationTrigger === ActivationTrigger . Pointer ? undefined : handleWheel ,
17351772 onMouseDown : handleMouseDown ,
@@ -1840,7 +1877,7 @@ function OptionFn<
18401877 ...theirProps
18411878 } = props
18421879
1843- let refocusInput = useRefocusableInput ( data . inputRef )
1880+ let refocusInput = useRefocusableInput ( data . inputElement )
18441881
18451882 let active = data . virtual
18461883 ? data . activeOptionIndex === data . calculateIndex ( value )
0 commit comments