@@ -83,7 +83,7 @@ def send_keys(key):
8383 dash_duo .start_server (app )
8484
8585 dropdown = dash_duo .find_element ("#dropdown" )
86- dropdown .click ()
86+ dropdown .send_keys ( Keys . ENTER ) # Open with Enter key
8787 dash_duo .wait_for_element (".dash-dropdown-options" )
8888
8989 send_keys (
@@ -237,14 +237,17 @@ def update_output(value):
237237
238238 # Select 3 items by alternating ArrowDown and Spacebar
239239 send_keys (Keys .ARROW_DOWN ) # Move to first option
240+ sleep (0.05 )
240241 send_keys (Keys .SPACE ) # Select Option 0
241242 dash_duo .wait_for_text_to_equal ("#output" , "Selected: ['Option 0']" )
242243
243244 send_keys (Keys .ARROW_DOWN ) # Move to second option
245+ sleep (0.05 )
244246 send_keys (Keys .SPACE ) # Select Option 1
245247 dash_duo .wait_for_text_to_equal ("#output" , "Selected: ['Option 0', 'Option 1']" )
246248
247249 send_keys (Keys .ARROW_DOWN ) # Move to third option
250+ sleep (0.05 )
248251 send_keys (Keys .SPACE ) # Select Option 2
249252 dash_duo .wait_for_text_to_equal (
250253 "#output" , "Selected: ['Option 0', 'Option 1', 'Option 2']"
@@ -253,6 +256,164 @@ def update_output(value):
253256 assert dash_duo .get_logs () == []
254257
255258
259+ def test_a11y007_opens_and_closes_without_races (dash_duo ):
260+ def send_keys (key ):
261+ actions = ActionChains (dash_duo .driver )
262+ actions .send_keys (key )
263+ actions .perform ()
264+
265+ app = Dash (__name__ )
266+ app .layout = Div (
267+ [
268+ Dropdown (
269+ id = "dropdown" ,
270+ options = [f"Option { i } " for i in range (0 , 10 )],
271+ value = "Option 5" ,
272+ multi = False ,
273+ ),
274+ Div (id = "output" ),
275+ ]
276+ )
277+
278+ def assert_focus_in_dropdown ():
279+ # Verify focus is inside the dropdown
280+ assert dash_duo .driver .execute_script (
281+ """
282+ const activeElement = document.activeElement;
283+ const dropdownContent = document.querySelector('.dash-dropdown-content');
284+ return dropdownContent && dropdownContent.contains(activeElement);
285+ """
286+ ), "Focus must be inside the dropdown when it opens"
287+
288+ @app .callback (
289+ Output ("output" , "children" ),
290+ Input ("dropdown" , "value" ),
291+ )
292+ def update_output (value ):
293+ return f"Selected: { value } "
294+
295+ dash_duo .start_server (app )
296+
297+ # Verify initial value is set
298+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: Option 5" )
299+
300+ dropdown = dash_duo .find_element ("#dropdown" )
301+
302+ # Test repeated open/close to confirm no race conditions or side effects
303+ for i in range (3 ):
304+ # Open with Enter
305+ dropdown .send_keys (Keys .ENTER )
306+ dash_duo .wait_for_element (".dash-dropdown-options" )
307+ assert_focus_in_dropdown ()
308+
309+ # Verify the value is still "Option 5" (not cleared)
310+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: Option 5" )
311+
312+ # Close with Escape
313+ send_keys (Keys .ESCAPE )
314+ sleep (0.1 )
315+
316+ # Verify the value is still "Option 5"
317+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: Option 5" )
318+
319+ for i in range (3 ):
320+ # Open with mouse
321+ dropdown .click ()
322+ dash_duo .wait_for_element (".dash-dropdown-options" )
323+ assert_focus_in_dropdown ()
324+
325+ # Verify the value is still "Option 5" (not cleared)
326+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: Option 5" )
327+
328+ # Close with Escape
329+ dropdown .click ()
330+ sleep (0.1 )
331+
332+ # Verify the value is still "Option 5"
333+ dash_duo .wait_for_text_to_equal ("#output" , "Selected: Option 5" )
334+
335+ assert dash_duo .get_logs () == []
336+
337+
338+ def test_a11y008_home_end_pageup_pagedown_navigation (dash_duo ):
339+ def send_keys (key ):
340+ actions = ActionChains (dash_duo .driver )
341+ actions .send_keys (key )
342+ actions .perform ()
343+
344+ def get_focused_option_text ():
345+ return dash_duo .driver .execute_script (
346+ """
347+ const focused = document.activeElement;
348+ if (focused && focused.closest('.dash-options-list-option')) {
349+ return focused.closest('.dash-options-list-option').textContent.trim();
350+ }
351+ return null;
352+ """
353+ )
354+
355+ app = Dash (__name__ )
356+ app .layout = Div (
357+ [
358+ Dropdown (
359+ id = "dropdown" ,
360+ options = [f"Option { i } " for i in range (0 , 50 )],
361+ multi = True ,
362+ ),
363+ ]
364+ )
365+
366+ dash_duo .start_server (app )
367+
368+ dropdown = dash_duo .find_element ("#dropdown" )
369+ dropdown .send_keys (Keys .ENTER ) # Open with Enter key
370+ dash_duo .wait_for_element (".dash-dropdown-options" )
371+
372+ # Navigate from search input to options
373+ send_keys (Keys .ARROW_DOWN ) # Move from search to first option
374+ sleep (0.05 )
375+ send_keys (Keys .ARROW_DOWN ) # Move to second option
376+ sleep (0.05 )
377+ send_keys (Keys .ARROW_DOWN ) # Move to third option
378+ sleep (0.05 )
379+ send_keys (Keys .ARROW_DOWN ) # Move to fourth option
380+ sleep (0.05 )
381+ assert get_focused_option_text () == "Option 3"
382+
383+ send_keys (Keys .HOME ) # Should go back to search input (index 0)
384+ # Verify we're back at search input
385+ assert dash_duo .driver .execute_script (
386+ "return document.activeElement.type === 'search';"
387+ )
388+
389+ # Now arrow down to first option
390+ send_keys (Keys .ARROW_DOWN )
391+ assert get_focused_option_text () == "Option 0"
392+
393+ # Test End key - should go to last option
394+ send_keys (Keys .END )
395+ assert get_focused_option_text () == "Option 49"
396+
397+ # Test PageUp - should jump up by 10
398+ send_keys (Keys .PAGE_UP )
399+ assert get_focused_option_text () == "Option 39"
400+
401+ # Test PageDown - should jump down by 10
402+ send_keys (Keys .PAGE_DOWN )
403+ assert get_focused_option_text () == "Option 49"
404+
405+ # Test PageUp from middle
406+ send_keys (Keys .HOME ) # Back to search input (index 0)
407+ send_keys (Keys .PAGE_DOWN ) # Jump to index 10 (Option 9)
408+ send_keys (Keys .PAGE_DOWN ) # Jump to index 20 (Option 19)
409+ assert get_focused_option_text () == "Option 19"
410+
411+ send_keys (Keys .PAGE_UP ) # Jump to index 10 (Option 9)
412+ assert get_focused_option_text () == "Option 9"
413+
414+ assert dash_duo .get_logs () == []
415+
416+
256417def elements_are_visible (dash_duo , elements ):
257418 # Check if the given elements are within the visible viewport of the dropdown
258419 elements = elements if isinstance (elements , list ) else [elements ]
0 commit comments