diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs index c4e42c73e5..11fc695d15 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs @@ -234,7 +234,7 @@ namespace Avalonia.Controls.Primitives else break; } - children.MoveRange(0, numCountsToMove, children.Count); + children.MoveRange(0, numCountsToMove, children.Count - 1); var scrollHeight = _extent.Height - Viewport.Height; if (ShouldLoop && value.Y >= scrollHeight - _extentOne) diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index 6084bbb9a0..198a3fd1f6 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reactive.Subjects; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -244,6 +245,40 @@ namespace Avalonia.Controls.UnitTests Assert.True(handled); } + [Theory] + [InlineData("PART_DaySelector")] + [InlineData("PART_MonthSelector")] + [InlineData("PART_YearSelector")] + public void Selector_ScrollUp_Should_Work(string selectorName) + => TestSelectorScrolling(selectorName, panel => panel.ScrollUp()); + + [Theory] + [InlineData("PART_DaySelector")] + [InlineData("PART_MonthSelector")] + [InlineData("PART_YearSelector")] + public void Selector_ScrollDown_Should_Work(string selectorName) + => TestSelectorScrolling(selectorName, panel => panel.ScrollDown()); + + private static void TestSelectorScrolling(string selectorName, Action scroll) + { + using var app = UnitTestApplication.Start(Services); + + var presenter = new DatePickerPresenter { Template = CreatePickerTemplate() }; + presenter.ApplyTemplate(); + presenter.Measure(new Size(1000, 1000)); + + var panel = presenter + .GetVisualDescendants() + .OfType() + .FirstOrDefault(panel => panel.Name == selectorName); + + Assert.NotNull(panel); + + var previousOffset = panel.Offset; + scroll(panel); + Assert.NotEqual(previousOffset, panel.Offset); + } + private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new HeadlessFontManagerStub(), standardCursorFactory: Mock.Of(), @@ -298,5 +333,74 @@ namespace Avalonia.Controls.UnitTests return layoutRoot; }); } + + private static IControlTemplate CreatePickerTemplate() + { + return new FuncControlTemplate((_, scope) => + { + var dayHost = new Panel + { + Name = "PART_DayHost" + }.RegisterInNameScope(scope); + + var daySelector = new DateTimePickerPanel + { + Name = "PART_DaySelector", + PanelType = DateTimePickerPanelType.Day, + ShouldLoop = true + }.RegisterInNameScope(scope); + + var monthHost = new Panel + { + Name = "PART_MonthHost" + }.RegisterInNameScope(scope); + + var monthSelector = new DateTimePickerPanel + { + Name = "PART_MonthSelector", + PanelType = DateTimePickerPanelType.Month, + ShouldLoop = true + }.RegisterInNameScope(scope); + + var yearHost = new Panel + { + Name = "PART_YearHost" + }.RegisterInNameScope(scope); + + var yearSelector = new DateTimePickerPanel + { + Name = "PART_YearSelector", + PanelType = DateTimePickerPanelType.Year, + ShouldLoop = true + }.RegisterInNameScope(scope); + + var acceptButton = new Button + { + Name = "PART_AcceptButton" + }.RegisterInNameScope(scope); + + var pickerContainer = new Grid + { + Name = "PART_PickerContainer" + }.RegisterInNameScope(scope); + + var firstSpacer = new Rectangle + { + Name = "PART_FirstSpacer" + }.RegisterInNameScope(scope); + + var secondSpacer = new Rectangle + { + Name = "PART_SecondSpacer" + }.RegisterInNameScope(scope); + + var contentPanel = new Panel(); + contentPanel.Children.AddRange([ + dayHost, daySelector, monthHost, monthSelector, yearHost, yearSelector, + acceptButton, pickerContainer, firstSpacer, secondSpacer + ]); + return contentPanel; + }); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs index fdef893320..00e5c4d385 100644 --- a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs @@ -275,6 +275,40 @@ namespace Avalonia.Controls.UnitTests } } + [Theory] + [InlineData("PART_HourSelector")] + [InlineData("PART_MinuteSelector")] + [InlineData("PART_SecondSelector")] + public void Selector_ScrollUp_Should_Work(string selectorName) + => TestSelectorScrolling(selectorName, panel => panel.ScrollUp()); + + [Theory] + [InlineData("PART_HourSelector")] + [InlineData("PART_MinuteSelector")] + [InlineData("PART_SecondSelector")] + public void Selector_ScrollDown_Should_Work(string selectorName) + => TestSelectorScrolling(selectorName, panel => panel.ScrollDown()); + + private static void TestSelectorScrolling(string selectorName, Action scroll) + { + using var app = UnitTestApplication.Start(Services); + + var presenter = new TimePickerPresenter { Template = CreatePickerTemplate() }; + presenter.ApplyTemplate(); + presenter.Measure(new Size(1000, 1000)); + + var panel = presenter + .GetVisualDescendants() + .OfType() + .FirstOrDefault(panel => panel.Name == selectorName); + + Assert.NotNull(panel); + + var previousOffset = panel.Offset; + scroll(panel); + Assert.NotEqual(previousOffset, panel.Offset); + } + private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new HeadlessFontManagerStub(), standardCursorFactory: Mock.Of(), @@ -398,12 +432,14 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_HourSelector", PanelType = DateTimePickerPanelType.Hour, + ShouldLoop = true }.RegisterInNameScope(scope); var minuteSelector = new DateTimePickerPanel { Name = "PART_MinuteSelector", PanelType = DateTimePickerPanelType.Minute, + ShouldLoop = true }.RegisterInNameScope(scope); var secondHost = new Panel @@ -415,6 +451,7 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_SecondSelector", PanelType = DateTimePickerPanelType.Second, + ShouldLoop = true }.RegisterInNameScope(scope); var periodHost = new Panel @@ -443,7 +480,7 @@ namespace Avalonia.Controls.UnitTests Name = "PART_ThirdSpacer" }.RegisterInNameScope(scope); - var contentPanel = new StackPanel(); + var contentPanel = new Panel(); contentPanel.Children.AddRange(new Control[] { acceptButton, hourSelector, minuteSelector, secondHost, secondSelector, periodHost, periodSelector, pickerContainer, secondSpacer, thirdSpacer }); return contentPanel; });