@@ -43,6 +43,9 @@ const Dropdown = (props: DropdownProps) => {
4343 const [ displayOptions , setDisplayOptions ] = useState < DetailedOption [ ] > ( [ ] ) ;
4444 const persistentOptions = useRef < DropdownProps [ 'options' ] > ( [ ] ) ;
4545 const dropdownContainerRef = useRef < HTMLButtonElement > ( null ) ;
46+ const dropdownContentRef = useRef < HTMLDivElement > (
47+ document . createElement ( 'div' )
48+ ) ;
4649
4750 const ctx = window . dash_component_api . useDashContext ( ) ;
4851 const loading = ctx . useLoading ( ) ;
@@ -207,23 +210,46 @@ const Dropdown = (props: DropdownProps) => {
207210 // Update display options when filtered options or selection changes
208211 useEffect ( ( ) => {
209212 if ( isOpen ) {
210- // Sort filtered options: selected first, then unselected
211- const sortedOptions = [ ...filteredOptions ] . sort ( ( a , b ) => {
212- const aSelected = sanitizedValues . includes ( a . value ) ;
213- const bSelected = sanitizedValues . includes ( b . value ) ;
213+ let sortedOptions = filteredOptions ;
214+ if ( multi ) {
215+ // Sort filtered options: selected first, then unselected
216+ sortedOptions = [ ...filteredOptions ] . sort ( ( a , b ) => {
217+ const aSelected = sanitizedValues . includes ( a . value ) ;
218+ const bSelected = sanitizedValues . includes ( b . value ) ;
214219
215- if ( aSelected && ! bSelected ) {
216- return - 1 ;
217- }
218- if ( ! aSelected && bSelected ) {
219- return 1 ;
220- }
221- return 0 ; // Maintain original order within each group
222- } ) ;
220+ if ( aSelected && ! bSelected ) {
221+ return - 1 ;
222+ }
223+ if ( ! aSelected && bSelected ) {
224+ return 1 ;
225+ }
226+ return 0 ; // Maintain original order within each group
227+ } ) ;
228+ }
223229
224230 setDisplayOptions ( sortedOptions ) ;
225231 }
226- } , [ filteredOptions , isOpen ] ) ; // Removed sanitizedValues to prevent re-sorting on selection changes
232+ } , [ filteredOptions , isOpen ] ) ;
233+
234+ // Focus (and scroll) the first selected item when dropdown opens
235+ useEffect ( ( ) => {
236+ if ( ! isOpen || multi || search_value ) {
237+ return ;
238+ }
239+
240+ // waiting for the DOM to be ready after the dropdown renders
241+ requestAnimationFrame ( ( ) => {
242+ const selectedValue = sanitizedValues [ 0 ] ;
243+
244+ const selectedElement = dropdownContentRef . current . querySelector (
245+ `.dash-options-list-option-checkbox[value="${ selectedValue } "]`
246+ ) ;
247+
248+ if ( selectedElement instanceof HTMLElement ) {
249+ selectedElement ?. focus ( ) ;
250+ }
251+ } ) ;
252+ } , [ isOpen , multi , displayOptions , sanitizedValues ] ) ;
227253
228254 // Handle keyboard navigation in popover
229255 const handleKeyDown = useCallback ( ( e : React . KeyboardEvent ) => {
@@ -299,10 +325,16 @@ const Dropdown = (props: DropdownProps) => {
299325
300326 if ( nextIndex > - 1 ) {
301327 focusableElements [ nextIndex ] . focus ( ) ;
302- focusableElements [ nextIndex ] . scrollIntoView ( {
303- behavior : 'auto' ,
304- block : 'center' ,
305- } ) ;
328+ if ( nextIndex === 0 ) {
329+ // first element is a sticky search bar, so if we are focusing
330+ // on that, also move the scroll to the top
331+ dropdownContentRef . current ?. scrollTo ( { top : 0 } ) ;
332+ } else {
333+ focusableElements [ nextIndex ] . scrollIntoView ( {
334+ behavior : 'auto' ,
335+ block : 'center' ,
336+ } ) ;
337+ }
306338 }
307339 } , [ ] ) ;
308340
@@ -311,33 +343,7 @@ const Dropdown = (props: DropdownProps) => {
311343 ( open : boolean ) => {
312344 setIsOpen ( open ) ;
313345
314- if ( open ) {
315- // Sort options: selected first, then unselected
316- const selectedOptions : DetailedOption [ ] = [ ] ;
317- const unselectedOptions : DetailedOption [ ] = [ ] ;
318-
319- // First, collect selected options in the order they appear in the `value` array
320- sanitizedValues . forEach ( value => {
321- const option = filteredOptions . find (
322- opt => opt . value === value
323- ) ;
324- if ( option ) {
325- selectedOptions . push ( option ) ;
326- }
327- } ) ;
328-
329- // Then, collect unselected options in the order they appear in `options` array
330- filteredOptions . forEach ( option => {
331- if ( ! sanitizedValues . includes ( option . value ) ) {
332- unselectedOptions . push ( option ) ;
333- }
334- } ) ;
335- const sortedOptions = [
336- ...selectedOptions ,
337- ...unselectedOptions ,
338- ] ;
339- setDisplayOptions ( sortedOptions ) ;
340- } else {
346+ if ( ! open ) {
341347 setProps ( { search_value : undefined } ) ;
342348 }
343349 } ,
@@ -404,6 +410,7 @@ const Dropdown = (props: DropdownProps) => {
404410
405411 < Popover . Portal container = { dropdownContainerRef . current } >
406412 < Popover . Content
413+ ref = { dropdownContentRef }
407414 className = "dash-dropdown-content"
408415 align = "start"
409416 sideOffset = { 5 }
0 commit comments