@@ -49,6 +49,7 @@ interface StateDefinition {
49
49
labelRef : MutableRefObject < HTMLLabelElement | null >
50
50
buttonRef : MutableRefObject < HTMLButtonElement | null >
51
51
optionsRef : MutableRefObject < HTMLUListElement | null >
52
+ disabled : boolean
52
53
options : { id : string ; dataRef : ListboxOptionDataRef } [ ]
53
54
searchQuery : string
54
55
activeOptionIndex : number | null
@@ -58,6 +59,8 @@ enum ActionTypes {
58
59
OpenListbox ,
59
60
CloseListbox ,
60
61
62
+ SetDisabled ,
63
+
61
64
GoToOption ,
62
65
Search ,
63
66
ClearSearch ,
@@ -69,6 +72,7 @@ enum ActionTypes {
69
72
type Actions =
70
73
| { type : ActionTypes . CloseListbox }
71
74
| { type : ActionTypes . OpenListbox }
75
+ | { type : ActionTypes . SetDisabled ; disabled : boolean }
72
76
| { type : ActionTypes . GoToOption ; focus : Focus . Specific ; id : string }
73
77
| { type : ActionTypes . GoToOption ; focus : Exclude < Focus , Focus . Specific > }
74
78
| { type : ActionTypes . Search ; value : string }
@@ -82,13 +86,24 @@ let reducers: {
82
86
action : Extract < Actions , { type : P } >
83
87
) => StateDefinition
84
88
} = {
85
- [ ActionTypes . CloseListbox ] : state => ( {
86
- ...state ,
87
- activeOptionIndex : null ,
88
- listboxState : ListboxStates . Closed ,
89
- } ) ,
90
- [ ActionTypes . OpenListbox ] : state => ( { ...state , listboxState : ListboxStates . Open } ) ,
91
- [ ActionTypes . GoToOption ] : ( state , action ) => {
89
+ [ ActionTypes . CloseListbox ] ( state ) {
90
+ if ( state . disabled ) return state
91
+ if ( state . listboxState === ListboxStates . Closed ) return state
92
+ return { ...state , activeOptionIndex : null , listboxState : ListboxStates . Closed }
93
+ } ,
94
+ [ ActionTypes . OpenListbox ] ( state ) {
95
+ if ( state . disabled ) return state
96
+ if ( state . listboxState === ListboxStates . Open ) return state
97
+ return { ...state , listboxState : ListboxStates . Open }
98
+ } ,
99
+ [ ActionTypes . SetDisabled ] ( state , action ) {
100
+ if ( state . disabled === action . disabled ) return state
101
+ return { ...state , disabled : action . disabled }
102
+ } ,
103
+ [ ActionTypes . GoToOption ] ( state , action ) {
104
+ if ( state . disabled ) return state
105
+ if ( state . listboxState === ListboxStates . Closed ) return state
106
+
92
107
let activeOptionIndex = calculateActiveIndex ( action , {
93
108
resolveItems : ( ) => state . options ,
94
109
resolveActiveIndex : ( ) => state . activeOptionIndex ,
@@ -100,6 +115,9 @@ let reducers: {
100
115
return { ...state , searchQuery : '' , activeOptionIndex }
101
116
} ,
102
117
[ ActionTypes . Search ] : ( state , action ) => {
118
+ if ( state . disabled ) return state
119
+ if ( state . listboxState === ListboxStates . Closed ) return state
120
+
103
121
let searchQuery = state . searchQuery + action . value
104
122
let match = state . options . findIndex (
105
123
option =>
@@ -110,7 +128,12 @@ let reducers: {
110
128
if ( match === - 1 || match === state . activeOptionIndex ) return { ...state , searchQuery }
111
129
return { ...state , searchQuery, activeOptionIndex : match }
112
130
} ,
113
- [ ActionTypes . ClearSearch ] : state => ( { ...state , searchQuery : '' } ) ,
131
+ [ ActionTypes . ClearSearch ] ( state ) {
132
+ if ( state . disabled ) return state
133
+ if ( state . listboxState === ListboxStates . Closed ) return state
134
+ if ( state . searchQuery === '' ) return state
135
+ return { ...state , searchQuery : '' }
136
+ } ,
114
137
[ ActionTypes . RegisterOption ] : ( state , action ) => ( {
115
138
...state ,
116
139
options : [ ...state . options , { id : action . id , dataRef : action . dataRef } ] ,
@@ -161,22 +184,25 @@ function stateReducer(state: StateDefinition, action: Actions) {
161
184
let DEFAULT_LISTBOX_TAG = Fragment
162
185
interface ListboxRenderPropArg {
163
186
open : boolean
187
+ disabled : boolean
164
188
}
165
189
166
190
export function Listbox < TTag extends ElementType = typeof DEFAULT_LISTBOX_TAG , TType = string > (
167
191
props : Props < TTag , ListboxRenderPropArg , 'value' | 'onChange' > & {
168
192
value : TType
169
193
onChange ( value : TType ) : void
194
+ disabled ?: boolean
170
195
}
171
196
) {
172
- let { value, onChange, ...passThroughProps } = props
197
+ let { value, onChange, disabled = false , ...passThroughProps } = props
173
198
let d = useDisposables ( )
174
199
let reducerBag = useReducer ( stateReducer , {
175
200
listboxState : ListboxStates . Closed ,
176
201
propsRef : { current : { value, onChange } } ,
177
202
labelRef : createRef ( ) ,
178
203
buttonRef : createRef ( ) ,
179
204
optionsRef : createRef ( ) ,
205
+ disabled,
180
206
options : [ ] ,
181
207
searchQuery : '' ,
182
208
activeOptionIndex : null ,
@@ -189,6 +215,7 @@ export function Listbox<TTag extends ElementType = typeof DEFAULT_LISTBOX_TAG, T
189
215
useIsoMorphicEffect ( ( ) => {
190
216
propsRef . current . onChange = onChange
191
217
} , [ onChange , propsRef ] )
218
+ useIsoMorphicEffect ( ( ) => dispatch ( { type : ActionTypes . SetDisabled , disabled } ) , [ disabled ] )
192
219
193
220
useEffect ( ( ) => {
194
221
function handler ( event : MouseEvent ) {
@@ -208,8 +235,8 @@ export function Listbox<TTag extends ElementType = typeof DEFAULT_LISTBOX_TAG, T
208
235
} , [ listboxState , optionsRef , buttonRef , d , dispatch ] )
209
236
210
237
let propsBag = useMemo < ListboxRenderPropArg > (
211
- ( ) => ( { open : listboxState === ListboxStates . Open } ) ,
212
- [ listboxState ]
238
+ ( ) => ( { open : listboxState === ListboxStates . Open , disabled } ) ,
239
+ [ listboxState , disabled ]
213
240
)
214
241
215
242
return (
@@ -224,6 +251,7 @@ export function Listbox<TTag extends ElementType = typeof DEFAULT_LISTBOX_TAG, T
224
251
let DEFAULT_BUTTON_TAG = 'button' as const
225
252
interface ButtonRenderPropArg {
226
253
open : boolean
254
+ disabled : boolean
227
255
}
228
256
type ButtonPropsWeControl =
229
257
| 'id'
@@ -232,6 +260,7 @@ type ButtonPropsWeControl =
232
260
| 'aria-controls'
233
261
| 'aria-expanded'
234
262
| 'aria-labelledby'
263
+ | 'disabled'
235
264
| 'onKeyDown'
236
265
| 'onClick'
237
266
@@ -279,7 +308,6 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
279
308
let handleClick = useCallback (
280
309
( event : ReactMouseEvent ) => {
281
310
if ( isDisabledReactIssue7711 ( event . currentTarget ) ) return event . preventDefault ( )
282
- if ( props . disabled ) return
283
311
if ( state . listboxState === ListboxStates . Open ) {
284
312
dispatch ( { type : ActionTypes . CloseListbox } )
285
313
d . nextFrame ( ( ) => state . buttonRef . current ?. focus ( { preventScroll : true } ) )
@@ -289,7 +317,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
289
317
d . nextFrame ( ( ) => state . optionsRef . current ?. focus ( { preventScroll : true } ) )
290
318
}
291
319
} ,
292
- [ dispatch , d , state , props . disabled ]
320
+ [ dispatch , d , state ]
293
321
)
294
322
295
323
let labelledby = useComputed ( ( ) => {
@@ -298,7 +326,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
298
326
} , [ state . labelRef . current , id ] )
299
327
300
328
let propsBag = useMemo < ButtonRenderPropArg > (
301
- ( ) => ( { open : state . listboxState === ListboxStates . Open } ) ,
329
+ ( ) => ( { open : state . listboxState === ListboxStates . Open , disabled : state . disabled } ) ,
302
330
[ state ]
303
331
)
304
332
let passthroughProps = props
@@ -310,6 +338,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
310
338
'aria-controls' : state . optionsRef . current ?. id ,
311
339
'aria-expanded' : state . listboxState === ListboxStates . Open ? true : undefined ,
312
340
'aria-labelledby' : labelledby ,
341
+ disabled : state . disabled ,
313
342
onKeyDown : handleKeyDown ,
314
343
onClick : handleClick ,
315
344
}
@@ -322,6 +351,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
322
351
let DEFAULT_LABEL_TAG = 'label' as const
323
352
interface LabelRenderPropArg {
324
353
open : boolean
354
+ disabled : boolean
325
355
}
326
356
type LabelPropsWeControl = 'id' | 'ref' | 'onClick'
327
357
@@ -336,7 +366,7 @@ function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
336
366
] )
337
367
338
368
let propsBag = useMemo < OptionsRenderPropArg > (
339
- ( ) => ( { open : state . listboxState === ListboxStates . Open } ) ,
369
+ ( ) => ( { open : state . listboxState === ListboxStates . Open , disabled : state . disabled } ) ,
340
370
[ state ]
341
371
)
342
372
let propsWeControl = { ref : state . labelRef , id, onClick : handleClick }
0 commit comments