@@ -28,7 +28,6 @@ import { useDefaultValue } from '../../hooks/use-default-value'
28
28
import { useDisposables } from '../../hooks/use-disposables'
29
29
import { useElementSize } from '../../hooks/use-element-size'
30
30
import { useEvent } from '../../hooks/use-event'
31
- import { useFrameDebounce } from '../../hooks/use-frame-debounce'
32
31
import { useId } from '../../hooks/use-id'
33
32
import { useInertOthers } from '../../hooks/use-inert-others'
34
33
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
@@ -112,6 +111,8 @@ interface StateDefinition<T> {
112
111
activeOptionIndex : number | null
113
112
activationTrigger : ActivationTrigger
114
113
114
+ isTyping : boolean
115
+
115
116
__demoMode : boolean
116
117
}
117
118
@@ -120,6 +121,7 @@ enum ActionTypes {
120
121
CloseCombobox ,
121
122
122
123
GoToOption ,
124
+ SetTyping ,
123
125
124
126
RegisterOption ,
125
127
UnregisterOption ,
@@ -170,6 +172,7 @@ type Actions<T> =
170
172
idx : number
171
173
trigger ?: ActivationTrigger
172
174
}
175
+ | { type : ActionTypes . SetTyping ; isTyping : boolean }
173
176
| {
174
177
type : ActionTypes . GoToOption
175
178
focus : Exclude < Focus , Focus . Specific >
@@ -202,6 +205,8 @@ let reducers: {
202
205
activeOptionIndex : null ,
203
206
comboboxState : ComboboxState . Closed ,
204
207
208
+ isTyping : false ,
209
+
205
210
// Clear the last known activation trigger
206
211
// This is because if a user interacts with the combobox using a mouse
207
212
// resulting in it closing we might incorrectly handle the next interaction
@@ -230,6 +235,10 @@ let reducers: {
230
235
231
236
return { ...state , comboboxState : ComboboxState . Open , __demoMode : false }
232
237
} ,
238
+ [ ActionTypes . SetTyping ] ( state , action ) {
239
+ if ( state . isTyping === action . isTyping ) return state
240
+ return { ...state , isTyping : action . isTyping }
241
+ } ,
233
242
[ ActionTypes . GoToOption ] ( state , action ) {
234
243
if ( state . dataRef . current ?. disabled ) return state
235
244
if (
@@ -268,6 +277,7 @@ let reducers: {
268
277
...state ,
269
278
activeOptionIndex,
270
279
activationTrigger,
280
+ isTyping : false ,
271
281
__demoMode : false ,
272
282
}
273
283
}
@@ -308,6 +318,7 @@ let reducers: {
308
318
return {
309
319
...state ,
310
320
...adjustedState ,
321
+ isTyping : false ,
311
322
activeOptionIndex,
312
323
activationTrigger,
313
324
__demoMode : false ,
@@ -413,6 +424,7 @@ let ComboboxActionsContext = createContext<{
413
424
registerOption ( id : string , dataRef : ComboboxOptionDataRef < unknown > ) : ( ) => void
414
425
goToOption ( focus : Focus . Specific , idx : number , trigger ?: ActivationTrigger ) : void
415
426
goToOption ( focus : Focus , idx ?: number , trigger ?: ActivationTrigger ) : void
427
+ setIsTyping ( isTyping : boolean ) : void
416
428
selectActiveOption ( ) : void
417
429
setActivationTrigger ( trigger : ActivationTrigger ) : void
418
430
onChange ( value : unknown ) : void
@@ -662,6 +674,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
662
674
let [ state , dispatch ] = useReducer ( stateReducer , {
663
675
dataRef : createRef ( ) ,
664
676
comboboxState : __demoMode ? ComboboxState . Open : ComboboxState . Closed ,
677
+ isTyping : false ,
665
678
options : [ ] ,
666
679
virtual : virtual
667
680
? { options : virtual . options , disabled : virtual . disabled ?? ( ( ) => false ) }
@@ -793,6 +806,8 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
793
806
let selectActiveOption = useEvent ( ( ) => {
794
807
if ( data . activeOptionIndex === null ) return
795
808
809
+ actions . setIsTyping ( false )
810
+
796
811
if ( data . virtual ) {
797
812
onChange ( data . virtual . options [ data . activeOptionIndex ] )
798
813
} else {
@@ -816,6 +831,10 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
816
831
onClose ?.( )
817
832
} )
818
833
834
+ let setIsTyping = useEvent ( ( isTyping : boolean ) => {
835
+ dispatch ( { type : ActionTypes . SetTyping , isTyping } )
836
+ } )
837
+
819
838
let goToOption = useEvent ( ( focus , idx , trigger ) => {
820
839
defaultToFirstOption . current = false
821
840
@@ -875,6 +894,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
875
894
onChange,
876
895
registerOption,
877
896
goToOption,
897
+ setIsTyping,
878
898
closeCombobox,
879
899
openCombobox,
880
900
setActivationTrigger,
@@ -995,8 +1015,6 @@ function InputFn<
995
1015
let inputRef = useSyncRefs ( data . inputRef , ref , useFloatingReference ( ) )
996
1016
let ownerDocument = useOwnerDocument ( data . inputRef )
997
1017
998
- let isTyping = useRef ( false )
999
-
1000
1018
let d = useDisposables ( )
1001
1019
1002
1020
let clear = useEvent ( ( ) => {
@@ -1044,7 +1062,7 @@ function InputFn<
1044
1062
( [ currentDisplayValue , state ] , [ oldCurrentDisplayValue , oldState ] ) => {
1045
1063
// When the user is typing, we want to not touch the `input` at all. Especially when they are
1046
1064
// using an IME, we don't want to mess with the input at all.
1047
- if ( isTyping . current ) return
1065
+ if ( data . isTyping ) return
1048
1066
1049
1067
let input = data . inputRef . current
1050
1068
if ( ! input ) return
@@ -1060,7 +1078,7 @@ function InputFn<
1060
1078
// the user is currently typing, because we don't want to mess with the cursor position while
1061
1079
// typing.
1062
1080
requestAnimationFrame ( ( ) => {
1063
- if ( isTyping . current ) return
1081
+ if ( data . isTyping ) return
1064
1082
if ( ! input ) return
1065
1083
1066
1084
// Bail when the input is not the currently focused element. When it is not the focused
@@ -1080,7 +1098,7 @@ function InputFn<
1080
1098
input . setSelectionRange ( input . value . length , input . value . length )
1081
1099
} )
1082
1100
} ,
1083
- [ currentDisplayValue , data . comboboxState , ownerDocument ]
1101
+ [ currentDisplayValue , data . comboboxState , ownerDocument , data . isTyping ]
1084
1102
)
1085
1103
1086
1104
// Trick VoiceOver in behaving a little bit better. Manually "resetting" the input makes VoiceOver
@@ -1094,7 +1112,7 @@ function InputFn<
1094
1112
if ( newState === ComboboxState . Open && oldState === ComboboxState . Closed ) {
1095
1113
// When the user is typing, we want to not touch the `input` at all. Especially when they are
1096
1114
// using an IME, we don't want to mess with the input at all.
1097
- if ( isTyping . current ) return
1115
+ if ( data . isTyping ) return
1098
1116
1099
1117
let input = data . inputRef . current
1100
1118
if ( ! input ) return
@@ -1128,18 +1146,13 @@ function InputFn<
1128
1146
} )
1129
1147
} )
1130
1148
1131
- let debounce = useFrameDebounce ( )
1132
1149
let handleKeyDown = useEvent ( ( event : ReactKeyboardEvent < HTMLInputElement > ) => {
1133
- isTyping . current = true
1134
- debounce ( ( ) => {
1135
- isTyping . current = false
1136
- } )
1150
+ actions . setIsTyping ( true )
1137
1151
1138
1152
switch ( event . key ) {
1139
1153
// Ref: https://www.w3.org/WAI/ARIA/apg/patterns/menu/#keyboard-interaction-12
1140
1154
1141
1155
case Keys . Enter :
1142
- isTyping . current = false
1143
1156
if ( data . comboboxState !== ComboboxState . Open ) return
1144
1157
1145
1158
// When the user is still in the middle of composing by using an IME, then we don't want to
@@ -1162,16 +1175,15 @@ function InputFn<
1162
1175
break
1163
1176
1164
1177
case Keys . ArrowDown :
1165
- isTyping . current = false
1166
1178
event . preventDefault ( )
1167
1179
event . stopPropagation ( )
1180
+
1168
1181
return match ( data . comboboxState , {
1169
1182
[ ComboboxState . Open ] : ( ) => actions . goToOption ( Focus . Next ) ,
1170
1183
[ ComboboxState . Closed ] : ( ) => actions . openCombobox ( ) ,
1171
1184
} )
1172
1185
1173
1186
case Keys . ArrowUp :
1174
- isTyping . current = false
1175
1187
event . preventDefault ( )
1176
1188
event . stopPropagation ( )
1177
1189
return match ( data . comboboxState , {
@@ -1191,13 +1203,11 @@ function InputFn<
1191
1203
break
1192
1204
}
1193
1205
1194
- isTyping . current = false
1195
1206
event . preventDefault ( )
1196
1207
event . stopPropagation ( )
1197
1208
return actions . goToOption ( Focus . First )
1198
1209
1199
1210
case Keys . PageUp :
1200
- isTyping . current = false
1201
1211
event . preventDefault ( )
1202
1212
event . stopPropagation ( )
1203
1213
return actions . goToOption ( Focus . First )
@@ -1207,19 +1217,16 @@ function InputFn<
1207
1217
break
1208
1218
}
1209
1219
1210
- isTyping . current = false
1211
1220
event . preventDefault ( )
1212
1221
event . stopPropagation ( )
1213
1222
return actions . goToOption ( Focus . Last )
1214
1223
1215
1224
case Keys . PageDown :
1216
- isTyping . current = false
1217
1225
event . preventDefault ( )
1218
1226
event . stopPropagation ( )
1219
1227
return actions . goToOption ( Focus . Last )
1220
1228
1221
1229
case Keys . Escape :
1222
- isTyping . current = false
1223
1230
if ( data . comboboxState !== ComboboxState . Open ) return
1224
1231
event . preventDefault ( )
1225
1232
if ( data . optionsRef . current && ! data . optionsPropsRef . current . static ) {
@@ -1240,7 +1247,6 @@ function InputFn<
1240
1247
return actions . closeCombobox ( )
1241
1248
1242
1249
case Keys . Tab :
1243
- isTyping . current = false
1244
1250
if ( data . comboboxState !== ComboboxState . Open ) return
1245
1251
if ( data . mode === ValueMode . Single && data . activationTrigger !== ActivationTrigger . Focus ) {
1246
1252
actions . selectActiveOption ( )
@@ -1275,7 +1281,6 @@ function InputFn<
1275
1281
let handleBlur = useEvent ( ( event : ReactFocusEvent ) => {
1276
1282
let relatedTarget =
1277
1283
( event . relatedTarget as HTMLElement ) ?? history . find ( ( x ) => x !== event . currentTarget )
1278
- isTyping . current = false
1279
1284
1280
1285
// Focus is moved into the list, we don't want to close yet.
1281
1286
if ( data . optionsRef . current ?. contains ( relatedTarget ) ) return
@@ -1819,7 +1824,10 @@ function OptionFn<
1819
1824
virtualizer ? virtualizer . measureElement : null
1820
1825
)
1821
1826
1822
- let select = useEvent ( ( ) => actions . onChange ( value ) )
1827
+ let select = useEvent ( ( ) => {
1828
+ actions . setIsTyping ( false )
1829
+ actions . onChange ( value )
1830
+ } )
1823
1831
useIsoMorphicEffect ( ( ) => actions . registerOption ( id , bag ) , [ bag , id ] )
1824
1832
1825
1833
let enableScrollIntoView = useRef ( data . virtual || data . __demoMode ? false : true )
0 commit comments