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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with
63 additions and
2 deletions
src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs
src/Avalonia.Base/Input/Navigation/XYFocusHelpers.cs
tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_XY.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 InputElemen t
SearchRoot = searchRoo t
} ) ;
}
@ -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 ;
}
}
@ -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 = 2 0 } ;
var candidate = new Button ( ) { Height = 2 0 } ;
var currentParent = new StackPanel
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes . Enabled ,
Orientation = Orientation . Vertical ,
Spacing = 2 0 ,
Children = { current }
} ;
var candidateParent = new StackPanel
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes . Enabled ,
Orientation = Orientation . Vertical ,
Spacing = 2 0 ,
Children = { candidate }
} ;
var grandparent = new StackPanel
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes . Disabled ,
Orientation = Orientation . Vertical ,
Spacing = 2 0 ,
Children = { currentParent , candidateParent }
} ;
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes . Enabled ,
Content = grandparent ,
Height = 3 0 0
} ;
window . Show ( ) ;
Assert . Null ( KeyboardNavigationHandler . GetNext ( current , NavigationDirection . Down ) ) ;
}
}