Browse Source

Fix some issues with tabbing into virtualized list (#13826)

* Add failing unit test for scenario 1 in #11878.

* Set TabOnceActiveElement on realized container.

Fixes scenario 1 in #11878.

* Use TabOnceActiveElement to decide focused element.

Fixes scenario #3 in #11878.
pull/13818/head
Steven Kirk 2 years ago
committed by GitHub
parent
commit
62314a010e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      src/Avalonia.Controls/ItemsControl.cs
  2. 3
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  3. 34
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  4. 3
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  5. 19
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  6. 2
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  7. 5
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

11
src/Avalonia.Controls/ItemsControl.cs

@ -528,6 +528,17 @@ namespace Avalonia.Controls
_itemsPresenter = e.NameScope.Find<ItemsPresenter>("PART_ItemsPresenter");
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
// If the focus is coming from a child control, set the tab once active element to
// the focused control. This ensures that tabbing back into the control will focus
// the last focused control when TabNavigationMode == Once.
if (e.Source != this && e.Source is IInputElement ie)
KeyboardNavigation.SetTabOnceActiveElement(this, ie);
}
/// <summary>
/// Handles directional navigation within the <see cref="ItemsControl"/>.
/// </summary>

3
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -513,6 +513,9 @@ namespace Avalonia.Controls.Primitives
var containerIsSelected = GetIsSelected(container);
UpdateSelection(index, containerIsSelected, toggleModifier: true);
}
if (Selection.AnchorIndex == index)
KeyboardNavigation.SetTabOnceActiveElement(this, container);
}
/// <inheritdoc />

34
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -265,6 +265,16 @@ namespace Avalonia.Controls
}
}
protected override void OnItemsControlChanged(ItemsControl? oldValue)
{
base.OnItemsControlChanged(oldValue);
if (oldValue is not null)
oldValue.PropertyChanged -= OnItemsControlPropertyChanged;
if (ItemsControl is not null)
ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
}
protected override IInputElement? GetControl(NavigationDirection direction, IInputElement? from, bool wrap)
{
var count = Items.Count;
@ -378,7 +388,7 @@ namespace Avalonia.Controls
var scrollToElement = GetOrCreateElement(items, index);
scrollToElement.Measure(Size.Infinity);
// Get the expected position of the elment and put it in place.
// Get the expected position of the element and put it in place.
var anchorU = _realizedElements.GetOrEstimateElementU(index, ref _lastEstimatedElementSizeU);
var rect = Orientation == Orientation.Horizontal ?
new Rect(anchorU, 0, scrollToElement.DesiredSize.Width, scrollToElement.DesiredSize.Height) :
@ -661,6 +671,7 @@ namespace Avalonia.Controls
private void RecycleElement(Control element, int index)
{
Debug.Assert(ItemsControl is not null);
Debug.Assert(ItemContainerGenerator is not null);
_scrollAnchorProvider?.UnregisterAnchorCandidate(element);
@ -675,11 +686,10 @@ namespace Avalonia.Controls
{
element.IsVisible = false;
}
else if (element.IsKeyboardFocusWithin)
else if (KeyboardNavigation.GetTabOnceActiveElement(ItemsControl) == element)
{
_focusedElement = element;
_focusedIndex = index;
_focusedElement.LostFocus += OnUnrealizedFocusedElementLostFocus;
}
else
{
@ -746,15 +756,17 @@ namespace Avalonia.Controls
}
}
private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e)
private void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (_focusedElement is null || sender != _focusedElement)
return;
_focusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus;
RecycleElement(_focusedElement, _focusedIndex);
_focusedElement = null;
_focusedIndex = -1;
if (_focusedElement is not null &&
e.Property == KeyboardNavigation.TabOnceActiveElementProperty &&
e.GetOldValue<IInputElement?>() == _focusedElement)
{
// TabOnceActiveElement has moved away from _focusedElement so we can recycle it.
RecycleElement(_focusedElement, _focusedIndex);
_focusedElement = null;
_focusedIndex = -1;
}
}
/// <inheritdoc/>

3
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -217,6 +217,9 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(10, target.Presenter.Panel.Children.Count);
Assert.True(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
// The selected item must not be the anchor, otherwise it won't get recycled.
target.Selection.AnchorIndex = -1;
// Scroll down a page.
target.Scroll.Offset = new Vector(0, 10);
Layout(target);

19
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -1226,6 +1226,25 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(0, root.SelectedIndex);
}
[Fact]
public void TabOnceActiveElement_Should_Be_Initialized_With_SelectedItem()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new ListBox
{
Template = Template(),
ItemsSource = new[] { "Foo", "Bar", "Baz " },
SelectedIndex = 1,
};
Prepare(target);
var container = target.ContainerFromIndex(1)!;
Assert.Same(container, KeyboardNavigation.GetTabOnceActiveElement(target));
}
}
[Fact]
public void Setting_SelectedItem_With_Pointer_Should_Set_TabOnceActiveElement()
{

2
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -1110,7 +1110,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(19, panel.LastRealizedIndex);
// The selection should be preserved.
Assert.Empty(SelectedContainers(target));
Assert.Equal(new[] { 1 }, SelectedContainers(target));
Assert.Equal(1, target.SelectedIndex);
Assert.Same(items[1], target.SelectedItem);
Assert.Equal(new[] { 1 }, target.Selection.SelectedIndexes);

5
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@ -8,6 +8,7 @@ using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
@ -314,15 +315,19 @@ namespace Avalonia.Controls.UnitTests
{
using var app = App();
var (target, scroll, itemsControl) = CreateTarget();
var items = (IList)itemsControl.ItemsSource!;
var focused = target.GetRealizedElements().First()!;
focused.Focusable = true;
focused.Focus();
Assert.True(focused.IsKeyboardFocusWithin);
Assert.Equal(focused, KeyboardNavigation.GetTabOnceActiveElement(itemsControl));
scroll.Offset = new Vector(0, 200);
Layout(target);
items.RemoveAt(0);
Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
Assert.All(target.GetRealizedElements(), x => Assert.NotSame(focused, x));
}

Loading…
Cancel
Save