Browse Source

Merge pull request #11323 from AvaloniaUI/fixes/itemscontrol-tab-navigation

Fix ItemsControl tab navigation
pull/11344/head
Max Katz 3 years ago
committed by GitHub
parent
commit
132c3b8a6a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      src/Avalonia.Controls/ListBox.cs
  2. 3
      src/Avalonia.Controls/Menu.cs
  3. 7
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  4. 15
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  5. 24
      src/Avalonia.Controls/TabControl.cs
  6. 121
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  7. 8
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  8. 129
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs

3
src/Avalonia.Controls/ListBox.cs

@ -58,6 +58,9 @@ namespace Avalonia.Controls
static ListBox()
{
ItemsPanelProperty.OverrideDefaultValue<ListBox>(DefaultPanel);
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
typeof(ListBox),
KeyboardNavigationMode.Once);
}
/// <summary>

3
src/Avalonia.Controls/Menu.cs

@ -35,6 +35,9 @@ namespace Avalonia.Controls
static Menu()
{
ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel);
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
typeof(Menu),
KeyboardNavigationMode.Once);
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<Menu>(AccessibilityView.Control);
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<Menu>(AutomationControlType.Menu);
}

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

15
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);
}
}
/// <summary>
@ -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)

24
src/Avalonia.Controls/TabControl.cs

@ -225,6 +225,20 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter");
ItemsPresenterPart?.ApplyTemplate();
// Set TabNavigation to Once on the panel if not already set and
// forward the TabOnceActiveElement to the panel.
if (ItemsPresenterPart?.Panel is { } panel)
{
if (!panel.IsSet(KeyboardNavigation.TabNavigationProperty))
panel.SetCurrentValue(
KeyboardNavigation.TabNavigationProperty,
KeyboardNavigationMode.Once);
KeyboardNavigation.SetTabOnceActiveElement(
panel,
KeyboardNavigation.GetTabOnceActiveElement(this));
}
}
/// <inheritdoc/>
@ -268,7 +282,17 @@ namespace Avalonia.Controls
base.OnPropertyChanged(change);
if (change.Property == TabStripPlacementProperty)
{
RefreshContainers();
}
else if (change.Property == KeyboardNavigation.TabOnceActiveElementProperty &&
ItemsPresenterPart?.Panel is { } panel)
{
// Forward TabOnceActiveElement to the panel.
KeyboardNavigation.SetTabOnceActiveElement(
panel,
change.GetNewValue<IInputElement?>());
}
}
}
}

121
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<string>, INotifyCollectionChanged

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

@ -1236,13 +1236,13 @@ namespace Avalonia.Controls.UnitTests.Primitives
};
AvaloniaLocator.CurrentMutable.Bind<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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));
}
}

129
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -5,8 +5,10 @@ using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
@ -413,6 +415,117 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Tab_Navigation_Should_Move_To_First_TabItem_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 TabControl
{
Template = TabControlTemplate(),
Items =
{
new TabItem { Header = "foo" },
new TabItem { Header = "bar" },
new TabItem { Header = "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_TabItem()
{
var services = TestServices.StyledWindow.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice());
using var app = UnitTestApplication.Start(services);
var target = new TestTabControl
{
Template = TabControlTemplate(),
Items =
{
new TabItem { Header = "foo" },
new TabItem { Header = "bar" },
new TabItem { Header = "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 IControlTemplate TabControlTemplate()
{
return new FuncControlTemplate<TabControl>((parent, scope) =>
@ -452,6 +565,16 @@ namespace Avalonia.Controls.UnitTests
target.Arrange(new Rect(target.DesiredSize));
}
private static void RaiseKeyEvent(Control target, Key key, KeyModifiers inputModifiers = 0)
{
target.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
KeyModifiers = inputModifiers,
Key = key
});
}
private static void ApplyTemplate(TabControl target)
{
target.ApplyTemplate();
@ -479,5 +602,11 @@ namespace Avalonia.Controls.UnitTests
public string Value { get; }
}
private class TestTabControl : TabControl, IStyleable
{
Type IStyleable.StyleKey => typeof(TabControl);
public new ISelectionModel Selection => base.Selection;
}
}
}

Loading…
Cancel
Save