@@ -27,6 +27,7 @@ const Dropdown = (props: DropdownProps) => {
2727 className,
2828 closeOnSelect,
2929 clearable,
30+ debounce,
3031 disabled,
3132 labels,
3233 maxHeight,
@@ -42,6 +43,7 @@ const Dropdown = (props: DropdownProps) => {
4243 const [ optionsCheck , setOptionsCheck ] = useState < DetailedOption [ ] > ( ) ;
4344 const [ isOpen , setIsOpen ] = useState ( false ) ;
4445 const [ displayOptions , setDisplayOptions ] = useState < DetailedOption [ ] > ( [ ] ) ;
46+ const [ val , setVal ] = useState < DropdownProps [ 'value' ] > ( value ) ;
4547 const persistentOptions = useRef < DropdownProps [ 'options' ] > ( [ ] ) ;
4648 const dropdownContainerRef = useRef < HTMLButtonElement > ( null ) ;
4749 const dropdownContentRef = useRef < HTMLDivElement > (
@@ -52,6 +54,13 @@ const Dropdown = (props: DropdownProps) => {
5254 const ctx = window . dash_component_api . useDashContext ( ) ;
5355 const loading = ctx . useLoading ( ) ;
5456
57+ // Sync val when external value prop changes
58+ useEffect ( ( ) => {
59+ if ( ! isEqual ( value , val ) ) {
60+ setVal ( value ) ;
61+ }
62+ } , [ value ] ) ;
63+
5564 if ( ! persistentOptions || ! isEqual ( options , persistentOptions . current ) ) {
5665 persistentOptions . current = options ;
5766 }
@@ -67,14 +76,27 @@ const Dropdown = (props: DropdownProps) => {
6776 ) ;
6877
6978 const sanitizedValues : OptionValue [ ] = useMemo ( ( ) => {
70- if ( value instanceof Array ) {
71- return value ;
79+ if ( val instanceof Array ) {
80+ return val ;
7281 }
73- if ( isNil ( value ) ) {
82+ if ( isNil ( val ) ) {
7483 return [ ] ;
7584 }
76- return [ value ] ;
77- } , [ value ] ) ;
85+ return [ val ] ;
86+ } , [ val ] ) ;
87+
88+ const handleSetProps = useCallback (
89+ ( newValue : DropdownProps [ 'value' ] ) => {
90+ if ( debounce && isOpen ) {
91+ // local only
92+ setVal ( newValue ) ;
93+ } else {
94+ setVal ( newValue ) ;
95+ setProps ( { value : newValue } ) ;
96+ }
97+ } ,
98+ [ debounce , isOpen , setProps ]
99+ ) ;
78100
79101 const updateSelection = useCallback (
80102 ( selection : OptionValue [ ] ) => {
@@ -87,30 +109,28 @@ const Dropdown = (props: DropdownProps) => {
87109 if ( selection . length === 0 ) {
88110 // Empty selection: only allow if clearable is true
89111 if ( clearable ) {
90- setProps ( { value : [ ] } ) ;
112+ handleSetProps ( [ ] ) ;
91113 }
92114 // If clearable is false and trying to set empty, do nothing
93115 // return;
94116 } else {
95- // Non-empty selection: always allowed in multi-select
96- setProps ( { value : selection } ) ;
117+ handleSetProps ( selection ) ;
97118 }
98119 } else {
99120 // For single-select, take the first value or null
100121 if ( selection . length === 0 ) {
101122 // Empty selection: only allow if clearable is true
102123 if ( clearable ) {
103- setProps ( { value : null } ) ;
124+ handleSetProps ( null ) ;
104125 }
105126 // If clearable is false and trying to set empty, do nothing
106127 // return;
107128 } else {
108- // Take the first value for single-select
109- setProps ( { value : selection [ selection . length - 1 ] } ) ;
129+ handleSetProps ( selection [ selection . length - 1 ] ) ;
110130 }
111131 }
112132 } ,
113- [ multi , clearable , closeOnSelect ]
133+ [ multi , clearable , closeOnSelect , handleSetProps ]
114134 ) ;
115135
116136 const onInputChange = useCallback (
@@ -179,8 +199,8 @@ const Dropdown = (props: DropdownProps) => {
179199
180200 const handleClear = useCallback ( ( ) => {
181201 const finalValue : DropdownProps [ 'value' ] = multi ? [ ] : null ;
182- setProps ( { value : finalValue } ) ;
183- } , [ multi ] ) ;
202+ handleSetProps ( finalValue ) ;
203+ } , [ multi , handleSetProps ] ) ;
184204
185205 const handleSelectAll = useCallback ( ( ) => {
186206 if ( multi ) {
@@ -189,12 +209,12 @@ const Dropdown = (props: DropdownProps) => {
189209 . filter ( option => ! sanitizedValues . includes ( option . value ) )
190210 . map ( option => option . value )
191211 ) ;
192- setProps ( { value : allValues } ) ;
212+ handleSetProps ( allValues ) ;
193213 }
194214 if ( closeOnSelect ) {
195215 setIsOpen ( false ) ;
196216 }
197- } , [ multi , displayOptions , sanitizedValues , closeOnSelect ] ) ;
217+ } , [ multi , displayOptions , sanitizedValues , closeOnSelect , handleSetProps ] ) ;
198218
199219 const handleDeselectAll = useCallback ( ( ) => {
200220 if ( multi ) {
@@ -203,12 +223,12 @@ const Dropdown = (props: DropdownProps) => {
203223 displayOption => displayOption . value === option
204224 ) ;
205225 } ) ;
206- setProps ( { value : withDeselected } ) ;
226+ handleSetProps ( withDeselected ) ;
207227 }
208228 if ( closeOnSelect ) {
209229 setIsOpen ( false ) ;
210230 }
211- } , [ multi , displayOptions , sanitizedValues , closeOnSelect ] ) ;
231+ } , [ multi , displayOptions , sanitizedValues , closeOnSelect , handleSetProps ] ) ;
212232
213233 // Sort options when popover opens - selected options first
214234 // Update display options when filtered options or selection changes
@@ -354,16 +374,29 @@ const Dropdown = (props: DropdownProps) => {
354374 } , [ ] ) ;
355375
356376 // Handle popover open/close
357- const handleOpenChange = useCallback (
358- ( open : boolean ) => {
359- setIsOpen ( open ) ;
377+ const handleOpenChange = useCallback (
378+ ( open : boolean ) => {
379+ setIsOpen ( open ) ;
360380
361- if ( ! open ) {
362- setProps ( { search_value : undefined } ) ;
381+ if ( ! open ) {
382+ const updates : Partial < DropdownProps > = { } ;
383+
384+ if ( ! isNil ( search_value ) ) {
385+ updates . search_value = undefined ;
363386 }
364- } ,
365- [ filteredOptions , sanitizedValues ]
366- ) ;
387+
388+ // Commit debounced value on close only
389+ if ( debounce && ! isEqual ( value , val ) ) {
390+ updates . value = val ;
391+ }
392+
393+ if ( Object . keys ( updates ) . length > 0 ) {
394+ setProps ( updates ) ;
395+ }
396+ }
397+ } ,
398+ [ debounce , value , val , search_value , setProps ]
399+ ) ;
367400
368401 const accessibleId = id ?? uuid ( ) ;
369402 const positioningContainerRef = useRef < HTMLDivElement > ( null ) ;
0 commit comments