From e7b61ef002ef49f3823c9f8dd6c884d0063d63cc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 18 May 2023 22:57:04 +0200 Subject: [PATCH] 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 &&