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