diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SearchBarLandscapeShowsInlineKeyboardNotExtractMode.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SearchBarLandscapeShowsInlineKeyboardNotExtractMode.png new file mode 100644 index 000000000000..46f2c304a162 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SearchBarLandscapeShowsInlineKeyboardNotExtractMode.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue14708.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue14708.cs new file mode 100644 index 000000000000..0a81586e0665 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue14708.cs @@ -0,0 +1,69 @@ +using Microsoft.Maui.Controls; + +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 14708, "Android SearchBar in landscape shows full-screen IME extract mode", PlatformAffected.Android)] +public class Issue14708 : ContentPage +{ + Label _searchTextLabel; + SearchBar _primarySearchBar; + + public Issue14708() + { + _searchTextLabel = new Label + { + Text = "Search text: (none)", + AutomationId = "SearchTextLabel", + FontSize = 13 + }; + + _primarySearchBar = new SearchBar + { + Text = "Hello, landscape!", + Placeholder = "Tap here in landscape — keyboard should be inline", + AutomationId = "SearchBarControl" + }; + + var searchBar2 = new SearchBar + { + Placeholder = "Second SearchBar", + AutomationId = "SearchBar2" + }; + + + var searchBar3 = new SearchBar + { + Text = "Hello, landscape!", + Placeholder = "Third SearchBar", + AutomationId = "SearchBar3" + }; + + + Content = new ScrollView + { + VerticalOptions = LayoutOptions.Fill, + Content = new VerticalStackLayout + { + Padding = new Thickness(16), + Spacing = 12, + Children = + { + new Label + { + Text = "Rotate to LANDSCAPE, then tap any SearchBar. " + + "The keyboard should appear inline at the bottom — " + + "NOT as a full-screen black overlay.", + HorizontalTextAlignment = TextAlignment.Center + }, + _primarySearchBar, + _searchTextLabel, + new BoxView { HeightRequest = 1, Color = Colors.LightGray }, + new Label { Text = "Additional SearchBars:", FontAttributes = FontAttributes.Italic, FontSize = 13 }, + searchBar2, + searchBar3 + } + } + }; + } + +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue14708.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue14708.cs new file mode 100644 index 000000000000..7f02d242f54a --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue14708.cs @@ -0,0 +1,39 @@ +#if ANDROID || IOS // Orientation changes and IME behavior are only relevant on mobile platforms +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue14708 : _IssuesUITest +{ + public Issue14708(TestDevice device) : base(device) { } + + public override string Issue => "Android SearchBar in landscape shows full-screen IME extract mode"; + + [TearDown] + public void TearDown() + { + App.SetOrientationPortrait(); + } + + [Test] + [Category(UITestCategories.SearchBar)] + public void SearchBarLandscapeShowsInlineKeyboardNotExtractMode() + { + // Rotate to landscape — this is the trigger condition for the bug + App.SetOrientationLandscape(); + + App.WaitForElement("SearchBarControl"); + + // Tap the primary SearchBar to open the keyboard + App.Tap("SearchBarControl"); + + // In the unfixed state, Android enters IME extract mode in landscape: + // a full-screen black overlay replaces the inline keyboard and covers all + // page content. VerifyScreenshot() catches this because the visual output + // is dramatically different from the fixed (inline-keyboard) baseline. + VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2)); + } +} +#endif diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SearchBarLandscapeShowsInlineKeyboardNotExtractMode.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SearchBarLandscapeShowsInlineKeyboardNotExtractMode.png new file mode 100644 index 000000000000..1dfe3140b33c Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SearchBarLandscapeShowsInlineKeyboardNotExtractMode.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SearchBarLandscapeShowsInlineKeyboardNotExtractMode.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SearchBarLandscapeShowsInlineKeyboardNotExtractMode.png new file mode 100644 index 000000000000..de0dcbf49184 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SearchBarLandscapeShowsInlineKeyboardNotExtractMode.png differ diff --git a/src/Core/src/Handlers/SearchBar/SearchBarHandler2.Android.cs b/src/Core/src/Handlers/SearchBar/SearchBarHandler2.Android.cs index ec1ae3644122..cd5d1f31f2ad 100644 --- a/src/Core/src/Handlers/SearchBar/SearchBarHandler2.Android.cs +++ b/src/Core/src/Handlers/SearchBar/SearchBarHandler2.Android.cs @@ -252,7 +252,10 @@ void OnEditorAction(object? sender, TextView.EditorActionEventArgs e) { var actionId = e.ActionId; var evt = e.Event; - ImeAction currentInputImeFlag = PlatformView.EditText.ImeOptions; + // Mask out ImeFlags (e.g., NoFullscreen) so we compare only the action bits. + // Without the mask, ImeOptions may contain 0x02000000 (NoFullscreen) OR'd with + // the action value, causing action comparisons (e.g., ImeAction.Done) to fail. + ImeAction currentInputImeFlag = (ImeAction)((int)PlatformView.EditText.ImeOptions & (int)ImeAction.ImeMaskAction); // On API 34 the issue where actionId is ImeAction.ImeNull when using a hardware keyboard was fixed. // Normalize it here so the rest of the logic is consistent across API levels. diff --git a/src/Core/src/Platform/Android/EditTextExtensions.cs b/src/Core/src/Platform/Android/EditTextExtensions.cs index a81674d035eb..b8f5040b440f 100644 --- a/src/Core/src/Platform/Android/EditTextExtensions.cs +++ b/src/Core/src/Platform/Android/EditTextExtensions.cs @@ -510,5 +510,22 @@ static bool RectContainsMotionEvent(global::Android.Graphics.Rect rect, MotionEv return new global::Android.Graphics.Rect(leftEdge, topEdge, rightEdge, bottomEdge); } } + + /// + /// Ensures is set on the EditText's ImeOptions, + /// preventing the IME from entering full-screen extract mode in landscape orientation. + /// + /// + /// Call this helper after any assignment to editText.ImeOptions inside the + /// SearchBar platform code (MauiSearchView and SearchViewExtensions), or the + /// NoFullscreen flag will be lost and the landscape IME regression (#14708) will + /// silently re-appear. ImeOptions is typed as in the Android + /// binding, but it holds combined ImeAction + ImeFlags bits; NoFullscreen is an + /// ImeFlags value (0x02000000). + /// + internal static void EnsureNoFullscreenFlag(this EditText editText) + { + editText.ImeOptions = (ImeAction)((int)editText.ImeOptions | (int)ImeFlags.NoFullscreen); + } } -} \ No newline at end of file +} diff --git a/src/Core/src/Platform/Android/MauiSearchView.cs b/src/Core/src/Platform/Android/MauiSearchView.cs index c2f868d28d6f..385d5f107bba 100644 --- a/src/Core/src/Platform/Android/MauiSearchView.cs +++ b/src/Core/src/Platform/Android/MauiSearchView.cs @@ -1,5 +1,6 @@ using Android.Content; using Android.Views; +using Android.Views.InputMethods; using Android.Widget; using Java.IO; using SearchView = AndroidX.AppCompat.Widget.SearchView; @@ -30,6 +31,7 @@ void Initialize() if (_queryEditor is not null) { _queryEditor.SaveEnabled = false; + _queryEditor.EnsureNoFullscreenFlag(); } if (_queryEditor?.LayoutParameters is LinearLayout.LayoutParams layoutParams) diff --git a/src/Core/src/Platform/Android/SearchViewExtensions.cs b/src/Core/src/Platform/Android/SearchViewExtensions.cs index 57b42f70a679..dbcc9dce727b 100644 --- a/src/Core/src/Platform/Android/SearchViewExtensions.cs +++ b/src/Core/src/Platform/Android/SearchViewExtensions.cs @@ -238,8 +238,22 @@ public static void UpdateKeyboard(this SearchView searchView, ISearchBar searchB public static void UpdateReturnType(this SearchView searchView, ISearchBar searchBar) { - searchView.SetInputType(searchBar); - searchView.ImeOptions = (int)searchBar.ReturnType.ToPlatform(); + // Resolve the inner EditText once and reuse it to avoid two tree-walk calls. + var editText = searchView.GetFirstChildOfType(); + searchView.SetInputType(searchBar, editText); + // Keep SearchView's own ImeOptions current (including NoFullscreen) so Android + // doesn't re-propagate a stale stored value back to the EditText after + // configuration changes or focus resets. + searchView.ImeOptions = (int)((int)searchBar.ReturnType.ToPlatform() | (int)ImeFlags.NoFullscreen); + // Also set directly on the inner EditText: SearchView.setImeOptions propagates to + // the inner query EditText on most API levels, but does not reliably forward ImeFlags + // (e.g., NoFullscreen) on older APIs. Writing to EditText directly and then calling + // EnsureNoFullscreenFlag guarantees the flag is always present regardless of API level. + if (editText is not null) + { + editText.ImeOptions = searchBar.ReturnType.ToPlatform(); + editText.EnsureNoFullscreenFlag(); + } } internal static void SetInputType(this SearchView searchView, ISearchBar searchBar, EditText? editText = null) @@ -395,10 +409,12 @@ static bool TryGetDefaultStateColor(TextInputLayout textInputLayout, int attribu internal static void UpdateReturnType(this EditText editText, ISearchBar searchBar) { editText.ImeOptions = searchBar.ReturnType.ToPlatform(); + editText.EnsureNoFullscreenFlag(); // Restart the input on the current focused EditText InputMethodManager? imm = (InputMethodManager?)editText.Context?.GetSystemService(Context.InputMethodService); imm?.RestartInput(editText); } + } -} +} \ No newline at end of file