From e7b61ef002ef49f3823c9f8dd6c884d0063d63cc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 18 May 2023 22:57:04 +0200 Subject: [PATCH 1/4] Added BringIntoViewOnFocusChange. Added `ScrollViewer.BringIntoViewOnFocusChange` attached property, based on the UWP API. --- src/Avalonia.Controls/ScrollViewer.cs | 44 ++++++++++++ .../ScrollViewerTests.cs | 71 +++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index a7188c6226..6c039b048a 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -16,6 +16,12 @@ namespace Avalonia.Controls [TemplatePart("PART_VerticalScrollBar", typeof(ScrollBar))] public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider { + /// + /// Defines the property. + /// + public static readonly AttachedProperty BringIntoViewOnFocusChangeProperty = + AvaloniaProperty.RegisterAttached(nameof(BringIntoViewOnFocusChange), true); + /// /// Defines the property. /// @@ -174,6 +180,16 @@ namespace Avalonia.Controls remove => RemoveHandler(ScrollChangedEvent, value); } + /// + /// Gets or sets a value that determines whether the uses a + /// bring-into-view scroll behavior when an item in the view gets focus. + /// + public bool BringIntoViewOnFocusChange + { + get => GetValue(BringIntoViewOnFocusChangeProperty); + set => SetValue(BringIntoViewOnFocusChangeProperty, value); + } + /// /// Gets the extent of the scrollable content. /// @@ -400,6 +416,26 @@ namespace Avalonia.Controls /// public void ScrollToEnd() => SetCurrentValue(OffsetProperty, new Vector(double.NegativeInfinity, double.PositiveInfinity)); + /// + /// Gets the value of the attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static bool GetBringIntoViewOnFocusChange(Control control) + { + return control.GetValue(BringIntoViewOnFocusChangeProperty); + } + + /// + /// Gets the value of the attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetBringIntoViewOnFocusChange(Control control, bool value) + { + control.SetValue(BringIntoViewOnFocusChangeProperty, value); + } + /// /// Gets the value of the HorizontalScrollBarVisibility attached property. /// @@ -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) diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index 82c4b5f8f0..9a7d51d86d 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/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(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(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 && From 3f6b59d0edfe013522f3a752e27b98c32c231826 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 18 May 2023 23:07:34 +0200 Subject: [PATCH 2/4] Hook up BringIntoViewOnFocusChange in templates. --- src/Avalonia.Themes.Fluent/Controls/ListBox.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/TextBox.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/TreeView.xaml | 3 ++- src/Avalonia.Themes.Simple/Controls/ListBox.xaml | 1 + src/Avalonia.Themes.Simple/Controls/TextBox.xaml | 1 + src/Avalonia.Themes.Simple/Controls/TreeView.xaml | 1 + 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml index e2273cd487..05bbbe9558 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml +++ b/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)}"> + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}" + BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}"> + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}" + BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}"> diff --git a/src/Avalonia.Themes.Simple/Controls/ListBox.xaml b/src/Avalonia.Themes.Simple/Controls/ListBox.xaml index eaa1f914ca..a51fcbf72e 100644 --- a/src/Avalonia.Themes.Simple/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ListBox.xaml @@ -17,6 +17,7 @@ CornerRadius="{TemplateBinding CornerRadius}"> diff --git a/src/Avalonia.Themes.Simple/Controls/TreeView.xaml b/src/Avalonia.Themes.Simple/Controls/TreeView.xaml index 8630c14aa2..f2f451fe6f 100644 --- a/src/Avalonia.Themes.Simple/Controls/TreeView.xaml +++ b/src/Avalonia.Themes.Simple/Controls/TreeView.xaml @@ -15,6 +15,7 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> Date: Fri, 19 May 2023 09:27:42 +0200 Subject: [PATCH 3/4] Added info on default value. --- src/Avalonia.Controls/ScrollViewer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 6c039b048a..b8a5c589f0 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -184,6 +184,10 @@ namespace Avalonia.Controls /// Gets or sets a value that determines whether the uses a /// bring-into-view scroll behavior when an item in the view gets focus. /// + /// + /// 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. + /// public bool BringIntoViewOnFocusChange { get => GetValue(BringIntoViewOnFocusChangeProperty); From f5e950980c8a8c0d840c06e8aa3e32b3618bded8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 19 May 2023 09:35:54 +0200 Subject: [PATCH 4/4] Add info on attached property usage. --- src/Avalonia.Controls/ScrollViewer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index b8a5c589f0..1b9cb7de34 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -188,6 +188,12 @@ namespace Avalonia.Controls /// 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. /// + /// + /// can either be set explicitly on a + /// , or a the attached + /// ScrollViewer.BringIntoViewOnFocusChange property can be set on an element + /// that hosts a . + /// public bool BringIntoViewOnFocusChange { get => GetValue(BringIntoViewOnFocusChangeProperty);