Browse Source

fix(VirtualizingStackPanel): hide cached focused items (#21835)

* test(VirtualizingStackPanel): cover focused item virtualization

A focused item remains attached so keyboard focus can be preserved after it leaves the realized range. It must nevertheless stop rendering until it is realized again; otherwise an estimated position can surface it in the viewport.

* fix(VirtualizingStackPanel): hide cached focused items

A focused container stays attached after it leaves the realized range so keyboard focus survives. Its estimated position can overlap the viewport when item sizes vary, causing it to render and receive pointer input as a ghost item.\n\nSuppress rendering and hit testing while the container is cached, then restore its original state before re-realization or recycling. This preserves focus and template-provided opacity.

* fix(VirtualizingStackPanel): restore current focused container state

Focused-container suppression previously replayed the opacity and hit-test values observed when virtualization began. That could hide updates from bindings, styles, or application code while the container was retained.\n\nUse disposable animation-priority overrides for the temporary suppression so disposing them reveals the property's current underlying value.

* fix(VirtualizingStackPanel): keep retained focused container out of the viewport

The container of a focused item that leaves the realized range is retained
to preserve keyboard focus, and arranged at a position estimated from the
average realized element size. With differing item sizes that estimate can
place the container so that it overlaps the viewport, where it renders as a
ghost item and intercepts pointer input (#17935).

An unrealized item before the realized range always ends at or before the
range's start, and one after it always starts at or after the range's end.
Clamp the estimated arrange position to that invariant so the retained
container can never overlap the realized elements, replacing the previous
approach of masking the container with animation-priority opacity and
hit-test overrides.
pull/22027/head
Nathan Nguyen 2 months ago
committed by GitHub
parent
commit
f5d2bc9e6d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 22
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  2. 34
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

22
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);
}

34
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)]

Loading…
Cancel
Save