diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index a8cc7b669d..62010aecc4 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -293,10 +293,28 @@ namespace Avalonia.Controls // Ensure that the focused element is in the correct position. if (_focusedElement is not null && _focusedIndex >= 0) { + var realizedEndU = u; + var sizeU = orientation == Orientation.Horizontal ? + _focusedElement.DesiredSize.Width : + _focusedElement.DesiredSize.Height; + u = GetOrEstimateElementU(_focusedIndex); + + // The focused element's position is estimated as it's outside the realized + // range. A bad estimate must not place it over the realized elements in the + // viewport: an element before the realized range always ends at or before its + // start, and one after it always starts at or after its end. + if (_realizedElements.Count > 0 && !double.IsNaN(_realizedElements.StartU)) + { + if (_focusedIndex < _realizedElements.FirstIndex) + u = Math.Min(u, _realizedElements.StartU - sizeU); + else if (_focusedIndex > _realizedElements.LastIndex) + u = Math.Max(u, realizedEndU); + } + var rect = orientation == Orientation.Horizontal ? - new Rect(u, 0, _focusedElement.DesiredSize.Width, finalSize.Height) : - new Rect(0, u, finalSize.Width, _focusedElement.DesiredSize.Height); + new Rect(u, 0, sizeU, finalSize.Height) : + new Rect(0, u, finalSize.Width, sizeU); _focusedElement.Arrange(rect); } diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 873da11c67..f9d079835d 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -492,6 +492,40 @@ namespace Avalonia.Controls.UnitTests Assert.Same(focused, target.GetRealizedElements().First()); } + [Theory] + [InlineData(0d)] + [InlineData(0.5d)] + public void Focused_Element_Outside_Realized_Range_Is_Not_Arranged_In_Viewport(double bufferFactor) + { + using var app = App(); + + // Item 0 is much taller than the others, so the average-based position estimated for + // it once it has left the realized range lands inside the viewport (#17935). + var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x, x == 0 ? 100 : 10)); + var (target, scroll, itemsControl) = CreateTarget( + items: items, + itemTemplate: CanvasWithHeightTemplate, + bufferFactor: bufferFactor); + + var focused = target.GetRealizedElements().First()!; + focused.Focusable = true; + focused.Focus(); + Assert.True(focused.IsKeyboardFocusWithin); + + scroll.Offset = new Vector(0, 160); + Layout(target); + + var viewport = new Rect(scroll.Offset.X, scroll.Offset.Y, scroll.Viewport.Width, scroll.Viewport.Height); + Assert.True(focused.IsKeyboardFocusWithin); + Assert.False(focused.Bounds.Intersects(viewport)); + + scroll.Offset = new Vector(0, 0); + Layout(target); + + Assert.Same(focused, target.GetRealizedElements().First()); + Assert.Equal(new Rect(0, 0, 100, 100), focused.Bounds); + } + [Theory] [InlineData(0d)] [InlineData(0.5d)]