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;