diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml index 7e945aeaa9..8b697b7948 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml @@ -35,6 +35,7 @@ + diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs index 28c0fe8df6..722215f8b7 100644 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ b/src/Avalonia.Input/KeyboardNavigation.cs @@ -30,6 +30,19 @@ namespace Avalonia.Input "TabOnceActiveElement", typeof(KeyboardNavigation)); + + /// + /// Defines the IsTabStop attached property. + /// + /// + /// The IsTabStop attached property determines whether the control is focusable by tab navigation. + /// + public static readonly AttachedProperty IsTabStopProperty = + AvaloniaProperty.RegisterAttached( + "IsTabStop", + typeof(KeyboardNavigation), + true); + /// /// Gets the for a container. /// @@ -69,5 +82,25 @@ namespace Avalonia.Input { element.SetValue(TabOnceActiveElementProperty, value); } + + /// + /// Sets the for a container. + /// + /// The container. + /// Value indicating whether the container is a tab stop. + public static void SetIsTabStop(InputElement element, bool value) + { + element.SetValue(IsTabStopProperty, value); + } + + /// + /// Gets the for a container. + /// + /// The container. + /// Whether the container is a tab stop. + public static bool GetIsTabStop(InputElement element) + { + return element.GetValue(IsTabStopProperty); + } } } diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 596395eac6..dd50ea438a 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -77,7 +77,8 @@ namespace Avalonia.Input.Navigation /// The element. /// The tab direction. Must be Next or Previous. /// The element's focusable descendants. - private static IEnumerable GetFocusableDescendants(IInputElement element, NavigationDirection direction) + private static IEnumerable GetFocusableDescendants(IInputElement element, + NavigationDirection direction) { var mode = KeyboardNavigation.GetTabNavigation((InputElement)element); @@ -113,7 +114,7 @@ namespace Avalonia.Input.Navigation } else { - if (child.CanFocus()) + if (child.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement)child)) { yield return child; } @@ -122,7 +123,10 @@ namespace Avalonia.Input.Navigation { foreach (var descendant in GetFocusableDescendants(child, direction)) { - yield return descendant; + if (KeyboardNavigation.GetIsTabStop((InputElement)descendant)) + { + yield return descendant; + } } } } @@ -167,7 +171,9 @@ namespace Avalonia.Input.Navigation { element = navigable.GetControl(direction, element, false); - if (element != null && element.CanFocus()) + if (element != null && + element.CanFocus() && + KeyboardNavigation.GetIsTabStop((InputElement) element)) { break; } @@ -233,26 +239,22 @@ namespace Avalonia.Input.Navigation return customNext.next; } - if (sibling.CanFocus()) + if (sibling.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement) sibling)) { return sibling; } - else + + next = direction == NavigationDirection.Next ? + GetFocusableDescendants(sibling, direction).FirstOrDefault() : + GetFocusableDescendants(sibling, direction).LastOrDefault(); + + if (next != null) { - next = direction == NavigationDirection.Next ? - GetFocusableDescendants(sibling, direction).FirstOrDefault() : - GetFocusableDescendants(sibling, direction).LastOrDefault(); - if(next != null) - { - return next; - } + return next; } } - if (next == null) - { - next = GetFirstInNextContainer(element, parent, direction); - } + next = GetFirstInNextContainer(element, parent, direction); } else { @@ -264,7 +266,8 @@ namespace Avalonia.Input.Navigation return next; } - private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, NavigationDirection direction) + private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, + NavigationDirection direction) { if (element is ICustomKeyboardNavigation custom) { diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index 01f0ec247f..1efbbed2e8 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -185,6 +185,35 @@ namespace Avalonia.Input.UnitTests Assert.Equal(next, result); } + [Fact] + public void Next_Skips_Non_TabStop_Siblings() + { + Button current; + Button next; + + var top = new StackPanel + { + Children = + { + new StackPanel + { + Children = + { + new Button { Name = "Button1" }, + new Button { Name = "Button2" }, + (current = new Button { Name = "Button3" }), + new Button { Name="Button4", [KeyboardNavigation.IsTabStopProperty] = false } + } + }, + (next = new Button { Name = "Button5" }), + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Equal(next, result); + } + [Fact] public void Next_Continue_Returns_First_Control_In_Next_Uncle_Container() {