@@ -139,6 +139,63 @@ def send_keys(key):
139139 assert dash_duo .get_logs () == []
140140
141141
142+ def test_a11y003b_keyboard_navigation_not_searchable (dash_duo ):
143+ def send_keys (key ):
144+ actions = ActionChains (dash_duo .driver )
145+ actions .send_keys (key )
146+ actions .perform ()
147+
148+ app = Dash (__name__ )
149+ app .layout = Div (
150+ [
151+ Dropdown (
152+ id = "dropdown" ,
153+ options = [i for i in range (0 , 100 )],
154+ multi = True ,
155+ searchable = False ,
156+ placeholder = "Testing keyboard navigation without search" ,
157+ ),
158+ ],
159+ )
160+
161+ dash_duo .start_server (app )
162+
163+ dropdown = dash_duo .find_element ("#dropdown" )
164+ dropdown .send_keys (Keys .ENTER ) # Open with Enter key
165+ dash_duo .wait_for_element (".dash-dropdown-options" )
166+
167+ send_keys (Keys .ESCAPE )
168+ with pytest .raises (TimeoutException ):
169+ dash_duo .wait_for_element (".dash-dropdown-options" , timeout = 0.25 )
170+
171+ send_keys (Keys .ARROW_DOWN ) # Expecting the dropdown to open up
172+ dash_duo .wait_for_element (".dash-dropdown-options" )
173+
174+ send_keys (Keys .SPACE ) # Expecting to be selecting the focused first option
175+ value_items = dash_duo .find_elements (".dash-dropdown-value-item" )
176+ assert len (value_items ) == 1
177+ assert value_items [0 ].text == "0"
178+
179+ send_keys (Keys .ARROW_DOWN )
180+ send_keys (Keys .SPACE )
181+ value_items = dash_duo .find_elements (".dash-dropdown-value-item" )
182+ assert len (value_items ) == 2
183+ assert [item .text for item in value_items ] == ["0" , "1" ]
184+
185+ send_keys (Keys .SPACE ) # Expecting to be de-selecting
186+ value_items = dash_duo .find_elements (".dash-dropdown-value-item" )
187+ assert len (value_items ) == 1
188+ assert value_items [0 ].text == "0"
189+
190+ send_keys (Keys .ESCAPE )
191+ sleep (0.25 )
192+ value_items = dash_duo .find_elements (".dash-dropdown-value-item" )
193+ assert len (value_items ) == 1
194+ assert value_items [0 ].text == "0"
195+
196+ assert dash_duo .get_logs () == []
197+
198+
142199def test_a11y004_selection_visibility_single (dash_duo ):
143200 app = Dash (__name__ )
144201 app .layout = (
@@ -414,6 +471,230 @@ def get_focused_option_text():
414471 assert dash_duo .get_logs () == []
415472
416473
474+ def test_a11y009_enter_on_search_selects_first_option_multi (dash_duo ):
475+ def send_keys (key ):
476+ actions = ActionChains (dash_duo .driver )
477+ actions .send_keys (key )
478+ actions .perform ()
479+
480+ app = Dash (__name__ )
481+ app .layout = Div (
482+ [
483+ Dropdown (
484+ id = "dropdown" ,
485+ options = ["Apple" , "Banana" , "Cherry" ],
486+ multi = True ,
487+ searchable = True ,
488+ ),
489+ Div (id = "output" ),
490+ ]
491+ )
492+
493+ @app .callback (Output ("output" , "children" ), Input ("dropdown" , "value" ))
494+ def update_output (value ):
495+ return f"Selected: { value } "
496+
497+ dash_duo .start_server (app )
498+
499+ dropdown = dash_duo .find_element ("#dropdown" )
500+ dropdown .click ()
501+ dash_duo .wait_for_element (".dash-dropdown-search" )
502+
503+ # Type to filter, then Enter selects the first visible option
504+ send_keys ("a" )
505+ sleep (0.1 )
506+ send_keys (Keys .ENTER )
507+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: ['Apple']" )
508+ assert dash_duo .driver .execute_script (
509+ "return document.activeElement.type === 'search';"
510+ ), "Focus should remain on the search input after Enter"
511+
512+ # Enter again deselects it
513+ send_keys (Keys .ENTER )
514+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: []" )
515+ assert dash_duo .driver .execute_script (
516+ "return document.activeElement.type === 'search';"
517+ ), "Focus should remain on the search input after deselect"
518+
519+ # Filtering to a different option selects that one
520+ send_keys (Keys .BACKSPACE )
521+ send_keys ("b" )
522+ sleep (0.1 )
523+ send_keys (Keys .ENTER )
524+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: ['Banana']" )
525+
526+ assert dash_duo .get_logs () == []
527+
528+
529+ def test_a11y010_enter_on_search_selects_first_option_single (dash_duo ):
530+ def send_keys (key ):
531+ actions = ActionChains (dash_duo .driver )
532+ actions .send_keys (key )
533+ actions .perform ()
534+
535+ app = Dash (__name__ )
536+ app .layout = Div (
537+ [
538+ Dropdown (
539+ id = "dropdown" ,
540+ options = ["Apple" , "Banana" , "Cherry" ],
541+ multi = False ,
542+ searchable = True ,
543+ ),
544+ Div (id = "output" ),
545+ ]
546+ )
547+
548+ @app .callback (Output ("output" , "children" ), Input ("dropdown" , "value" ))
549+ def update_output (value ):
550+ return f"Selected: { value } "
551+
552+ dash_duo .start_server (app )
553+
554+ dropdown = dash_duo .find_element ("#dropdown" )
555+ dropdown .click ()
556+ dash_duo .wait_for_element (".dash-dropdown-search" )
557+
558+ send_keys ("a" )
559+ sleep (0.1 )
560+ send_keys (Keys .ENTER )
561+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: Apple" )
562+
563+ assert dash_duo .get_logs () == []
564+
565+
566+ def test_a11y011_enter_on_search_no_deselect_when_not_clearable (dash_duo ):
567+ def send_keys (key ):
568+ actions = ActionChains (dash_duo .driver )
569+ actions .send_keys (key )
570+ actions .perform ()
571+
572+ app = Dash (__name__ )
573+ app .layout = Div (
574+ [
575+ Dropdown (
576+ id = "dropdown" ,
577+ options = ["Apple" , "Banana" , "Cherry" ],
578+ value = "Apple" ,
579+ multi = False ,
580+ searchable = True ,
581+ clearable = False ,
582+ ),
583+ Div (id = "output" ),
584+ ]
585+ )
586+
587+ @app .callback (Output ("output" , "children" ), Input ("dropdown" , "value" ))
588+ def update_output (value ):
589+ return f"Selected: { value } "
590+
591+ dash_duo .start_server (app )
592+
593+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: Apple" )
594+
595+ dropdown = dash_duo .find_element ("#dropdown" )
596+ dropdown .click ()
597+ dash_duo .wait_for_element (".dash-dropdown-search" )
598+
599+ # Apple is the first option and already selected; Enter should not deselect it
600+ send_keys (Keys .ENTER )
601+ sleep (0.1 )
602+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: Apple" )
603+
604+ assert dash_duo .get_logs () == []
605+
606+
607+ def test_a11y012_typing_on_trigger_opens_dropdown_with_search (dash_duo ):
608+ app = Dash (__name__ )
609+ app .layout = Div (
610+ [
611+ Dropdown (
612+ id = "dropdown" ,
613+ options = ["Apple" , "Banana" , "Cherry" ],
614+ searchable = True ,
615+ ),
616+ Div (id = "output" ),
617+ ]
618+ )
619+
620+ @app .callback (Output ("output" , "children" ), Input ("dropdown" , "search_value" ))
621+ def update_output (search_value ):
622+ return f"Search: { search_value } "
623+
624+ dash_duo .start_server (app )
625+
626+ dropdown = dash_duo .find_element ("#dropdown" )
627+ dropdown .send_keys ("b" )
628+
629+ dash_duo .wait_for_element (".dash-dropdown-search" )
630+ dash_duo .wait_for_text_to_equal ("#output" , "Search: b" )
631+
632+ # Only Banana should be visible
633+ options = dash_duo .find_elements (".dash-dropdown-option" )
634+ assert len (options ) == 1
635+ assert options [0 ].text == "Banana"
636+
637+ # Focus should be on the search input
638+ assert dash_duo .driver .execute_script (
639+ "return document.activeElement.type === 'search';"
640+ ), "Focus should be on the search input after typing on the trigger"
641+
642+ assert dash_duo .get_logs () == []
643+
644+
645+ def test_a11y013_enter_on_search_after_reopen_selects_correctly (dash_duo ):
646+ def send_keys (key ):
647+ actions = ActionChains (dash_duo .driver )
648+ actions .send_keys (key )
649+ actions .perform ()
650+
651+ app = Dash (__name__ )
652+ app .layout = Div (
653+ [
654+ Dropdown (
655+ id = "dropdown" ,
656+ options = ["Cambodia" , "Cameroon" , "Canada" ],
657+ multi = False ,
658+ searchable = True ,
659+ ),
660+ Div (id = "output" ),
661+ ]
662+ )
663+
664+ @app .callback (Output ("output" , "children" ), Input ("dropdown" , "value" ))
665+ def update_output (value ):
666+ return f"Selected: { value } "
667+
668+ dash_duo .start_server (app )
669+
670+ dropdown = dash_duo .find_element ("#dropdown" )
671+ dropdown .send_keys ("c" )
672+ dash_duo .wait_for_element (".dash-dropdown-search" )
673+ sleep (0.1 )
674+
675+ # Enter selects Cambodia (first result)
676+ send_keys (Keys .ENTER )
677+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: Cambodia" )
678+
679+ # Type "can" — should filter to only Canada
680+ send_keys ("can" )
681+ sleep (0.1 )
682+ options = dash_duo .find_elements (".dash-dropdown-option" )
683+ assert len (options ) == 1
684+ assert options [0 ].text == "Canada"
685+
686+ # Focus should still be on the search input, not the selected option
687+ assert dash_duo .driver .execute_script (
688+ "return document.activeElement.type === 'search';"
689+ ), "Focus should remain on the search input while typing"
690+
691+ # Enter selects Canada
692+ send_keys (Keys .ENTER )
693+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: Canada" )
694+
695+ assert dash_duo .get_logs () == []
696+
697+
417698def elements_are_visible (dash_duo , elements ):
418699 # Check if the given elements are within the visible viewport of the dropdown
419700 elements = elements if isinstance (elements , list ) else [elements ]
0 commit comments