@@ -116,6 +116,10 @@ interface StateDefinition<T> {
116
116
117
117
isTyping : boolean
118
118
119
+ inputElement : HTMLInputElement | null
120
+ buttonElement : HTMLButtonElement | null
121
+ optionsElement : HTMLElement | null
122
+
119
123
__demoMode : boolean
120
124
}
121
125
@@ -132,6 +136,10 @@ enum ActionTypes {
132
136
SetActivationTrigger ,
133
137
134
138
UpdateVirtualConfiguration ,
139
+
140
+ SetInputElement ,
141
+ SetButtonElement ,
142
+ SetOptionsElement ,
135
143
}
136
144
137
145
function adjustOrderedState < T > (
@@ -192,6 +200,9 @@ type Actions<T> =
192
200
options : T [ ]
193
201
disabled : ( ( value : any ) => boolean ) | null
194
202
}
203
+ | { type : ActionTypes . SetInputElement ; element : HTMLInputElement | null }
204
+ | { type : ActionTypes . SetButtonElement ; element : HTMLButtonElement | null }
205
+ | { type : ActionTypes . SetOptionsElement ; element : HTMLElement | null }
195
206
196
207
let reducers : {
197
208
[ P in ActionTypes ] : < T > (
@@ -245,7 +256,7 @@ let reducers: {
245
256
[ ActionTypes . GoToOption ] ( state , action ) {
246
257
if ( state . dataRef . current ?. disabled ) return state
247
258
if (
248
- state . dataRef . current ?. optionsRef . current &&
259
+ state . optionsElement &&
249
260
! state . dataRef . current ?. optionsPropsRef . current . static &&
250
261
state . comboboxState === ComboboxState . Closed
251
262
) {
@@ -419,6 +430,18 @@ let reducers: {
419
430
virtual : { options : action . options , disabled : action . disabled ?? ( ( ) => false ) } ,
420
431
}
421
432
} ,
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
+ } ,
422
445
}
423
446
424
447
let ComboboxActionsContext = createContext < {
@@ -431,6 +454,10 @@ let ComboboxActionsContext = createContext<{
431
454
selectActiveOption ( ) : void
432
455
setActivationTrigger ( trigger : ActivationTrigger ) : void
433
456
onChange ( value : unknown ) : void
457
+
458
+ setInputElement ( element : HTMLInputElement | null ) : void
459
+ setButtonElement ( element : HTMLButtonElement | null ) : void
460
+ setOptionsElement ( element : HTMLElement | null ) : void
434
461
} | null > ( null )
435
462
ComboboxActionsContext . displayName = 'ComboboxActionsContext'
436
463
@@ -455,7 +482,7 @@ function VirtualProvider(props: {
455
482
let { options } = data . virtual !
456
483
457
484
let [ paddingStart , paddingEnd ] = useMemo ( ( ) => {
458
- let el = data . optionsRef . current
485
+ let el = data . optionsElement
459
486
if ( ! el ) return [ 0 , 0 ]
460
487
461
488
let styles = window . getComputedStyle ( el )
@@ -464,7 +491,7 @@ function VirtualProvider(props: {
464
491
parseFloat ( styles . paddingBlockStart || styles . paddingTop ) ,
465
492
parseFloat ( styles . paddingBlockEnd || styles . paddingBottom ) ,
466
493
]
467
- } , [ data . optionsRef . current ] )
494
+ } , [ data . optionsElement ] )
468
495
469
496
let virtualizer = useVirtualizer ( {
470
497
enabled : options . length !== 0 ,
@@ -475,7 +502,7 @@ function VirtualProvider(props: {
475
502
return 40
476
503
} ,
477
504
getScrollElement ( ) {
478
- return ( data . optionsRef . current ?? null ) as HTMLElement | null
505
+ return data . optionsElement
479
506
} ,
480
507
overscan : 12 ,
481
508
} )
@@ -573,10 +600,6 @@ let ComboboxDataContext = createContext<
573
600
static : boolean
574
601
hold : boolean
575
602
} >
576
-
577
- inputRef : MutableRefObject < HTMLInputElement | null >
578
- buttonRef : MutableRefObject < HTMLButtonElement | null >
579
- optionsRef : MutableRefObject < HTMLElement | null >
580
603
} & Omit < StateDefinition < unknown > , 'dataRef' > )
581
604
| null
582
605
> ( null )
@@ -688,17 +711,16 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
688
711
: null ,
689
712
activeOptionIndex : null ,
690
713
activationTrigger : ActivationTrigger . Other ,
714
+ inputElement : null ,
715
+ buttonElement : null ,
716
+ optionsElement : null ,
691
717
__demoMode,
692
718
} as StateDefinition < TValue > )
693
719
694
720
let defaultToFirstOption = useRef ( false )
695
721
696
722
let optionsPropsRef = useRef < _Data [ 'optionsPropsRef' ] [ 'current' ] > ( { static : false , hold : false } )
697
723
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
-
702
724
type TActualValue = true extends typeof multiple ? EnsureArray < TValue > [ number ] : TValue
703
725
let compare = useByComparator < TActualValue > ( by )
704
726
@@ -733,9 +755,6 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
733
755
...state ,
734
756
immediate,
735
757
optionsPropsRef,
736
- inputRef,
737
- buttonRef,
738
- optionsRef,
739
758
value,
740
759
defaultValue,
741
760
disabled,
@@ -791,8 +810,10 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
791
810
792
811
// Handle outside click
793
812
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 ( )
796
817
)
797
818
798
819
let slot = useMemo ( ( ) => {
@@ -896,6 +917,18 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
896
917
dispatch ( { type : ActionTypes . SetActivationTrigger , trigger } )
897
918
} )
898
919
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
+
899
932
let actions = useMemo < _Actions > (
900
933
( ) => ( {
901
934
onChange,
@@ -906,6 +939,9 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
906
939
openCombobox,
907
940
setActivationTrigger,
908
941
selectActiveOption,
942
+ setInputElement,
943
+ setButtonElement,
944
+ setOptionsElement,
909
945
} ) ,
910
946
[ ]
911
947
)
@@ -923,7 +959,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
923
959
< LabelProvider
924
960
value = { labelledby }
925
961
props = { {
926
- htmlFor : data . inputRef . current ?. id ,
962
+ htmlFor : data . inputElement ?. id ,
927
963
} }
928
964
slot = { {
929
965
open : data . comboboxState === ComboboxState . Open ,
@@ -1019,15 +1055,16 @@ function InputFn<
1019
1055
...theirProps
1020
1056
} = props
1021
1057
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 )
1024
1061
1025
1062
let d = useDisposables ( )
1026
1063
1027
1064
let clear = useEvent ( ( ) => {
1028
1065
actions . onChange ( null )
1029
- if ( data . optionsRef . current ) {
1030
- data . optionsRef . current . scrollTop = 0
1066
+ if ( data . optionsElement ) {
1067
+ data . optionsElement . scrollTop = 0
1031
1068
}
1032
1069
actions . goToOption ( Focus . Nothing )
1033
1070
} )
@@ -1071,7 +1108,7 @@ function InputFn<
1071
1108
// using an IME, we don't want to mess with the input at all.
1072
1109
if ( data . isTyping ) return
1073
1110
1074
- let input = data . inputRef . current
1111
+ let input = internalInputRef . current
1075
1112
if ( ! input ) return
1076
1113
1077
1114
if ( oldState === ComboboxState . Open && state === ComboboxState . Closed ) {
@@ -1121,7 +1158,7 @@ function InputFn<
1121
1158
// using an IME, we don't want to mess with the input at all.
1122
1159
if ( data . isTyping ) return
1123
1160
1124
- let input = data . inputRef . current
1161
+ let input = internalInputRef . current
1125
1162
if ( ! input ) return
1126
1163
1127
1164
// Capture current state
@@ -1232,7 +1269,7 @@ function InputFn<
1232
1269
case Keys . Escape :
1233
1270
if ( data . comboboxState !== ComboboxState . Open ) return
1234
1271
event . preventDefault ( )
1235
- if ( data . optionsRef . current && ! data . optionsPropsRef . current . static ) {
1272
+ if ( data . optionsElement && ! data . optionsPropsRef . current . static ) {
1236
1273
event . stopPropagation ( )
1237
1274
}
1238
1275
@@ -1286,10 +1323,10 @@ function InputFn<
1286
1323
( event . relatedTarget as HTMLElement ) ?? history . find ( ( x ) => x !== event . currentTarget )
1287
1324
1288
1325
// 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
1290
1327
1291
1328
// 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
1293
1330
1294
1331
// Focus is moved, but the combobox is not open. This can mean two things:
1295
1332
//
@@ -1316,8 +1353,8 @@ function InputFn<
1316
1353
let handleFocus = useEvent ( ( event : ReactFocusEvent ) => {
1317
1354
let relatedTarget =
1318
1355
( 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
1321
1358
if ( data . disabled ) return
1322
1359
1323
1360
if ( ! data . immediate ) return
@@ -1378,7 +1415,7 @@ function InputFn<
1378
1415
id,
1379
1416
role : 'combobox' ,
1380
1417
type,
1381
- 'aria-controls' : data . optionsRef . current ?. id ,
1418
+ 'aria-controls' : data . optionsElement ?. id ,
1382
1419
'aria-expanded' : data . comboboxState === ComboboxState . Open ,
1383
1420
'aria-activedescendant' :
1384
1421
data . activeOptionIndex === null
@@ -1457,7 +1494,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
1457
1494
) {
1458
1495
let data = useData ( 'Combobox.Button' )
1459
1496
let actions = useActions ( 'Combobox.Button' )
1460
- let buttonRef = useSyncRefs ( data . buttonRef , ref )
1497
+ let buttonRef = useSyncRefs ( ref , actions . setButtonElement )
1498
+
1461
1499
let internalId = useId ( )
1462
1500
let {
1463
1501
id = `headlessui-combobox-button-${ internalId } ` ,
@@ -1466,7 +1504,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
1466
1504
...theirProps
1467
1505
} = props
1468
1506
1469
- let refocusInput = useRefocusableInput ( data . inputRef )
1507
+ let refocusInput = useRefocusableInput ( data . inputElement )
1470
1508
1471
1509
let handleKeyDown = useEvent ( ( event : ReactKeyboardEvent < HTMLElement > ) => {
1472
1510
switch ( event . key ) {
@@ -1505,7 +1543,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
1505
1543
case Keys . Escape :
1506
1544
if ( data . comboboxState !== ComboboxState . Open ) return
1507
1545
event . preventDefault ( )
1508
- if ( data . optionsRef . current && ! data . optionsPropsRef . current . static ) {
1546
+ if ( data . optionsElement && ! data . optionsPropsRef . current . static ) {
1509
1547
event . stopPropagation ( )
1510
1548
}
1511
1549
flushSync ( ( ) => actions . closeCombobox ( ) )
@@ -1561,10 +1599,10 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
1561
1599
{
1562
1600
ref : buttonRef ,
1563
1601
id,
1564
- type : useResolveButtonType ( props , data . buttonRef ) ,
1602
+ type : useResolveButtonType ( props , data . buttonElement ) ,
1565
1603
tabIndex : - 1 ,
1566
1604
'aria-haspopup' : 'listbox' ,
1567
- 'aria-controls' : data . optionsRef . current ?. id ,
1605
+ 'aria-controls' : data . optionsElement ?. id ,
1568
1606
'aria-expanded' : data . comboboxState === ComboboxState . Open ,
1569
1607
'aria-labelledby' : labelledBy ,
1570
1608
disabled : disabled || undefined ,
@@ -1635,20 +1673,20 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
1635
1673
1636
1674
let [ floatingRef , style ] = useFloatingPanel ( anchor )
1637
1675
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 )
1640
1678
1641
1679
let usesOpenClosedState = useOpenClosed ( )
1642
1680
let [ visible , transitionData ] = useTransition (
1643
1681
transition ,
1644
- data . optionsRef ,
1682
+ data . optionsElement ,
1645
1683
usesOpenClosedState !== null
1646
1684
? ( usesOpenClosedState & State . Open ) === State . Open
1647
1685
: data . comboboxState === ComboboxState . Open
1648
1686
)
1649
1687
1650
1688
// 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 )
1652
1690
1653
1691
// Enable scroll locking when the combobox is visible, and `modal` is enabled
1654
1692
let scrollLockEnabled = data . __demoMode
@@ -1661,11 +1699,10 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
1661
1699
? false
1662
1700
: modal && data . comboboxState === ComboboxState . Open
1663
1701
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
+ ) ,
1669
1706
} )
1670
1707
1671
1708
useIsoMorphicEffect ( ( ) => {
@@ -1676,7 +1713,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
1676
1713
} , [ data . optionsPropsRef , hold ] )
1677
1714
1678
1715
useTreeWalker ( data . comboboxState === ComboboxState . Open , {
1679
- container : data . optionsRef . current ,
1716
+ container : data . optionsElement ,
1680
1717
accept ( node ) {
1681
1718
if ( node . getAttribute ( 'role' ) === 'option' ) return NodeFilter . FILTER_REJECT
1682
1719
if ( node . hasAttribute ( 'role' ) ) return NodeFilter . FILTER_SKIP
@@ -1687,7 +1724,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
1687
1724
} ,
1688
1725
} )
1689
1726
1690
- let labelledBy = useLabelledBy ( [ data . buttonRef . current ?. id ] )
1727
+ let labelledBy = useLabelledBy ( [ data . buttonElement ?. id ] )
1691
1728
1692
1729
let slot = useMemo ( ( ) => {
1693
1730
return {
@@ -1728,8 +1765,8 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
1728
1765
style : {
1729
1766
...theirProps . style ,
1730
1767
...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 ,
1733
1770
} as CSSProperties ,
1734
1771
onWheel : data . activationTrigger === ActivationTrigger . Pointer ? undefined : handleWheel ,
1735
1772
onMouseDown : handleMouseDown ,
@@ -1840,7 +1877,7 @@ function OptionFn<
1840
1877
...theirProps
1841
1878
} = props
1842
1879
1843
- let refocusInput = useRefocusableInput ( data . inputRef )
1880
+ let refocusInput = useRefocusableInput ( data . inputElement )
1844
1881
1845
1882
let active = data . virtual
1846
1883
? data . activeOptionIndex === data . calculateIndex ( value )
0 commit comments