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