@@ -5,6 +5,93 @@ defmodule PrimaWeb.ComboboxKeyboardNavigationTest do
55 @ options_container Query . css ( "#demo-combobox [data-prima-ref=options]" )
66 @ all_options Query . css ( "#demo-combobox [role=option]" )
77
8+ feature "navigates options with keyboard arrows" , % { session: session } do
9+ session
10+ |> visit_fixture ( "/fixtures/simple-combobox" , "#demo-combobox" )
11+ |> click ( @ search_input )
12+ |> assert_has ( @ options_container |> Query . visible ( true ) )
13+ |> assert_has ( @ all_options |> Query . count ( 4 ) )
14+ # First option should be focused by default
15+ |> assert_has ( Query . css ( "#demo-combobox [role=option][data-value='Apple'][data-focus=true]" ) )
16+ # Arrow down to next option - use correct key codes and send to the focused element
17+ |> send_keys ( [ :down_arrow ] )
18+ |> assert_has ( Query . css ( "#demo-combobox [role=option][data-value='Pear'][data-focus=true]" ) )
19+ # Arrow down again
20+ |> send_keys ( [ :down_arrow ] )
21+ |> assert_has ( Query . css ( "#demo-combobox [role=option][data-value='Mango'][data-focus=true]" ) )
22+ # Arrow up back to previous
23+ |> send_keys ( [ :up_arrow ] )
24+ |> assert_has ( Query . css ( "#demo-combobox [role=option][data-value='Pear'][data-focus=true]" ) )
25+ end
26+
27+ feature "keyboard navigation wrapping (last to first, first to last)" , % { session: session } do
28+ session
29+ |> visit_fixture ( "/fixtures/simple-combobox" , "#demo-combobox" )
30+ |> click ( @ search_input )
31+ |> assert_has ( @ options_container |> Query . visible ( true ) )
32+ |> assert_has ( @ all_options |> Query . count ( 4 ) )
33+ # First option should be focused by default
34+ |> assert_has ( Query . css ( "#demo-combobox [role=option][data-value='Apple'][data-focus=true]" ) )
35+ # Arrow up from first should wrap to last (Pineapple)
36+ |> send_keys ( [ :up_arrow ] )
37+ |> assert_has (
38+ Query . css ( "#demo-combobox [role=option][data-value='Pineapple'][data-focus=true]" )
39+ )
40+ # Arrow down from last should wrap to first
41+ |> send_keys ( [ :down_arrow ] )
42+ |> assert_has ( Query . css ( "#demo-combobox [role=option][data-value='Apple'][data-focus=true]" ) )
43+ end
44+
45+ feature "selects focused option with Enter key" , % { session: session } do
46+ session
47+ |> visit_fixture ( "/fixtures/simple-combobox" , "#demo-combobox" )
48+ |> click ( @ search_input )
49+ |> assert_has ( @ options_container |> Query . visible ( true ) )
50+ # Navigate to second option
51+ |> send_keys ( [ :down_arrow ] )
52+ |> assert_has ( Query . css ( "#demo-combobox [role=option][data-value='Pear'][data-focus=true]" ) )
53+ # Select with Enter
54+ |> send_keys ( [ :enter ] )
55+ # Options should be hidden after selection
56+ |> assert_has ( @ options_container |> Query . visible ( false ) )
57+ # Check that both inputs have the selected value
58+ |> execute_script (
59+ "const searchVal = document.querySelector('#demo-combobox input[data-prima-ref=search_input]').value; const hiddenInput = document.querySelector('#demo-combobox [data-prima-ref=submit_container] input[type=hidden]'); return {search: searchVal, submit: hiddenInput ? hiddenInput.value : ''}" ,
60+ fn values ->
61+ assert values [ "search" ] == "Pear" ,
62+ "Expected search input value to be 'Pear', got '#{ values [ "search" ] } '"
63+
64+ assert values [ "submit" ] == "Pear" ,
65+ "Expected submit input value to be 'Pear', got '#{ values [ "submit" ] } '"
66+ end
67+ )
68+ end
69+
70+ feature "selects focused option with Tab key" , % { session: session } do
71+ session
72+ |> visit_fixture ( "/fixtures/simple-combobox" , "#demo-combobox" )
73+ |> click ( @ search_input )
74+ |> assert_has ( @ options_container |> Query . visible ( true ) )
75+ # Navigate to third option
76+ |> send_keys ( [ :down_arrow , :down_arrow ] )
77+ |> assert_has ( Query . css ( "#demo-combobox [role=option][data-value='Mango'][data-focus=true]" ) )
78+ # Select with Tab
79+ |> send_keys ( [ :tab ] )
80+ # Options should be hidden after selection
81+ |> assert_has ( @ options_container |> Query . visible ( false ) )
82+ # Check that both inputs have the selected value
83+ |> execute_script (
84+ "const searchVal = document.querySelector('#demo-combobox input[data-prima-ref=search_input]').value; const hiddenInput = document.querySelector('#demo-combobox [data-prima-ref=submit_container] input[type=hidden]'); return {search: searchVal, submit: hiddenInput ? hiddenInput.value : ''}" ,
85+ fn values ->
86+ assert values [ "search" ] == "Mango" ,
87+ "Expected search input value to be 'Mango', got '#{ values [ "search" ] } '"
88+
89+ assert values [ "submit" ] == "Mango" ,
90+ "Expected submit input value to be 'Mango', got '#{ values [ "submit" ] } '"
91+ end
92+ )
93+ end
94+
895 feature "ArrowDown opens combobox when closed and input is focused" , % { session: session } do
996 session
1097 |> visit_fixture ( "/fixtures/simple-combobox" , "#demo-combobox" )
@@ -289,4 +376,88 @@ defmodule PrimaWeb.ComboboxKeyboardNavigationTest do
289376 end
290377 )
291378 end
379+
380+ feature "focused option scrolls into view when using up arrow in overflow container" , % {
381+ session: session
382+ } do
383+ session
384+ |> visit_fixture ( "/fixtures/overflow-combobox" , "#overflow-combobox" )
385+ |> click ( Query . css ( "#overflow-combobox input[data-prima-ref=search_input]" ) )
386+ |> assert_has ( Query . css ( "#overflow-combobox-options" ) |> Query . visible ( true ) )
387+ # First option should be focused by default
388+ |> assert_has (
389+ Query . css ( "#overflow-combobox [role=option][data-value='Item 1'][data-focus=true]" )
390+ )
391+ # Press up arrow to wrap to last option (Item 50)
392+ |> send_keys ( [ :up_arrow ] )
393+ |> assert_has (
394+ Query . css ( "#overflow-combobox [role=option][data-value='Item 50'][data-focus=true]" )
395+ )
396+ # Verify that Item 50 is visible (scrolled into view)
397+ |> execute_script (
398+ """
399+ const option = document.querySelector('#overflow-combobox [role=option][data-value=\" Item 50\" ]');
400+ const container = document.querySelector('#overflow-combobox-options');
401+ const optionRect = option.getBoundingClientRect();
402+ const containerRect = container.getBoundingClientRect();
403+ return {
404+ isVisible: optionRect.top >= containerRect.top && optionRect.bottom <= containerRect.bottom,
405+ optionTop: optionRect.top,
406+ optionBottom: optionRect.bottom,
407+ containerTop: containerRect.top,
408+ containerBottom: containerRect.bottom
409+ };
410+ """ ,
411+ fn result ->
412+ assert result [ "isVisible" ] == true ,
413+ "Expected Item 50 to be scrolled into view. Option top: #{ result [ "optionTop" ] } , bottom: #{ result [ "optionBottom" ] } , Container top: #{ result [ "containerTop" ] } , bottom: #{ result [ "containerBottom" ] } "
414+ end
415+ )
416+ end
417+
418+ feature "scroll position resets when reopening combobox after navigating to bottom" , % {
419+ session: session
420+ } do
421+ session
422+ |> visit_fixture ( "/fixtures/overflow-combobox" , "#overflow-combobox" )
423+ # Open combobox
424+ |> click ( Query . css ( "#overflow-combobox input[data-prima-ref=search_input]" ) )
425+ |> assert_has ( Query . css ( "#overflow-combobox-options" ) |> Query . visible ( true ) )
426+ # Navigate to last option with up arrow
427+ |> send_keys ( [ :up_arrow ] )
428+ |> assert_has (
429+ Query . css ( "#overflow-combobox [role=option][data-value='Item 50'][data-focus=true]" )
430+ )
431+ # Close combobox
432+ |> send_keys ( [ :escape ] )
433+ |> assert_has ( Query . css ( "#overflow-combobox-options" ) |> Query . visible ( false ) )
434+ # Reopen combobox
435+ |> click ( Query . css ( "#overflow-combobox input[data-prima-ref=search_input]" ) )
436+ |> assert_has ( Query . css ( "#overflow-combobox-options" ) |> Query . visible ( true ) )
437+ # First option should be focused
438+ |> assert_has (
439+ Query . css ( "#overflow-combobox [role=option][data-value='Item 1'][data-focus=true]" )
440+ )
441+ # Verify that Item 1 is visible (container scrolled to top)
442+ |> execute_script (
443+ """
444+ const option = document.querySelector('#overflow-combobox [role=option][data-value=\" Item 1\" ]');
445+ const container = document.querySelector('#overflow-combobox-options');
446+ const optionRect = option.getBoundingClientRect();
447+ const containerRect = container.getBoundingClientRect();
448+ return {
449+ isVisible: optionRect.top >= containerRect.top && optionRect.bottom <= containerRect.bottom,
450+ optionTop: optionRect.top,
451+ optionBottom: optionRect.bottom,
452+ containerTop: containerRect.top,
453+ containerBottom: containerRect.bottom,
454+ scrollTop: container.scrollTop
455+ };
456+ """ ,
457+ fn result ->
458+ assert result [ "isVisible" ] == true ,
459+ "Expected Item 1 to be scrolled into view. Option top: #{ result [ "optionTop" ] } , bottom: #{ result [ "optionBottom" ] } , Container top: #{ result [ "containerTop" ] } , bottom: #{ result [ "containerBottom" ] } , scrollTop: #{ result [ "scrollTop" ] } "
460+ end
461+ )
462+ end
292463end
0 commit comments