From 31ece204fcdc9ade3c43ffc8927323332770325d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 11:36:00 +0200 Subject: [PATCH] Fix tab navigation in ItemsControl & ListBox - Don't set `KeyboardNavigationMode.Once` on `ItemsPresenter` - Instead set it on `ListBox` (more controls to come) - Make `TabOnceActiveElement` follow `Selection.AnchorIndex` in `SelectingItemsControl` and set it on `ItemsControl` itself --- src/Avalonia.Controls/ListBox.cs | 3 + .../Presenters/ItemsPresenter.cs | 7 - .../Primitives/SelectingItemsControl.cs | 15 +-- .../ListBoxTests.cs | 121 ++++++++++++++++-- .../Primitives/SelectingItemsControlTests.cs | 8 +- 5 files changed, 120 insertions(+), 34 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 2bdc37537a..2586333f20 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -58,6 +58,9 @@ namespace Avalonia.Controls static ListBox() { ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( + typeof(ListBox), + KeyboardNavigationMode.Once); } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 88d1e10ba4..9610be088b 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -52,13 +52,6 @@ namespace Avalonia.Controls.Presenters nameof(VerticalSnapPointsChanged), RoutingStrategies.Bubble); - static ItemsPresenter() - { - KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( - typeof(ItemsPresenter), - KeyboardNavigationMode.Once); - } - event EventHandler? ILogicalScrollable.ScrollInvalidated { add => _scrollInvalidated += value; diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 663a315732..af82a89517 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -5,6 +5,7 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Xml.Linq; using Avalonia.Controls.Selection; using Avalonia.Controls.Utils; using Avalonia.Data; @@ -528,13 +529,6 @@ namespace Avalonia.Controls.Primitives protected internal override void ClearContainerForItemOverride(Control element) { base.ClearContainerForItemOverride(element); - - if (Presenter?.Panel is InputElement panel && - KeyboardNavigation.GetTabOnceActiveElement(panel) == element) - { - KeyboardNavigation.SetTabOnceActiveElement(panel, null); - } - element.ClearValue(IsSelectedProperty); } @@ -834,12 +828,6 @@ namespace Avalonia.Controls.Primitives Selection.Clear(); Selection.Select(index); } - - if (Presenter?.Panel is { } panel) - { - var container = ContainerFromIndex(index); - KeyboardNavigation.SetTabOnceActiveElement(panel, container); - } } /// @@ -928,6 +916,7 @@ namespace Avalonia.Controls.Primitives if (e.PropertyName == nameof(ISelectionModel.AnchorIndex)) { _hasScrolledToSelectedItem = false; + KeyboardNavigation.SetTabOnceActiveElement(this, ContainerFromIndex(Selection.AnchorIndex)); AutoScrollToSelectedItemIfNecessary(); } else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 84eed5ec82..72f476a3b0 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -752,16 +752,6 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(Enumerable.Range(0, 10).Select(x => $"Item{10 - x}"), realized); } - private static void RaiseKeyEvent(ListBox listBox, Key key, KeyModifiers inputModifiers = 0) - { - listBox.RaiseEvent(new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - KeyModifiers = inputModifiers, - Key = key - }); - } - [Fact] public void WrapSelection_Should_Wrap() { @@ -948,6 +938,117 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, raised); } + [Fact] + public void Tab_Navigation_Should_Move_To_First_Item_When_No_Anchor_Element_Selected() + { + var services = TestServices.StyledWindow.With( + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice()); + using var app = UnitTestApplication.Start(services); + + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = { "Foo", "Bar", "Baz" }, + }; + + var button = new Button + { + Content = "Button", + [DockPanel.DockProperty] = Dock.Top, + }; + + var root = new TestRoot + { + Child = new DockPanel + { + Children = + { + button, + target, + } + } + }; + + var navigation = new KeyboardNavigationHandler(); + navigation.SetOwner(root); + + root.LayoutManager.ExecuteInitialLayoutPass(); + + button.Focus(); + RaiseKeyEvent(button, Key.Tab); + + var item = target.ContainerFromIndex(0); + Assert.Same(item, FocusManager.Instance.Current); + } + + [Fact] + public void Tab_Navigation_Should_Move_To_Anchor_Element() + { + var services = TestServices.StyledWindow.With( + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice()); + using var app = UnitTestApplication.Start(services); + + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = { "Foo", "Bar", "Baz" }, + }; + + var button = new Button + { + Content = "Button", + [DockPanel.DockProperty] = Dock.Top, + }; + + var root = new TestRoot + { + Width = 1000, + Height = 1000, + Child = new DockPanel + { + Children = + { + button, + target, + } + } + }; + + var navigation = new KeyboardNavigationHandler(); + navigation.SetOwner(root); + + root.LayoutManager.ExecuteInitialLayoutPass(); + + button.Focus(); + target.Selection.AnchorIndex = 1; + RaiseKeyEvent(button, Key.Tab); + + var item = target.ContainerFromIndex(1); + Assert.Same(item, FocusManager.Instance.Current); + + RaiseKeyEvent(item, Key.Tab); + + Assert.Same(button, FocusManager.Instance.Current); + + target.Selection.AnchorIndex = 2; + RaiseKeyEvent(button, Key.Tab); + + item = target.ContainerFromIndex(2); + Assert.Same(item, FocusManager.Instance.Current); + } + + private static void RaiseKeyEvent(Control target, Key key, KeyModifiers inputModifiers = 0) + { + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + KeyModifiers = inputModifiers, + Key = key + }); + } + private record ItemViewModel(string Caption); private class ResettingCollection : List, INotifyCollectionChanged diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index dfdcd09bf9..9d2ffb1fa2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1236,13 +1236,13 @@ namespace Avalonia.Controls.UnitTests.Primitives }; AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); Prepare(target); - _helper.Down((Interactive)target.Presenter.Panel.Children[1]); + + var container = target.ContainerFromIndex(1)!; + _helper.Down(container); var panel = target.Presenter.Panel; - Assert.Equal( - KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel), - panel.Children[1]); + Assert.Same(container, KeyboardNavigation.GetTabOnceActiveElement(target)); } }