diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_CalendarView.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_CalendarView.cs index 851a539b1ddd..4b29f020d760 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_CalendarView.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_CalendarView.cs @@ -24,6 +24,8 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls; [RunsOnUIThread] public class Given_CalendarView { + const int DEFAULT_MIN_MAX_DATE_YEAR_OFFSET = 100; + [TestMethod] [UnoWorkItem("https://github.com/unoplatform/uno/issues/16123")] [Ignore("Test is unstable on CI: https://github.com/unoplatform/uno/issues/16123")] @@ -61,6 +63,42 @@ public async Task When_MinDate_Has_Different_Offset() await UITestHelper.Load(calendarView); } + [TestMethod] + public async Task When_Scroll_To_MaxDate() + { + var calendarView = new CalendarView() + { + DisplayMode = CalendarViewDisplayMode.Decade + }; + + await UITestHelper.Load(calendarView); + + Type calendarViewType = typeof(CalendarView); + MethodInfo ChangeVisualStateInfo = calendarViewType.GetMethod("ChangeVisualState", BindingFlags.NonPublic | BindingFlags.Instance); + + // Scroll to max date + calendarView.SetDisplayDate(calendarView.MaxDate); + + // Switch to Year view + calendarView.DisplayMode = CalendarViewDisplayMode.Year; + ChangeVisualStateInfo.Invoke(calendarView, new object[] { false }); + await TestServices.WindowHelper.WaitForIdle(); + + // Switch back to Decade view + calendarView.DisplayMode = CalendarViewDisplayMode.Decade; + ChangeVisualStateInfo.Invoke(calendarView, new object[] { false }); + await TestServices.WindowHelper.WaitForIdle(); + + // Decade viewport should be full of items (no missing row) + calendarView.GetActiveGeneratorHost(out var pHost); + var maxDecadeIndex = DEFAULT_MIN_MAX_DATE_YEAR_OFFSET * 2; + var maxDisplayedItems = pHost.Panel.Rows * pHost.Panel.Cols; + + // The first visible index should be less than the max possible index minus the max items we can display + // Worst case scenario is that the last row only has 1 item + Assert.IsTrue(pHost.Panel.FirstVisibleIndex <= maxDecadeIndex - (maxDisplayedItems - pHost.Panel.Rows - 1)); + } + #if __WASM__ [TestMethod] [Ignore("Fails on Fluent styles #17272")] diff --git a/src/Uno.UI/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.ModernCollectionBasePanel.cs b/src/Uno.UI/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.ModernCollectionBasePanel.cs index 7cd5d8476da8..7befaaec8e9f 100644 --- a/src/Uno.UI/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.ModernCollectionBasePanel.cs +++ b/src/Uno.UI/UI/Xaml/Controls/CalendarView/Primitives/CalendarPanel.ModernCollectionBasePanel.cs @@ -432,7 +432,21 @@ internal void ScrollItemIntoView(int index, ScrollIntoViewAlignment alignment, d // then it will request GetContainerFromIndex and tries to focus it. // So here we prepare the _effectiveViewport (which will most probably be re-updated by the ChangeView below), // and then force a base_Measure() - _effectiveViewport.Y += newOffset - currentOffset; + +#if HAS_UNO + // Scrolling to the MaxDate and switching between Decade/Year/Month views keeps increasing the _effectiveViewport.Y + // even though there's no more items to show after MaxDate, causing empty rows in the viewport + if (newOffset >= sv.ScrollableHeight) + { + _effectiveViewport.Y = currentOffset; + } + else + { + _effectiveViewport.Y += newOffset - currentOffset; + } +#else + _effectiveViewport.Y += newOffset - currentOffset; +#endif sv.ChangeView( horizontalOffset: null,