@@ -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 > (
@@ -53,6 +55,13 @@ const Dropdown = (props: DropdownProps) => {
5355 const ctx = window . dash_component_api . useDashContext ( ) ;
5456 const loading = ctx . useLoading ( ) ;
5557
58+ // Sync val when external value prop changes
59+ useEffect ( ( ) => {
60+ if ( ! isEqual ( value , val ) ) {
61+ setVal ( value ) ;
62+ }
63+ } , [ value ] ) ;
64+
5665 if ( ! persistentOptions || ! isEqual ( options , persistentOptions . current ) ) {
5766 persistentOptions . current = options ;
5867 }
@@ -68,14 +77,27 @@ const Dropdown = (props: DropdownProps) => {
6877 ) ;
6978
7079 const sanitizedValues : OptionValue [ ] = useMemo ( ( ) => {
71- if ( value instanceof Array ) {
72- return value ;
80+ if ( val instanceof Array ) {
81+ return val ;
7382 }
74- if ( isNil ( value ) ) {
83+ if ( isNil ( val ) ) {
7584 return [ ] ;
7685 }
77- return [ value ] ;
78- } , [ value ] ) ;
86+ return [ val ] ;
87+ } , [ val ] ) ;
88+
89+ const handleSetProps = useCallback (
90+ ( newValue : DropdownProps [ 'value' ] ) => {
91+ if ( debounce && isOpen ) {
92+ // local only
93+ setVal ( newValue ) ;
94+ } else {
95+ setVal ( newValue ) ;
96+ setProps ( { value : newValue } ) ;
97+ }
98+ } ,
99+ [ debounce , isOpen , setProps ]
100+ ) ;
79101
80102 const updateSelection = useCallback (
81103 ( selection : OptionValue [ ] ) => {
@@ -90,30 +112,28 @@ const Dropdown = (props: DropdownProps) => {
90112 if ( selection . length === 0 ) {
91113 // Empty selection: only allow if clearable is true
92114 if ( clearable ) {
93- setProps ( { value : [ ] } ) ;
115+ handleSetProps ( [ ] ) ;
94116 }
95117 // If clearable is false and trying to set empty, do nothing
96118 // return;
97119 } else {
98- // Non-empty selection: always allowed in multi-select
99- setProps ( { value : selection } ) ;
120+ handleSetProps ( selection ) ;
100121 }
101122 } else {
102123 // For single-select, take the first value or null
103124 if ( selection . length === 0 ) {
104125 // Empty selection: only allow if clearable is true
105126 if ( clearable ) {
106- setProps ( { value : null } ) ;
127+ handleSetProps ( null ) ;
107128 }
108129 // If clearable is false and trying to set empty, do nothing
109130 // return;
110131 } else {
111- // Take the first value for single-select
112- setProps ( { value : selection [ selection . length - 1 ] } ) ;
132+ handleSetProps ( selection [ selection . length - 1 ] ) ;
113133 }
114134 }
115135 } ,
116- [ multi , clearable , closeOnSelect ]
136+ [ multi , clearable , closeOnSelect , handleSetProps ]
117137 ) ;
118138
119139 const onInputChange = useCallback (
@@ -182,8 +202,8 @@ const Dropdown = (props: DropdownProps) => {
182202
183203 const handleClear = useCallback ( ( ) => {
184204 const finalValue : DropdownProps [ 'value' ] = multi ? [ ] : null ;
185- setProps ( { value : finalValue } ) ;
186- } , [ multi ] ) ;
205+ handleSetProps ( finalValue ) ;
206+ } , [ multi , handleSetProps ] ) ;
187207
188208 const handleSelectAll = useCallback ( ( ) => {
189209 if ( multi ) {
@@ -192,12 +212,12 @@ const Dropdown = (props: DropdownProps) => {
192212 . filter ( option => ! sanitizedValues . includes ( option . value ) )
193213 . map ( option => option . value )
194214 ) ;
195- setProps ( { value : allValues } ) ;
215+ handleSetProps ( allValues ) ;
196216 }
197217 if ( closeOnSelect ) {
198218 setIsOpen ( false ) ;
199219 }
200- } , [ multi , displayOptions , sanitizedValues , closeOnSelect ] ) ;
220+ } , [ multi , displayOptions , sanitizedValues , closeOnSelect , handleSetProps ] ) ;
201221
202222 const handleDeselectAll = useCallback ( ( ) => {
203223 if ( multi ) {
@@ -206,12 +226,12 @@ const Dropdown = (props: DropdownProps) => {
206226 displayOption => displayOption . value === option
207227 ) ;
208228 } ) ;
209- setProps ( { value : withDeselected } ) ;
229+ handleSetProps ( withDeselected ) ;
210230 }
211231 if ( closeOnSelect ) {
212232 setIsOpen ( false ) ;
213233 }
214- } , [ multi , displayOptions , sanitizedValues , closeOnSelect ] ) ;
234+ } , [ multi , displayOptions , sanitizedValues , closeOnSelect , handleSetProps ] ) ;
215235
216236 // Sort options when popover opens - selected options first
217237 // Update display options when filtered options or selection changes
@@ -370,11 +390,24 @@ const Dropdown = (props: DropdownProps) => {
370390 setIsOpen ( open ) ;
371391
372392 if ( ! open ) {
373- setProps ( { search_value : undefined } ) ;
374393 pendingSearchRef . current = '' ;
394+ const updates : Partial < DropdownProps > = { } ;
395+
396+ if ( ! isNil ( search_value ) ) {
397+ updates . search_value = undefined ;
398+ }
399+
400+ // Commit debounced value on close only
401+ if ( debounce && ! isEqual ( value , val ) ) {
402+ updates . value = val ;
403+ }
404+
405+ if ( Object . keys ( updates ) . length > 0 ) {
406+ setProps ( updates ) ;
407+ }
375408 }
376409 } ,
377- [ filteredOptions , sanitizedValues ]
410+ [ debounce , value , val , search_value , setProps ]
378411 ) ;
379412
380413 const accessibleId = id ?? uuid ( ) ;
0 commit comments