From 930833e9651bb1dd0065b5ccb0d8950d5e9623b8 Mon Sep 17 00:00:00 2001 From: tj-devel709 Date: Mon, 17 Jul 2023 15:17:38 -0500 Subject: [PATCH] Squash all the iOS Keyboard Improvements --- .../CoreViews/CorePageView.cs | 4 + .../KeyboardScrollingEditorsPage.xaml | 30 ++++ .../KeyboardScrollingEditorsPage.xaml.cs | 14 ++ .../KeyboardScrollingEntriesPage.xaml | 31 ++++ .../KeyboardScrollingEntriesPage.xaml.cs | 15 ++ .../KeyboardScrollingEntryNextEditorPage.xaml | 12 ++ ...yboardScrollingEntryNextEditorPage.xaml.cs | 14 ++ ...llingNonScrollingPageLargeTitlesGallery.cs | 23 +++ ...llingNonScrollingPageSmallTitlesGallery.cs | 23 +++ ...crollingScrollingPageLargeTitlesGallery.cs | 23 +++ ...crollingScrollingPageSmallTitlesGallery.cs | 23 +++ .../tests/UITests/Tests/KeyboardScrolling.cs | 149 ++++++++++++++++++ ...rollingNonScrollingPageLargeTitlesTests.cs | 52 ++++++ ...rollingNonScrollingPageSmallTitlesTests.cs | 52 ++++++ ...dScrollingScrollingPageLargeTitlesTests.cs | 52 ++++++ ...dScrollingScrollingPageSmallTitlesTests.cs | 52 ++++++ .../Platform/iOS/KeyboardAutoManagerScroll.cs | 132 ++++++++++++++-- 17 files changed, 685 insertions(+), 16 deletions(-) create mode 100644 src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEditorsPage.xaml create mode 100644 src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEditorsPage.xaml.cs create mode 100644 src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntriesPage.xaml create mode 100644 src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntriesPage.xaml.cs create mode 100644 src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntryNextEditorPage.xaml create mode 100644 src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntryNextEditorPage.xaml.cs create mode 100644 src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingNonScrollingPageLargeTitlesGallery.cs create mode 100644 src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingNonScrollingPageSmallTitlesGallery.cs create mode 100644 src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingScrollingPageLargeTitlesGallery.cs create mode 100644 src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingScrollingPageSmallTitlesGallery.cs create mode 100644 src/Controls/tests/UITests/Tests/KeyboardScrolling.cs create mode 100644 src/Controls/tests/UITests/Tests/KeyboardScrollingNonScrollingPageLargeTitlesTests.cs create mode 100644 src/Controls/tests/UITests/Tests/KeyboardScrollingNonScrollingPageSmallTitlesTests.cs create mode 100644 src/Controls/tests/UITests/Tests/KeyboardScrollingScrollingPageLargeTitlesTests.cs create mode 100644 src/Controls/tests/UITests/Tests/KeyboardScrollingScrollingPageSmallTitlesTests.cs diff --git a/src/Controls/samples/Controls.Sample.UITests/CoreViews/CorePageView.cs b/src/Controls/samples/Controls.Sample.UITests/CoreViews/CorePageView.cs index c2230b1eea1d..b42f7b0f7e11 100644 --- a/src/Controls/samples/Controls.Sample.UITests/CoreViews/CorePageView.cs +++ b/src/Controls/samples/Controls.Sample.UITests/CoreViews/CorePageView.cs @@ -50,6 +50,10 @@ public override string ToString() new GalleryPageFactory(() => new EditorCoreGalleryPage(), "Editor Gallery"), new GalleryPageFactory(() => new RadioButtonCoreGalleryPage(), "RadioButton Core Gallery"), new GalleryPageFactory(() => new DragAndDropGallery(), "Drag and Drop Gallery"), + new GalleryPageFactory(() => new KeyboardScrollingScrollingPageSmallTitlesGallery(), "Keyboard Scrolling Gallery - Scrolling Page / Small Titles"), + new GalleryPageFactory(() => new KeyboardScrollingScrollingPageLargeTitlesGallery(), "Keyboard Scrolling Gallery - Scrolling Page / Large Titles"), + new GalleryPageFactory(() => new KeyboardScrollingNonScrollingPageSmallTitlesGallery(), "Keyboard Scrolling Gallery - NonScrolling Page / Small Titles"), + new GalleryPageFactory(() => new KeyboardScrollingNonScrollingPageLargeTitlesGallery(), "Keyboard Scrolling Gallery - NonScrolling Page / Large Titles"), new GalleryPageFactory(() => new LabelCoreGalleryPage(), "Label Gallery"), new GalleryPageFactory(() => new GestureRecognizerGallery(), "Gesture Recognizer Gallery"), new GalleryPageFactory(() => new ScrollViewCoreGalleryPage(), "ScrollView Gallery"), diff --git a/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEditorsPage.xaml b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEditorsPage.xaml new file mode 100644 index 000000000000..6dfe3ab1ff8a --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEditorsPage.xaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEditorsPage.xaml.cs b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEditorsPage.xaml.cs new file mode 100644 index 000000000000..ec13e5cc6fcd --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEditorsPage.xaml.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Xaml; + +namespace Maui.Controls.Sample; + +[XamlCompilation(XamlCompilationOptions.Compile)] +public partial class KeyboardScrollingEditorsPage : ContentView +{ + public KeyboardScrollingEditorsPage() + { + InitializeComponent(); + } +} diff --git a/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntriesPage.xaml b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntriesPage.xaml new file mode 100644 index 000000000000..376a6815de4f --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntriesPage.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntriesPage.xaml.cs b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntriesPage.xaml.cs new file mode 100644 index 000000000000..0b21cbe476e5 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntriesPage.xaml.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.PlatformConfiguration; +using Microsoft.Maui.Controls.Xaml; + +namespace Maui.Controls.Sample; + +[XamlCompilation(XamlCompilationOptions.Compile)] +public partial class KeyboardScrollingEntriesPage : ContentView +{ + public KeyboardScrollingEntriesPage() + { + InitializeComponent(); + } +} diff --git a/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntryNextEditorPage.xaml b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntryNextEditorPage.xaml new file mode 100644 index 000000000000..42c829e6d4a5 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntryNextEditorPage.xaml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntryNextEditorPage.xaml.cs b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntryNextEditorPage.xaml.cs new file mode 100644 index 000000000000..274c84700490 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntryNextEditorPage.xaml.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Xaml; + +namespace Maui.Controls.Sample; + +[XamlCompilation(XamlCompilationOptions.Compile)] +public partial class KeyboardScrollingEntryNextEditorPage : ContentView +{ + public KeyboardScrollingEntryNextEditorPage() + { + InitializeComponent(); + } +} diff --git a/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingNonScrollingPageLargeTitlesGallery.cs b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingNonScrollingPageLargeTitlesGallery.cs new file mode 100644 index 000000000000..8736805d29a6 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingNonScrollingPageLargeTitlesGallery.cs @@ -0,0 +1,23 @@ +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.PlatformConfiguration; +using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; + +namespace Maui.Controls.Sample +{ + [Preserve(AllMembers = true)] + public class KeyboardScrollingNonScrollingPageLargeTitlesGallery : ContentViewGalleryPage + { + public KeyboardScrollingNonScrollingPageLargeTitlesGallery() + { + On().SetLargeTitleDisplay(LargeTitleDisplayMode.Always); + Add(new KeyboardScrollingEntriesPage()); + Add(new KeyboardScrollingEditorsPage()); + Add(new KeyboardScrollingEntryNextEditorPage()); + } + + protected override bool SupportsScroll + { + get { return false; } + } + } +} \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingNonScrollingPageSmallTitlesGallery.cs b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingNonScrollingPageSmallTitlesGallery.cs new file mode 100644 index 000000000000..8de84ebc40f2 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingNonScrollingPageSmallTitlesGallery.cs @@ -0,0 +1,23 @@ +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.PlatformConfiguration; +using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; + +namespace Maui.Controls.Sample +{ + [Preserve(AllMembers = true)] + public class KeyboardScrollingNonScrollingPageSmallTitlesGallery : ContentViewGalleryPage + { + public KeyboardScrollingNonScrollingPageSmallTitlesGallery() + { + On().SetLargeTitleDisplay(LargeTitleDisplayMode.Never); + Add(new KeyboardScrollingEntriesPage()); + Add(new KeyboardScrollingEditorsPage()); + Add(new KeyboardScrollingEntryNextEditorPage()); + } + + protected override bool SupportsScroll + { + get { return false; } + } + } +} \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingScrollingPageLargeTitlesGallery.cs b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingScrollingPageLargeTitlesGallery.cs new file mode 100644 index 000000000000..985f23a6eb91 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingScrollingPageLargeTitlesGallery.cs @@ -0,0 +1,23 @@ +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.PlatformConfiguration; +using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; + +namespace Maui.Controls.Sample +{ + [Preserve(AllMembers = true)] + public class KeyboardScrollingScrollingPageLargeTitlesGallery : ContentViewGalleryPage + { + public KeyboardScrollingScrollingPageLargeTitlesGallery() + { + On().SetLargeTitleDisplay(LargeTitleDisplayMode.Always); + Add(new KeyboardScrollingEntriesPage()); + Add(new KeyboardScrollingEditorsPage()); + Add(new KeyboardScrollingEntryNextEditorPage()); + } + + protected override bool SupportsScroll + { + get { return true; } + } + } +} \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingScrollingPageSmallTitlesGallery.cs b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingScrollingPageSmallTitlesGallery.cs new file mode 100644 index 000000000000..362277e622ea --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingScrollingPageSmallTitlesGallery.cs @@ -0,0 +1,23 @@ +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.PlatformConfiguration; +using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; + +namespace Maui.Controls.Sample +{ + [Preserve(AllMembers = true)] + public class KeyboardScrollingScrollingPageSmallTitlesGallery : ContentViewGalleryPage + { + public KeyboardScrollingScrollingPageSmallTitlesGallery() + { + On().SetLargeTitleDisplay(LargeTitleDisplayMode.Never); + Add(new KeyboardScrollingEntriesPage()); + Add(new KeyboardScrollingEditorsPage()); + Add(new KeyboardScrollingEntryNextEditorPage()); + } + + protected override bool SupportsScroll + { + get { return true; } + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/UITests/Tests/KeyboardScrolling.cs b/src/Controls/tests/UITests/Tests/KeyboardScrolling.cs new file mode 100644 index 000000000000..ff51fae11b10 --- /dev/null +++ b/src/Controls/tests/UITests/Tests/KeyboardScrolling.cs @@ -0,0 +1,149 @@ +using Maui.Controls.Sample; +using Microsoft.Maui.Appium; +using Microsoft.Maui.Controls.Xaml; +using Microsoft.Maui.Graphics; +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Enums; +using TestUtils.Appium.UITests; +using VisualTestUtils; +using Xamarin.UITest; +using Xamarin.UITest.Queries; + +namespace Microsoft.Maui.AppiumTests +{ + internal static class KeyboardScrolling + { + internal static void EntriesScrollingTest(IApp app, IUITestContext? testContext, string galleryName) + { + BeginScrollingTest(app, testContext, galleryName, false); + } + + internal static void EditorsScrollingTest(IApp app, IUITestContext? testContext, string galleryName) + { + BeginScrollingTest(app, testContext, galleryName, true); + } + + static void BeginScrollingTest(IApp app, IUITestContext? testContext, string galleryName, bool isEditor) + { + testContext.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, + "These tests take a while and we are more interested in iOS Scrolling Behavior since it is not out-of-the-box."); + + // Entries 6 - 14 hit a group of interesting areas on scrolling + // depending on the type of iOS device. + for (int i = 6; i <= 14; i++) + { + var shouldContinue = false; + if (isEditor) + shouldContinue = NavigateTest(app, $"Editor{i}", "KeyboardScrollingEditorsPage", isEditor); + else + shouldContinue = NavigateTest(app, $"Entry{i}", "KeyboardScrollingEntriesPage", isEditor); + + app.NavigateToGallery(galleryName); + + if (!shouldContinue) + break; + } + } + + static bool NavigateTest(IApp app, string marked, string pageName, bool isEditor) + { + app.WaitForElement("TargetView"); + app.EnterText("TargetView", pageName); + app.Tap("GoButton"); + app.Tap(marked); + var shouldContinue = CheckIfViewAboveKeyboard(app, marked, isEditor); + if (shouldContinue) + HideKeyboard(app, (app as AppiumUITestApp)?.Driver, isEditor); + app.NavigateBack(); + return shouldContinue; + } + + // will return a bool showing if the view is visible + static bool CheckIfViewAboveKeyboard(IApp app, string marked, bool isEditor) + { + var views = app.WaitForElement(marked); + + // if this view is not on the screen, the keyboard will not be + // showing and we can skip this view + if (!app.IsKeyboardShown()) + return false; + + Assert.IsNotEmpty(views); + var rect = views[0].Rect; + + var testApp = app as AppiumUITestApp; + var keyboardPositionNullable = FindiOSKeyboardLocation(testApp?.Driver); + Assert.NotNull(keyboardPositionNullable); + + var keyboardPosition = (System.Drawing.Point)keyboardPositionNullable!; + if (isEditor) + { + // Until we can get the default Maui accessory view added on the editors, + // let's just use the default height added for the accessory view + var defaultSizeAccessoryView = 44; + keyboardPosition.Y -= defaultSizeAccessoryView; + + } + Assert.Less(rect.CenterY, keyboardPosition.Y); + + return true; + } + + internal static void HideKeyboard(IApp app, AppiumDriver? driver, bool isEditor) + { + if (isEditor) + CloseiOSEditorKeyboard(driver); + else + app.DismissKeyboard(); + } + + internal static System.Drawing.Point? FindiOSKeyboardLocation(AppiumDriver? driver) + { + if (driver?.IsKeyboardShown() == true) + { + var keyboard = driver.FindElement(MobileBy.ClassName("UIAKeyboard")); + return keyboard.Location; + } + return null; + } + + internal static void CloseiOSEditorKeyboard(AppiumDriver? driver) + { + var keyboardDoneButton = driver?.FindElement(MobileBy.Name("Done")); + keyboardDoneButton?.Click(); + } + + internal static void EntryNextEditorScrollingTest(IApp app, IUITestContext? testContext, string galleryName) + { + testContext.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, + "These tests take a while and we are more interested in iOS Scrolling Behavior since it is not out-of-the-box."); + + app.WaitForElement("TargetView"); + app.EnterText("TargetView", "KeyboardScrollingEntryNextEditorPage"); + app.Tap("GoButton"); + + app.WaitForElement("Entry1"); + app.Tap("Entry1"); + CheckIfViewAboveKeyboard(app, "Entry1", false); + NextiOSKeyboardPress((app as AppiumUITestApp)?.Driver); + + CheckIfViewAboveKeyboard(app, "Entry2", false); + NextiOSKeyboardPress((app as AppiumUITestApp)?.Driver); + + CheckIfViewAboveKeyboard(app, "Entry3", false); + NextiOSKeyboardPress((app as AppiumUITestApp)?.Driver); + + CheckIfViewAboveKeyboard(app, "Editor", true); + } + + // Unintentionally types a 'V' but also presses the next keyboard key + internal static void NextiOSKeyboardPress(AppiumDriver? driver) + { + var keyboard = driver?.FindElement(MobileBy.ClassName("UIAKeyboard")); + keyboard?.SendKeys("\n"); + } + } +} + diff --git a/src/Controls/tests/UITests/Tests/KeyboardScrollingNonScrollingPageLargeTitlesTests.cs b/src/Controls/tests/UITests/Tests/KeyboardScrollingNonScrollingPageLargeTitlesTests.cs new file mode 100644 index 000000000000..132584003bde --- /dev/null +++ b/src/Controls/tests/UITests/Tests/KeyboardScrollingNonScrollingPageLargeTitlesTests.cs @@ -0,0 +1,52 @@ +using Maui.Controls.Sample; +using Microsoft.Maui.Appium; +using Microsoft.Maui.Controls.Xaml; +using Microsoft.Maui.Graphics; +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Enums; +using TestUtils.Appium.UITests; +using Xamarin.UITest; + +namespace Microsoft.Maui.AppiumTests +{ + public class KeyboardScrollingNonScrollingPageLargeTitlesTests : UITestBase + { + const string KeyboardScrollingGallery = "* marked:'Keyboard Scrolling Gallery - NonScrolling Page / Large Titles'"; + public KeyboardScrollingNonScrollingPageLargeTitlesTests(TestDevice device) + : base(device) + { + } + + protected override void FixtureSetup() + { + base.FixtureSetup(); + App.NavigateToGallery(KeyboardScrollingGallery); + } + + protected override void FixtureTeardown() + { + base.FixtureTeardown(); + App.NavigateBack(); + } + + [Test] + public void EntriesScrollingPageTest() + { + KeyboardScrolling.EntriesScrollingTest(App, UITestContext, KeyboardScrollingGallery); + } + + [Test] + public void EditorsScrollingPageTest() + { + KeyboardScrolling.EditorsScrollingTest(App, UITestContext, KeyboardScrollingGallery); + } + + [Test] + public void EntryNextEditorTest() + { + KeyboardScrolling.EntryNextEditorScrollingTest(App, UITestContext, KeyboardScrollingGallery); + } + } +} diff --git a/src/Controls/tests/UITests/Tests/KeyboardScrollingNonScrollingPageSmallTitlesTests.cs b/src/Controls/tests/UITests/Tests/KeyboardScrollingNonScrollingPageSmallTitlesTests.cs new file mode 100644 index 000000000000..8dda4368c632 --- /dev/null +++ b/src/Controls/tests/UITests/Tests/KeyboardScrollingNonScrollingPageSmallTitlesTests.cs @@ -0,0 +1,52 @@ +using Maui.Controls.Sample; +using Microsoft.Maui.Appium; +using Microsoft.Maui.Controls.Xaml; +using Microsoft.Maui.Graphics; +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Enums; +using TestUtils.Appium.UITests; +using Xamarin.UITest; + +namespace Microsoft.Maui.AppiumTests +{ + public class KeyboardScrollingNonScrollingPageSmallTitlesTests : UITestBase + { + const string KeyboardScrollingGallery = "* marked:'Keyboard Scrolling Gallery - NonScrolling Page / Small Titles'"; + public KeyboardScrollingNonScrollingPageSmallTitlesTests(TestDevice device) + : base(device) + { + } + + protected override void FixtureSetup() + { + base.FixtureSetup(); + App.NavigateToGallery(KeyboardScrollingGallery); + } + + protected override void FixtureTeardown() + { + base.FixtureTeardown(); + App.NavigateBack(); + } + + [Test] + public void EntriesScrollingPageTest() + { + KeyboardScrolling.EntriesScrollingTest(App, UITestContext, KeyboardScrollingGallery); + } + + [Test] + public void EditorsScrollingPageTest() + { + KeyboardScrolling.EditorsScrollingTest(App, UITestContext, KeyboardScrollingGallery); + } + + [Test] + public void EntryNextEditorTest() + { + KeyboardScrolling.EntryNextEditorScrollingTest(App, UITestContext, KeyboardScrollingGallery); + } + } +} diff --git a/src/Controls/tests/UITests/Tests/KeyboardScrollingScrollingPageLargeTitlesTests.cs b/src/Controls/tests/UITests/Tests/KeyboardScrollingScrollingPageLargeTitlesTests.cs new file mode 100644 index 000000000000..58a42fbdb6a2 --- /dev/null +++ b/src/Controls/tests/UITests/Tests/KeyboardScrollingScrollingPageLargeTitlesTests.cs @@ -0,0 +1,52 @@ +using Maui.Controls.Sample; +using Microsoft.Maui.Appium; +using Microsoft.Maui.Controls.Xaml; +using Microsoft.Maui.Graphics; +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Enums; +using TestUtils.Appium.UITests; +using Xamarin.UITest; + +namespace Microsoft.Maui.AppiumTests +{ + public class KeyboardScrollingScrollingPageLargeTitlesTests : UITestBase + { + const string KeyboardScrollingGallery = "* marked:'Keyboard Scrolling Gallery - Scrolling Page / Large Titles'"; + public KeyboardScrollingScrollingPageLargeTitlesTests(TestDevice device) + : base(device) + { + } + + protected override void FixtureSetup() + { + base.FixtureSetup(); + App.NavigateToGallery(KeyboardScrollingGallery); + } + + protected override void FixtureTeardown() + { + base.FixtureTeardown(); + App.NavigateBack(); + } + + [Test] + public void EntriesScrollingPageTest() + { + KeyboardScrolling.EntriesScrollingTest(App, UITestContext, KeyboardScrollingGallery); + } + + [Test] + public void EditorsScrollingPageTest() + { + KeyboardScrolling.EditorsScrollingTest(App, UITestContext, KeyboardScrollingGallery); + } + + [Test] + public void EntryNextEditorTest() + { + KeyboardScrolling.EntryNextEditorScrollingTest(App, UITestContext, KeyboardScrollingGallery); + } + } +} diff --git a/src/Controls/tests/UITests/Tests/KeyboardScrollingScrollingPageSmallTitlesTests.cs b/src/Controls/tests/UITests/Tests/KeyboardScrollingScrollingPageSmallTitlesTests.cs new file mode 100644 index 000000000000..0398c98a514d --- /dev/null +++ b/src/Controls/tests/UITests/Tests/KeyboardScrollingScrollingPageSmallTitlesTests.cs @@ -0,0 +1,52 @@ +using Maui.Controls.Sample; +using Microsoft.Maui.Appium; +using Microsoft.Maui.Controls.Xaml; +using Microsoft.Maui.Graphics; +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Enums; +using TestUtils.Appium.UITests; +using Xamarin.UITest; + +namespace Microsoft.Maui.AppiumTests +{ + public class KeyboardScrollingScrollingPageSmallTitlesTests : UITestBase + { + const string KeyboardScrollingGallery = "* marked:'Keyboard Scrolling Gallery - Scrolling Page / Small Titles'"; + public KeyboardScrollingScrollingPageSmallTitlesTests(TestDevice device) + : base(device) + { + } + + protected override void FixtureSetup() + { + base.FixtureSetup(); + App.NavigateToGallery(KeyboardScrollingGallery); + } + + protected override void FixtureTeardown() + { + base.FixtureTeardown(); + App.NavigateBack(); + } + + [Test] + public void EntriesScrollingPageTest() + { + KeyboardScrolling.EntriesScrollingTest(App, UITestContext, KeyboardScrollingGallery); + } + + [Test] + public void EditorsScrollingPageTest() + { + KeyboardScrolling.EditorsScrollingTest(App, UITestContext, KeyboardScrollingGallery); + } + + [Test] + public void EntryNextEditorTest() + { + KeyboardScrolling.EntryNextEditorScrollingTest(App, UITestContext, KeyboardScrollingGallery); + } + } +} diff --git a/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs b/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs index 69d484258830..9734ca0c5190 100644 --- a/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs +++ b/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs @@ -29,6 +29,7 @@ public static class KeyboardAutoManagerScroll static double AnimationDuration = 0.25; static UIView? View = null; static UIView? ContainerView = null; + static CGRect? StartingContainerViewFrame = null; static CGRect? CursorRect = null; internal static bool IsKeyboardShowing = false; static int TextViewTopDistance = 20; @@ -104,14 +105,10 @@ static async void DidUITextBeginEditing(NSNotification notification) ContainerView = View.GetContainerView(); - // the cursor needs a small amount of time to update the position - await Task.Delay(5); - - var localCursor = FindLocalCursorPosition(); - if (localCursor is CGRect local) - CursorRect = View.ConvertRectToView(local, null); - - TextViewTopDistance = ((int?)localCursor?.Height ?? 0) + 20; + // Grab the starting position of the ContainerView so we can track if + // there is any external scrolling going on + if (ContainerView is not null) + StartingContainerViewFrame = ContainerView.ConvertRectToView(ContainerView.Bounds, null); await AdjustPositionDebounce(); } @@ -257,7 +254,32 @@ internal static async Task AdjustPositionDebounce() var entranceCount = DebounceCount; - await Task.Delay(10); + // If we are going to a new view that has an InputAccessoryView + // while we have the keyboard up, we need a delay to recalculate + // the height of the InputAccessoryView + if (IsKeyboardShowing && View?.InputAccessoryView is not null) + await Task.Delay(10); + + // With Maui Community Toolkit Popup, for example, the popup viewcontroller + // uses UIKit.UIModalPresentationStyle.Popover with other customizations + // that cause the viewcontroller to translate in the postive y-axis. + // This translation happens at the same time that the Entry and Editors + // are focused and our keyboard scrolling begins. Due to this, we are adding + // to the delay so that the translation on the y-axis of the viewcontroller can + // occur prior to our calculations for scrolling. + var vc = View?.FindResponder(); + if (vc?.ActivePresentationController?.PresentationStyle == UIModalPresentationStyle.Popover) + { + await Task.Delay(30); + + var currentContainerViewFrame = ContainerView?.ConvertRectToView(ContainerView.Bounds, null); + while (currentContainerViewFrame != StartingContainerViewFrame) + { + StartingContainerViewFrame = currentContainerViewFrame; + await Task.Delay(5); + currentContainerViewFrame = ContainerView?.ConvertRectToView(ContainerView.Bounds, null); + } + } if (entranceCount == DebounceCount) AdjustPosition(); @@ -267,7 +289,6 @@ internal static async Task AdjustPositionDebounce() internal static void AdjustPosition() { if (ContainerView is null - || CursorRect is null || (View is not UITextField && View is not UITextView)) { IsKeyboardAutoScrollHandling = false; @@ -302,10 +323,25 @@ internal static void AdjustPosition() var topLayoutGuide = Math.Max(navigationBarAreaHeight, ContainerView.LayoutMargins.Top) + 5; - var keyboardYPosition = window.Frame.Height - kbSize.Height - TextViewTopDistance; + // calculate the cursor rect + var localCursor = FindLocalCursorPosition(); + if (localCursor is CGRect local) + CursorRect = View.ConvertRectToView(local, null); + + if (CursorRect is null) + { + IsKeyboardAutoScrollHandling = false; + return; + } var viewRectInWindow = View.ConvertRectToView(View.Bounds, window); + // give a small offset of 20 plus the cursor.Height for the distance + // between the selected text and the keyboard + TextViewTopDistance = ((int?)localCursor?.Height ?? 0) + 20; + + var keyboardYPosition = window.Frame.Height - kbSize.Height - TextViewTopDistance; + // readjust contentInset when the textView height is too large for the screen var rootSuperViewFrameInWindow = window.Frame; if (ContainerView.Superview is UIView v) @@ -318,14 +354,18 @@ internal static void AdjustPosition() bool cursorTooHigh = false; bool cursorTooLow = false; - if (cursorRect.Y >= viewRectInWindow.GetMaxY()) + // scenario where we go into an editor with the "Next" keyboard button, + // but the cursor location on the editor is scrolled below the visible section + if (View is UITextView && cursorRect.Y >= viewRectInWindow.GetMaxY()) { cursorNotInViewScroll = viewRectInWindow.GetMaxY() - cursorRect.GetMaxY(); move = cursorRect.Y - keyboardYPosition + cursorNotInViewScroll; cursorTooLow = true; } - else if (cursorRect.Y < viewRectInWindow.GetMinY()) + // scenario where we go into an editor with the "Next" keyboard button, + // but the cursor location on the editor is scrolled above the visible section + else if (View is UITextView && cursorRect.Y < viewRectInWindow.GetMinY()) { cursorNotInViewScroll = viewRectInWindow.GetMinY() - cursorRect.Y; move = cursorRect.Y - keyboardYPosition + cursorNotInViewScroll; @@ -401,7 +441,7 @@ internal static void AdjustPosition() { shouldContinue = superScrollView.ContentOffset.Y > 0; - if (shouldContinue && View.FindResponder() is UITableViewCell tableCell + if (shouldContinue && View?.FindResponder() is UITableViewCell tableCell && tableView.IndexPathForCell(tableCell) is NSIndexPath indexPath && tableView.GetPreviousIndexPath(indexPath) is NSIndexPath previousIndexPath) { @@ -418,7 +458,7 @@ internal static void AdjustPosition() { shouldContinue = superScrollView.ContentOffset.Y > 0; - if (shouldContinue && View.FindResponder() is UICollectionViewCell collectionCell + if (shouldContinue && View?.FindResponder() is UICollectionViewCell collectionCell && collectionView.IndexPathForCell(collectionCell) is NSIndexPath indexPath && collectionView.GetPreviousIndexPath(indexPath) is NSIndexPath previousIndexPath && collectionView.GetLayoutAttributesForItem(previousIndexPath) is UICollectionViewLayoutAttributes attributes) @@ -451,7 +491,16 @@ internal static void AdjustPosition() var tempScrollView = superScrollView.FindResponder(); var nextScrollView = FindParentScroll(tempScrollView); + // if PrefersLargeTitles is true, we may need additional logic to + // handle the collapsable navbar + var navController = View?.GetNavigationController(); + var prefersLargeTitles = navController?.NavigationBar.PrefersLargeTitles ?? false; + if (prefersLargeTitles) + move = AdjustForLargeTitles(move, superScrollView, navController!); + + var origContentOffsetY = superScrollView.ContentOffset.Y; var shouldOffsetY = superScrollView.ContentOffset.Y - Math.Min(superScrollView.ContentOffset.Y, -move); + var requestedMove = move; // the contentOffset.Y will change to shouldOffSetY so we can subtract the difference from the move move -= (nfloat)(shouldOffsetY - superScrollView.ContentOffset.Y); @@ -471,11 +520,32 @@ internal static void AdjustPosition() innerScrollValue = 0; ScrolledView = superScrollView; - if (View.FindResponder() is not null) + if (View?.FindResponder() is not null) superScrollView.SetContentOffset(newContentOffset, UIView.AnimationsEnabled); else superScrollView.ContentOffset = newContentOffset; }, () => { }); + + // after this scroll finishes, there is an edge case where if we have Large Titles, + // the entire requeseted scroll amount may not be allowed. If so, we need to scroll + // the rest of the distance afterwards + var actualScrolledAmount = superScrollView.ContentOffset.Y - origContentOffsetY; + var amountNotScrolled = requestedMove - actualScrolledAmount; + + if (prefersLargeTitles && amountNotScrolled != 0) + { + UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () => + { + newContentOffset.Y += (nfloat)amountNotScrolled; + innerScrollValue = 0; + ScrolledView = superScrollView; + + if (View?.FindResponder() is not null) + superScrollView.SetContentOffset(newContentOffset, UIView.AnimationsEnabled); + else + superScrollView.ContentOffset = newContentOffset; + }, () => { }); + } } else @@ -608,6 +678,35 @@ internal static nfloat FindKeyboardHeight() return window.Frame.Height - kbSize.Height; } + // In the case we have PrefersLargeTitles set to true, the UINavigationBar + // has additional height that collapses when scrolling. Try to remove + // this collapsable height difference from the calculated move distance. + static nfloat AdjustForLargeTitles(nfloat move, UIScrollView superScrollView, UINavigationController navController) + { + var navBarCollapsedHeight = 44; + var minPageMoveToCollapseNavBar = 52; + var amountScrolled = superScrollView.ContentOffset.Y; + var amountLeftToCollapseNavBar = minPageMoveToCollapseNavBar - amountScrolled; + var navBarCollapseDifference = navController.NavigationBar.Frame.Height - navBarCollapsedHeight; + + // if the navbar will collapse from our scroll + if (move >= amountLeftToCollapseNavBar) + { + // if subtracting navBarCollapseDifference from our scroll + // will cause the collapse not to happen, we need to scroll + // to the minimum amount that will cause the collapse or else + // we will not see our view + if (move - navBarCollapseDifference < amountLeftToCollapseNavBar) + return amountLeftToCollapseNavBar; + + // else the navBar will collapse and we want to subtract + // the navBarCollapseDifference to account for it + else + return move - navBarCollapseDifference; + } + return move; + } + static void RestorePosition() { if (ContainerView is not null @@ -629,6 +728,7 @@ static void RestorePosition() ContainerView = null; TopViewBeginOrigin = InvalidPoint; CursorRect = null; + StartingContainerViewFrame = null; } static NSIndexPath? GetPreviousIndexPath(this UIScrollView scrollView, NSIndexPath indexPath)