ソースを参照

Added BringIntoViewOnFocusChange.

Added `ScrollViewer.BringIntoViewOnFocusChange` attached property, based on the UWP API.
Steven Kirk 2 年 前
コミット
e7b61ef002

+ 44 - 0
src/Avalonia.Controls/ScrollViewer.cs

@@ -16,6 +16,12 @@ namespace Avalonia.Controls
     [TemplatePart("PART_VerticalScrollBar",   typeof(ScrollBar))]
     public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider
     {
+        /// <summary>
+        /// Defines the <see cref="BringIntoViewOnFocusChange "/> property.
+        /// </summary>
+        public static readonly AttachedProperty<bool> BringIntoViewOnFocusChangeProperty =
+            AvaloniaProperty.RegisterAttached<ScrollViewer, Control, bool>(nameof(BringIntoViewOnFocusChange), true);
+
         /// <summary>
         /// Defines the <see cref="Extent"/> property.
         /// </summary>
@@ -174,6 +180,16 @@ namespace Avalonia.Controls
             remove => RemoveHandler(ScrollChangedEvent, value);
         }
 
+        /// <summary>
+        /// Gets or sets a value that determines whether the <see cref="ScrollViewer"/> uses a
+        /// bring-into-view scroll behavior when an item in the view gets focus.
+        /// </summary>
+        public bool BringIntoViewOnFocusChange
+        {
+            get => GetValue(BringIntoViewOnFocusChangeProperty);
+            set => SetValue(BringIntoViewOnFocusChangeProperty, value);
+        }
+
         /// <summary>
         /// Gets the extent of the scrollable content.
         /// </summary>
@@ -400,6 +416,26 @@ namespace Avalonia.Controls
         /// </summary>
         public void ScrollToEnd() => SetCurrentValue(OffsetProperty, new Vector(double.NegativeInfinity, double.PositiveInfinity));
 
+        /// <summary>
+        /// Gets the value of the <see cref="BringIntoViewOnFocusChange"/> attached property.
+        /// </summary>
+        /// <param name="control">The control to read the value from.</param>
+        /// <returns>The value of the property.</returns>
+        public static bool GetBringIntoViewOnFocusChange(Control control)
+        {
+            return control.GetValue(BringIntoViewOnFocusChangeProperty);
+        }
+
+        /// <summary>
+        /// Gets the value of the <see cref="BringIntoViewOnFocusChange"/> attached property.
+        /// </summary>
+        /// <param name="control">The control to set the value on.</param>
+        /// <param name="value">The value of the property.</param>
+        public static void SetBringIntoViewOnFocusChange(Control control, bool value)
+        {
+            control.SetValue(BringIntoViewOnFocusChangeProperty, value);
+        }
+
         /// <summary>
         /// Gets the value of the HorizontalScrollBarVisibility attached property.
         /// </summary>
@@ -696,6 +732,14 @@ namespace Avalonia.Controls
             }
         }
 
+        protected override void OnGotFocus(GotFocusEventArgs e)
+        {
+            base.OnGotFocus(e);
+
+            if (e.Source != this && e.Source is Control c && BringIntoViewOnFocusChange)
+                c.BringIntoView();
+        }
+
         protected override void OnKeyDown(KeyEventArgs e)
         {
             if (e.Key == Key.PageUp)

+ 71 - 0
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@@ -358,6 +358,77 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(100, thumb.Bounds.Top);
         }
 
+        [Fact]
+        public void BringIntoViewOnFocusChange_Scrolls_Child_Control_Into_View_When_Focused()
+        {
+            using var app = UnitTestApplication.Start(TestServices.RealFocus);
+            var content = new StackPanel
+            {
+                Children =
+                {
+                    new Button
+                    {
+                        Width = 100,
+                        Height = 900,
+                    },
+                    new Button
+                    {
+                        Width = 100,
+                        Height = 900,
+                    },
+                }
+            };
+
+            var target = new ScrollViewer
+            {
+                Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
+                Content = content,
+            };
+            var root = new TestRoot(target);
+
+            root.LayoutManager.ExecuteInitialLayoutPass();
+
+            var button = (Button)content.Children[1];
+            button.Focus();
+
+            Assert.Equal(new Vector(0, 800), target.Offset);
+        }
+
+        [Fact]
+        public void BringIntoViewOnFocusChange_False_Does_Not_Scroll_Child_Control_Into_View_When_Focused()
+        {
+            var content = new StackPanel
+            {
+                Children =
+                {
+                    new Button
+                    {
+                        Width = 100,
+                        Height = 900,
+                    },
+                    new Button
+                    {
+                        Width = 100,
+                        Height = 900,
+                    },
+                }
+            };
+
+            var target = new ScrollViewer
+            {
+                Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
+                Content = content,
+            };
+            var root = new TestRoot(target);
+
+            root.LayoutManager.ExecuteInitialLayoutPass();
+
+            var button = (Button)content.Children[1];
+            button.Focus();
+
+            Assert.Equal(new Vector(0, 0), target.Offset);
+        }
+
         private Point GetRootPoint(Visual control, Point p)
         {
             if (control.GetVisualRoot() is Visual root &&