Ver Fonte

Merge pull request #11442 from AvaloniaUI/feature/bringintoviewonfocuschange

Bring controls into view when focused
Max Katz há 2 anos atrás
pai
commit
cdefb0c9d6

+ 54 - 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,26 @@ 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>
+        /// <value>
+        /// true to use a behavior that brings focused items into view. false to use a behavior
+        /// that focused items do not automatically scroll into view. The default is true.
+        /// </value>
+        /// <remarks>
+        /// <see cref="BringIntoViewOnFocusChange"/> can either be set explicitly on a
+        /// <see cref="ScrollViewer"/>, or a the attached 
+        /// <code>ScrollViewer.BringIntoViewOnFocusChange</code> property can be set on an element
+        /// that hosts a <see cref="ScrollViewer"/>.
+        /// </remarks>
+        public bool BringIntoViewOnFocusChange
+        {
+            get => GetValue(BringIntoViewOnFocusChangeProperty);
+            set => SetValue(BringIntoViewOnFocusChangeProperty, value);
+        }
+
         /// <summary>
         /// Gets the extent of the scrollable content.
         /// </summary>
@@ -400,6 +426,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 +742,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)

+ 2 - 1
src/Avalonia.Themes.Fluent/Controls/ListBox.xaml

@@ -36,7 +36,8 @@
                         VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
                         IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
                         IsScrollInertiaEnabled="{TemplateBinding (ScrollViewer.IsScrollInertiaEnabled)}"
-                        AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
+                        AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"
+                        BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}">
             <ItemsPresenter Name="PART_ItemsPresenter"
                             AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
                             AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"

+ 2 - 1
src/Avalonia.Themes.Fluent/Controls/TextBox.xaml

@@ -136,7 +136,8 @@
                   <ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
                                 VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
                                 IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
-                                AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
+                                AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"
+                                BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}">
                     <Panel>
                       <TextBlock Name="PART_Watermark"
                               Opacity="0.5"

+ 2 - 1
src/Avalonia.Themes.Fluent/Controls/TreeView.xaml

@@ -30,7 +30,8 @@
                         HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
                         VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
                         IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
-                        AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
+                        AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"
+                        BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}">
             <ItemsPresenter Name="PART_ItemsPresenter"
                             ItemsPanel="{TemplateBinding ItemsPanel}"
                             Margin="{TemplateBinding Padding}" />

+ 1 - 0
src/Avalonia.Themes.Simple/Controls/ListBox.xaml

@@ -17,6 +17,7 @@
                 CornerRadius="{TemplateBinding CornerRadius}">
           <ScrollViewer Name="PART_ScrollViewer"
                         AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"
+                        BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}"
                         Background="{TemplateBinding Background}"
                         HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
                         IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"

+ 1 - 0
src/Avalonia.Themes.Simple/Controls/TextBox.xaml

@@ -126,6 +126,7 @@
                 <ScrollViewer Grid.Column="1"
                               Grid.ColumnSpan="1"
                               AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"
+                              BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}"
                               HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
                               IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
                               VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">

+ 1 - 0
src/Avalonia.Themes.Simple/Controls/TreeView.xaml

@@ -15,6 +15,7 @@
                 BorderThickness="{TemplateBinding BorderThickness}"
                 CornerRadius="{TemplateBinding CornerRadius}">
           <ScrollViewer AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"
+                        BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}"
                         Background="{TemplateBinding Background}"
                         HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
                         IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"

+ 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 &&