diff --git a/src/Controls/samples/Controls.Sample.UITests/CoreViews/CorePageView.cs b/src/Controls/samples/Controls.Sample.UITests/CoreViews/CorePageView.cs index ac7f99f66a17..fc4b37e9608f 100644 --- a/src/Controls/samples/Controls.Sample.UITests/CoreViews/CorePageView.cs +++ b/src/Controls/samples/Controls.Sample.UITests/CoreViews/CorePageView.cs @@ -61,7 +61,12 @@ public override string ToString() new GalleryPageFactory(() => new FrameCoreGalleryPage(), "Frame Gallery"), new GalleryPageFactory(() => new ImageButtonCoreGalleryPage(), "Image Button Gallery"), new GalleryPageFactory(() => new ImageCoreGalleryPage(), "Image Gallery"), - new GalleryPageFactory(() => new LabelCoreGalleryPage(), "Label Gallery"), + new GalleryPageFactory(() => new KeyboardScrollingGridGallery(), "Keyboard Scrolling Gallery - Grid with Star Row"), + new GalleryPageFactory(() => new KeyboardScrollingNonScrollingPageLargeTitlesGallery(), "Keyboard Scrolling Gallery - NonScrolling Page / Large Titles"), + new GalleryPageFactory(() => new KeyboardScrollingNonScrollingPageSmallTitlesGallery(), "Keyboard Scrolling Gallery - NonScrolling Page / Small Titles"), + new GalleryPageFactory(() => new KeyboardScrollingScrollingPageLargeTitlesGallery(), "Keyboard Scrolling Gallery - Scrolling Page / Large Titles"), + new GalleryPageFactory(() => new KeyboardScrollingScrollingPageSmallTitlesGallery(), "Keyboard Scrolling Gallery - Scrolling Page / Small Titles"), + new GalleryPageFactory(() => new LabelCoreGalleryPage(), "Label Gallery"), new GalleryPageFactory(() => new ListViewCoreGalleryPage(), "ListView Gallery"), new GalleryPageFactory(() => new PickerCoreGalleryPage(), "Picker Gallery"), new GalleryPageFactory(() => new ProgressBarCoreGalleryPage(), "Progress Bar 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..ebab00e0e331 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEditorsPage.xaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..18e82d95659a --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntriesPage.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..5f281bce968e --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingEntryNextEditorPage.xaml @@ -0,0 +1,12 @@ + + + + + + + + + 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/KeyboardScrollingGridGallery.cs b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingGridGallery.cs new file mode 100644 index 000000000000..44c69f6c1a83 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingGridGallery.cs @@ -0,0 +1,15 @@ +using Microsoft.Maui.Controls; +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 KeyboardScrollingGridGallery : ContentViewGalleryPage +{ + public KeyboardScrollingGridGallery() + { + Content = new KeyboardScrollingGridPage(); + } +} diff --git a/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingGridPage.xaml b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingGridPage.xaml new file mode 100644 index 000000000000..c62e0538368e --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingGridPage.xaml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingGridPage.xaml.cs b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingGridPage.xaml.cs new file mode 100644 index 000000000000..cc1bb22864c4 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Elements/KeyboardScrollingGridPage.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 KeyboardScrollingGridPage : ContentView +{ + public KeyboardScrollingGridPage() + { + 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..8cd3082cb329 --- /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; } + } + } +} 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..a8cf29e684a8 --- /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; } + } + } +} 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..8d64edee6338 --- /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; } + } + } +} 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..83c3ebb0a00f --- /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; } + } + } +} diff --git a/src/Controls/tests/UITests/Tests/KeyboardScrolling.cs b/src/Controls/tests/UITests/Tests/KeyboardScrolling.cs new file mode 100644 index 000000000000..88f31c216426 --- /dev/null +++ b/src/Controls/tests/UITests/Tests/KeyboardScrolling.cs @@ -0,0 +1,158 @@ +using NUnit.Framework; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.MultiTouch; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.AppiumTests +{ + internal static class KeyboardScrolling + { + internal static readonly string IgnoreMessage = "These tests take a while and we are more interested in iOS Scrolling Behavior since it is not out-of-the-box."; + + internal static void EntriesScrollingTest(IApp app, string galleryName) + { + RunScrollingTest(app, galleryName, false); + } + + internal static void EditorsScrollingTest(IApp app, string galleryName) + { + RunScrollingTest(app, galleryName, true); + } + + static void RunScrollingTest(IApp app, string galleryName, bool isEditor) + { + var App = (app as AppiumApp); + if (App is null) + return; + + app.WaitForElement("TargetView"); + if (isEditor) + app.EnterText("TargetView", "KeyboardScrollingEditorsPage"); + else + app.EnterText("TargetView", "KeyboardScrollingEntriesPage"); + + app.Click("GoButton"); + + // 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 didReachEndofPage = false; + if (isEditor) + ClickText(app, $"Editor{i}", isEditor, out didReachEndofPage); + else + ClickText(app, $"Entry{i}", isEditor, out didReachEndofPage); + + // Scroll to the top of the page + var actions = new TouchAction(App.Driver); + actions.LongPress(null, 5, 300).MoveTo(null, 5, 650).Release().Perform(); + + if (!didReachEndofPage) + break; + } + } + + static void ClickText(IApp app, string marked, bool isEditor, out bool didReachEndofPage) + { + app.Click(marked); + didReachEndofPage = CheckIfViewAboveKeyboard(app, marked, isEditor); + if (didReachEndofPage) + HideKeyboard(app, (app as AppiumApp)?.Driver, isEditor); + } + + // 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.NotNull(views); + var rect = views.GetRect(); + + var testApp = app as AppiumApp; + 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, string galleryName) + { + app.WaitForElement("TargetView"); + app.EnterText("TargetView", "KeyboardScrollingEntryNextEditorPage"); + app.Click("GoButton"); + + app.WaitForElement("Entry1"); + app.Click("Entry1"); + CheckIfViewAboveKeyboard(app, "Entry1", false); + NextiOSKeyboardPress((app as AppiumApp)?.Driver); + + CheckIfViewAboveKeyboard(app, "Entry2", false); + NextiOSKeyboardPress((app as AppiumApp)?.Driver); + + CheckIfViewAboveKeyboard(app, "Entry3", false); + NextiOSKeyboardPress((app as AppiumApp)?.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"); + } + + internal static void GridStarRowScrollingTest (IApp app) + { + for (int i = 1; i <= 7; i++) + { + var entry = $"Entry{i}"; + app.WaitForElement(entry); + app.Click(entry); + CheckIfViewAboveKeyboard(app, entry, false); + HideKeyboard(app, (app as AppiumApp)?.Driver, false); + } + } + } +} diff --git a/src/Controls/tests/UITests/Tests/KeyboardScrollingGridTests.cs b/src/Controls/tests/UITests/Tests/KeyboardScrollingGridTests.cs new file mode 100644 index 000000000000..fb97d91ee304 --- /dev/null +++ b/src/Controls/tests/UITests/Tests/KeyboardScrollingGridTests.cs @@ -0,0 +1,34 @@ +using Maui.Controls.Sample; +using NUnit.Framework; +using UITest.Core; + +namespace Microsoft.Maui.AppiumTests +{ + public class KeyboardScrollingGridTests : UITest + { + const string KeyboardScrollingGallery = "Keyboard Scrolling Gallery - Grid with Star Row"; + public KeyboardScrollingGridTests(TestDevice device) + : base(device) + { + } + + protected override void FixtureSetup() + { + base.FixtureSetup(); + App.NavigateToGallery(KeyboardScrollingGallery); + } + + protected override void FixtureTeardown() + { + base.FixtureTeardown(); + this.Back(); + } + + [Test] + public void GridStarRowScrollingTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.GridStarRowScrollingTest(App); + } + } +} diff --git a/src/Controls/tests/UITests/Tests/KeyboardScrollingNonScrollingPageLargeTitlesTests.cs b/src/Controls/tests/UITests/Tests/KeyboardScrollingNonScrollingPageLargeTitlesTests.cs new file mode 100644 index 000000000000..4b89e207a56e --- /dev/null +++ b/src/Controls/tests/UITests/Tests/KeyboardScrollingNonScrollingPageLargeTitlesTests.cs @@ -0,0 +1,48 @@ +using Maui.Controls.Sample; +using NUnit.Framework; +using UITest.Core; + +namespace Microsoft.Maui.AppiumTests +{ + public class KeyboardScrollingNonScrollingPageLargeTitlesTests : UITest + { + const string KeyboardScrollingGallery = "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(); + this.Back(); + } + + [Test] + public void EntriesScrollingPageTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.EntriesScrollingTest(App, KeyboardScrollingGallery); + } + + [Test] + public void EditorsScrollingPageTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.EditorsScrollingTest(App, KeyboardScrollingGallery); + } + + [Test] + public void EntryNextEditorTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.EntryNextEditorScrollingTest(App, 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..eba00e90ec67 --- /dev/null +++ b/src/Controls/tests/UITests/Tests/KeyboardScrollingNonScrollingPageSmallTitlesTests.cs @@ -0,0 +1,48 @@ +using Maui.Controls.Sample; +using NUnit.Framework; +using UITest.Core; + +namespace Microsoft.Maui.AppiumTests +{ + public class KeyboardScrollingNonScrollingPageSmallTitlesTests : UITest + { + const string KeyboardScrollingGallery = "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(); + this.Back(); + } + + [Test] + public void EntriesScrollingPageTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.EntriesScrollingTest(App, KeyboardScrollingGallery); + } + + [Test] + public void EditorsScrollingPageTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.EditorsScrollingTest(App, KeyboardScrollingGallery); + } + + [Test] + public void EntryNextEditorTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.EntryNextEditorScrollingTest(App, 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..0a12137fdaac --- /dev/null +++ b/src/Controls/tests/UITests/Tests/KeyboardScrollingScrollingPageLargeTitlesTests.cs @@ -0,0 +1,48 @@ +using Maui.Controls.Sample; +using NUnit.Framework; +using UITest.Core; + +namespace Microsoft.Maui.AppiumTests +{ + public class KeyboardScrollingScrollingPageLargeTitlesTests : UITest + { + const string KeyboardScrollingGallery = "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(); + this.Back(); + } + + [Test] + public void EntriesScrollingPageTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.EntriesScrollingTest(App, KeyboardScrollingGallery); + } + + [Test] + public void EditorsScrollingPageTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.EditorsScrollingTest(App, KeyboardScrollingGallery); + } + + [Test] + public void EntryNextEditorTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.EntryNextEditorScrollingTest(App, 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..166f4b59e9fc --- /dev/null +++ b/src/Controls/tests/UITests/Tests/KeyboardScrollingScrollingPageSmallTitlesTests.cs @@ -0,0 +1,48 @@ +using Maui.Controls.Sample; +using NUnit.Framework; +using UITest.Core; + +namespace Microsoft.Maui.AppiumTests +{ + public class KeyboardScrollingScrollingPageSmallTitlesTests : UITest + { + const string KeyboardScrollingGallery = "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(); + this.Back(); + } + + [Test] + public void EntriesScrollingPageTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.EntriesScrollingTest(App, KeyboardScrollingGallery); + } + + [Test] + public void EditorsScrollingPageTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.EditorsScrollingTest(App, KeyboardScrollingGallery); + } + + [Test] + public void EntryNextEditorTest() + { + this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.Windows }, KeyboardScrolling.IgnoreMessage); + KeyboardScrolling.EntryNextEditorScrollingTest(App, KeyboardScrollingGallery); + } + } +} diff --git a/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs b/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs index c5e760bc58cd..d05a5686eb93 100644 --- a/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs +++ b/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs @@ -30,6 +30,7 @@ public static class KeyboardAutoManagerScroll static UIView? View; static UIView? ContainerView; static CGRect? CursorRect; + static CGRect? StartingContainerViewFrame; internal static bool IsKeyboardShowing; static int TextViewTopDistance = 20; static int DebounceCount; @@ -39,6 +40,8 @@ public static class KeyboardAutoManagerScroll static NSObject? TextFieldToken; static NSObject? TextViewToken; internal static bool ShouldDisconnectLifecycle; + internal static bool ShouldIgnoreSafeAreaAdjustment; + internal static bool ShouldScrollAgain; /// /// Enables automatic scrolling with keyboard interactions on iOS devices. @@ -123,14 +126,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(); } @@ -195,6 +194,8 @@ static void WillHideKeyboard(NSNotification notification) static void DidHideKeyboard(NSNotification notification) { IsKeyboardAutoScrollHandling = false; + ShouldIgnoreSafeAreaAdjustment = false; + ShouldScrollAgain = false; } static NSObject? FindValue(this NSDictionary dict, string key) @@ -276,17 +277,27 @@ 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(20); if (entranceCount == DebounceCount) + { AdjustPosition(); + + // See if the layout requests to scroll again after our initial scroll + await Task.Delay(5); + if (ShouldScrollAgain) + AdjustPosition(); + } } // main method to calculate and animate the scrolling internal static void AdjustPosition() { if (ContainerView is null - || CursorRect is null || (View is not UITextField && View is not UITextView)) { IsKeyboardAutoScrollHandling = false; @@ -321,10 +332,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) @@ -337,14 +363,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; @@ -420,7 +450,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) { @@ -437,7 +467,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) @@ -470,7 +500,17 @@ 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); @@ -490,11 +530,19 @@ 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 again. + var actualScrolledAmount = superScrollView.ContentOffset.Y - origContentOffsetY; + var amountNotScrolled = requestedMove - actualScrolledAmount; + + if (prefersLargeTitles && amountNotScrolled > 1) + ShouldScrollAgain = true; } else @@ -547,6 +595,7 @@ internal static void AdjustPosition() if (ContainerView.Frame.X != rootViewOrigin.X || ContainerView.Frame.Y != rootViewOrigin.Y) { + ShouldIgnoreSafeAreaAdjustment = true; var rect = ContainerView.Frame; rect.X = rootViewOrigin.X; rect.Y = rootViewOrigin.Y; @@ -627,6 +676,45 @@ 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) + { + // The Large Titles will be presented in Portrait modes on iPhones and all orientations of iPads, + // so skip if we are not in those scenarios. + if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone + && (UIDevice.CurrentDevice.Orientation == UIDeviceOrientation.LandscapeLeft || UIDevice.CurrentDevice.Orientation == UIDeviceOrientation.LandscapeRight)) + return move; + + // These values are not publicly available but can be tested. + // It is possible that these can change in the future. + var navBarCollapsedHeight = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone ? 44 : 50; + var navBarExpandedHeight = navController.NavigationBar.SizeThatFits(new CGSize(0, 0)).Height; + + var minPageMoveToCollapseNavBar = navBarExpandedHeight - navBarCollapsedHeight; + 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 @@ -648,6 +736,9 @@ static void RestorePosition() ContainerView = null; TopViewBeginOrigin = InvalidPoint; CursorRect = null; + StartingContainerViewFrame = null; + ShouldIgnoreSafeAreaAdjustment = false; + ShouldScrollAgain = false; } static NSIndexPath? GetPreviousIndexPath(this UIScrollView scrollView, NSIndexPath indexPath) diff --git a/src/Core/src/Platform/iOS/MauiView.cs b/src/Core/src/Platform/iOS/MauiView.cs index 976a742f66ad..e125a46f5586 100644 --- a/src/Core/src/Platform/iOS/MauiView.cs +++ b/src/Core/src/Platform/iOS/MauiView.cs @@ -32,6 +32,9 @@ bool RespondsToSafeArea() protected CGRect AdjustForSafeArea(CGRect bounds) { + if (KeyboardAutoManagerScroll.ShouldIgnoreSafeAreaAdjustment) + KeyboardAutoManagerScroll.ShouldScrollAgain = true; + if (View is not ISafeAreaView sav || sav.IgnoreSafeArea || !RespondsToSafeArea()) { return bounds;