Browse Source

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.
pull/16701/head
IanRawley 2 years ago
committed by GitHub
parent
commit
afa60f38de
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs
  2. 17
      src/Avalonia.Base/Input/Navigation/XYFocusHelpers.cs
  3. 41
      tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_XY.cs

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

17
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<InputElement>();
while (candidateParent is not null && candidateParent.IsAllowedXYNavigationMode(keyDeviceType))
{
candidate = candidateParent;
candidateParent = candidate.FindAncestorOfType<InputElement>();
}
return candidate;
}
}

41
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));
}
}

Loading…
Cancel
Save