浏览代码

Fix DateTimePickerPanel scrolling exception (#18584)

* Add failing tests for DateTimePickerPanel

* Fix DateTimePickerPanel scrolling exception
Julien Lebosquain 6 月之前
父节点
当前提交
676d5c76ef

+ 1 - 1
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)

+ 104 - 0
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<DateTimePickerPanel> 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<DateTimePickerPanel>()
+                .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<ICursorFactory>(),
@@ -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;
+            });
+        }
     }
 }

+ 38 - 1
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<DateTimePickerPanel> 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<DateTimePickerPanel>()
+                .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<ICursorFactory>(),
@@ -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;
             });