From afa60f38de787989a8c5f6600482789a51aeae3c Mon Sep 17 00:00:00 2001 From: IanRawley <132860927+IanRawley@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:52:19 +1200 Subject: [PATCH] UWP/WinUI style XYFocus subtree restrictions (#16557) * Basic failing unit test for UWP/WinUI XYFocus search boundary scenario. * Change IsAllowedXYNavigationMode to return false if keyDeviceType is null and modes == disabled. * Add helper function to find the closest InputElement to the target element whose parent does not allow XYFocus rather than always searching from the TopLevel. This restricts focus searches within a specific subtree rather than allowing searches to bridge subtrees that share an XYFocus disabled parent. --- .../Input/Navigation/XYFocus.Impl.cs | 7 +++- .../Input/Navigation/XYFocusHelpers.cs | 17 +++++++- .../Input/KeyboardNavigationTests_XY.cs | 41 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs b/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs index 867e80f176..b2a79aa3b9 100644 --- a/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs +++ b/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs @@ -92,6 +92,11 @@ public partial class XYFocus return null; } + if(!(XYFocusHelpers.FindXYSearchRoot(inputElement, keyDeviceType) is InputElement searchRoot)) + { + return null; + } + _instance.SetManifoldsFromBounds(bounds); return _instance.GetNextFocusableElement(direction, inputElement, engagedControl, true, new XYFocusOptions @@ -99,7 +104,7 @@ public partial class XYFocus KeyDeviceType = keyDeviceType, FocusedElementBounds = bounds, UpdateManifold = true, - SearchRoot = owner as InputElement ?? inputElement.GetVisualRoot() as InputElement + SearchRoot = searchRoot }); } diff --git a/src/Avalonia.Base/Input/Navigation/XYFocusHelpers.cs b/src/Avalonia.Base/Input/Navigation/XYFocusHelpers.cs index 1914f6f6c6..7bdcfc8fb9 100644 --- a/src/Avalonia.Base/Input/Navigation/XYFocusHelpers.cs +++ b/src/Avalonia.Base/Input/Navigation/XYFocusHelpers.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.VisualTree; namespace Avalonia.Input; @@ -13,11 +14,25 @@ internal static class XYFocusHelpers { return keyDeviceType switch { - null => true, // programmatic input, allow any subtree. + null => !modes.Equals(XYFocusNavigationModes.Disabled), // programmatic input, allow any subtree except Disabled. KeyDeviceType.Keyboard => modes.HasFlag(XYFocusNavigationModes.Keyboard), KeyDeviceType.Gamepad => modes.HasFlag(XYFocusNavigationModes.Gamepad), KeyDeviceType.Remote => modes.HasFlag(XYFocusNavigationModes.Remote), _ => throw new ArgumentOutOfRangeException(nameof(keyDeviceType), keyDeviceType, null) }; } + + internal static InputElement? FindXYSearchRoot(this InputElement visual, KeyDeviceType? keyDeviceType) + { + InputElement candidate = visual; + InputElement? candidateParent = visual.FindAncestorOfType(); + + while (candidateParent is not null && candidateParent.IsAllowedXYNavigationMode(keyDeviceType)) + { + candidate = candidateParent; + candidateParent = candidate.FindAncestorOfType(); + } + + return candidate; + } } diff --git a/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_XY.cs b/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_XY.cs index 2ea3a51908..fa5286a8d8 100644 --- a/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_XY.cs +++ b/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_XY.cs @@ -418,4 +418,45 @@ public class KeyboardNavigationTests_XY : ScopedTestBase Assert.Equal(candidate, FocusManager.GetFocusManager(window)!.GetFocusedElement()); } + + [Fact] + public void Cannot_Focus_Across_XYFocus_Boundaries() + { + using var _ = UnitTestApplication.Start(TestServices.FocusableWindow); + + var current = new Button() { Height = 20 }; + var candidate = new Button() { Height = 20 }; + var currentParent = new StackPanel + { + [XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled, + Orientation = Orientation.Vertical, + Spacing = 20, + Children = { current } + }; + var candidateParent = new StackPanel + { + [XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled, + Orientation = Orientation.Vertical, + Spacing = 20, + Children = { candidate } + }; + + var grandparent = new StackPanel + { + [XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Disabled, + Orientation = Orientation.Vertical, + Spacing = 20, + Children = { currentParent, candidateParent } + }; + + var window = new Window + { + [XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled, + Content = grandparent, + Height = 300 + }; + window.Show(); + + Assert.Null(KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down)); + } }