Browse Source

Merge pull request #4974 from AvaloniaUI/feature/keyboard-focus-within

Implement IsKeyboardFocusWithin
pull/4998/head
Max Katz 6 years ago
committed by GitHub
parent
commit
bb89d8d480
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/Avalonia.Input/ApiCompatBaseline.txt
  2. 5
      src/Avalonia.Input/IInputElement.cs
  3. 22
      src/Avalonia.Input/InputElement.cs
  4. 117
      src/Avalonia.Input/KeyboardDevice.cs
  5. 197
      tests/Avalonia.Input.UnitTests/InputElement_Focus.cs

4
src/Avalonia.Input/ApiCompatBaseline.txt

@ -0,0 +1,4 @@
Compat issues with assembly Avalonia.Input:
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Input.IInputElement.IsKeyboardFocusWithin' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Input.IInputElement.IsKeyboardFocusWithin.get()' is present in the implementation but not in the contract.
Total Issues: 2

5
src/Avalonia.Input/IInputElement.cs

@ -89,6 +89,11 @@ namespace Avalonia.Input
/// <see cref="IsEnabled"/> value of this control and its parent controls. /// <see cref="IsEnabled"/> value of this control and its parent controls.
/// </remarks> /// </remarks>
bool IsEffectivelyEnabled { get; } bool IsEffectivelyEnabled { get; }
/// <summary>
/// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements.
/// </summary>
bool IsKeyboardFocusWithin { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the control is focused. /// Gets a value indicating whether the control is focused.

22
src/Avalonia.Input/InputElement.cs

@ -42,6 +42,14 @@ namespace Avalonia.Input
public static readonly StyledProperty<Cursor?> CursorProperty = public static readonly StyledProperty<Cursor?> CursorProperty =
AvaloniaProperty.Register<InputElement, Cursor?>(nameof(Cursor), null, true); AvaloniaProperty.Register<InputElement, Cursor?>(nameof(Cursor), null, true);
/// <summary>
/// Defines the <see cref="IsKeyboardFocusWithin"/> property.
/// </summary>
public static readonly DirectProperty<InputElement, bool> IsKeyboardFocusWithinProperty =
AvaloniaProperty.RegisterDirect<InputElement, bool>(
nameof(IsKeyboardFocusWithin),
o => o.IsKeyboardFocusWithin);
/// <summary> /// <summary>
/// Defines the <see cref="IsFocused"/> property. /// Defines the <see cref="IsFocused"/> property.
/// </summary> /// </summary>
@ -160,6 +168,7 @@ namespace Avalonia.Input
private bool _isEffectivelyEnabled = true; private bool _isEffectivelyEnabled = true;
private bool _isFocused; private bool _isFocused;
private bool _isKeyboardFocusWithin;
private bool _isFocusVisible; private bool _isFocusVisible;
private bool _isPointerOver; private bool _isPointerOver;
private GestureRecognizerCollection? _gestureRecognizers; private GestureRecognizerCollection? _gestureRecognizers;
@ -343,6 +352,15 @@ namespace Avalonia.Input
get { return GetValue(CursorProperty); } get { return GetValue(CursorProperty); }
set { SetValue(CursorProperty, value); } set { SetValue(CursorProperty, value); }
} }
/// <summary>
/// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements.
/// </summary>
public bool IsKeyboardFocusWithin
{
get => _isKeyboardFocusWithin;
internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value);
}
/// <summary> /// <summary>
/// Gets a value indicating whether the control is focused. /// Gets a value indicating whether the control is focused.
@ -544,6 +562,10 @@ namespace Avalonia.Input
{ {
UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault<bool>()); UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault<bool>());
} }
else if (change.Property == IsKeyboardFocusWithinProperty)
{
PseudoClasses.Set(":focus-within", _isKeyboardFocusWithin);
}
} }
/// <summary> /// <summary>

117
src/Avalonia.Input/KeyboardDevice.cs

@ -9,6 +9,7 @@ namespace Avalonia.Input
public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged
{ {
private IInputElement? _focusedElement; private IInputElement? _focusedElement;
private IInputRoot? _focusedRoot;
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
@ -28,10 +29,115 @@ namespace Avalonia.Input
private set private set
{ {
_focusedElement = value; _focusedElement = value;
if (_focusedElement != null && _focusedElement.IsAttachedToVisualTree)
{
_focusedRoot = _focusedElement.VisualRoot as IInputRoot;
}
else
{
_focusedRoot = null;
}
RaisePropertyChanged(); RaisePropertyChanged();
} }
} }
private void ClearFocusWithinAncestors(IInputElement? element)
{
var el = element;
while (el != null)
{
if (el is InputElement ie)
{
ie.IsKeyboardFocusWithin = false;
}
el = (IInputElement)el.VisualParent;
}
}
private void ClearFocusWithin(IInputElement element, bool clearRoot)
{
foreach (var visual in element.VisualChildren)
{
if (visual is IInputElement el && el.IsKeyboardFocusWithin)
{
ClearFocusWithin(el, true);
break;
}
}
if (clearRoot)
{
if (element is InputElement ie)
{
ie.IsKeyboardFocusWithin = false;
}
}
}
private void SetIsFocusWithin(IInputElement? oldElement, IInputElement? newElement)
{
if (newElement == null && oldElement != null)
{
ClearFocusWithinAncestors(oldElement);
return;
}
IInputElement? branch = null;
var el = newElement;
while (el != null)
{
if (el.IsKeyboardFocusWithin)
{
branch = el;
break;
}
el = el.VisualParent as IInputElement;
}
el = oldElement;
if (el != null && branch != null)
{
ClearFocusWithin(branch, false);
}
el = newElement;
while (el != null && el != branch)
{
if (el is InputElement ie)
{
ie.IsKeyboardFocusWithin = true;
}
el = el.VisualParent as IInputElement;
}
}
private void ClearChildrenFocusWithin(IInputElement element, bool clearRoot)
{
foreach (var visual in element.VisualChildren)
{
if (visual is IInputElement el && el.IsKeyboardFocusWithin)
{
ClearChildrenFocusWithin(el, true);
break;
}
}
if (clearRoot && element is InputElement ie)
{
ie.IsKeyboardFocusWithin = false;
}
}
public void SetFocusedElement( public void SetFocusedElement(
IInputElement? element, IInputElement? element,
NavigationMethod method, NavigationMethod method,
@ -40,6 +146,17 @@ namespace Avalonia.Input
if (element != FocusedElement) if (element != FocusedElement)
{ {
var interactive = FocusedElement as IInteractive; var interactive = FocusedElement as IInteractive;
if (FocusedElement != null &&
(!FocusedElement.IsAttachedToVisualTree ||
_focusedRoot != element?.VisualRoot as IInputRoot) &&
_focusedRoot != null)
{
ClearChildrenFocusWithin(_focusedRoot, true);
}
SetIsFocusWithin(FocusedElement, element);
FocusedElement = element; FocusedElement = element;
interactive?.RaiseEvent(new RoutedEventArgs interactive?.RaiseEvent(new RoutedEventArgs

197
tests/Avalonia.Input.UnitTests/InputElement_Focus.cs

@ -121,5 +121,202 @@ namespace Avalonia.Input.UnitTests
Assert.False(target2.Classes.Contains(":focus-visible")); Assert.False(target2.Classes.Contains(":focus-visible"));
} }
} }
[Fact]
public void Control_FocusWithin_PseudoClass_Should_Be_Applied()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
target1,
target2
}
}
};
target1.ApplyTemplate();
target2.ApplyTemplate();
FocusManager.Instance?.Focus(target1);
Assert.True(target1.IsFocused);
Assert.True(target1.Classes.Contains(":focus-within"));
Assert.True(target1.IsKeyboardFocusWithin);
Assert.True(root.Child.Classes.Contains(":focus-within"));
Assert.True(root.Child.IsKeyboardFocusWithin);
Assert.True(root.Classes.Contains(":focus-within"));
Assert.True(root.IsKeyboardFocusWithin);
}
}
[Fact]
public void Control_FocusWithin_PseudoClass_Should_Be_Applied_and_Removed()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var panel1 = new Panel { Children = { target1 } };
var panel2 = new Panel { Children = { target2 } };
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
panel1,
panel2
}
}
};
target1.ApplyTemplate();
target2.ApplyTemplate();
FocusManager.Instance?.Focus(target1);
Assert.True(target1.IsFocused);
Assert.True(target1.Classes.Contains(":focus-within"));
Assert.True(target1.IsKeyboardFocusWithin);
Assert.True(panel1.Classes.Contains(":focus-within"));
Assert.True(panel1.IsKeyboardFocusWithin);
Assert.True(root.Child.Classes.Contains(":focus-within"));
Assert.True(root.Child.IsKeyboardFocusWithin);
Assert.True(root.Classes.Contains(":focus-within"));
Assert.True(root.IsKeyboardFocusWithin);
FocusManager.Instance?.Focus(target2);
Assert.False(target1.IsFocused);
Assert.False(target1.Classes.Contains(":focus-within"));
Assert.False(target1.IsKeyboardFocusWithin);
Assert.False(panel1.Classes.Contains(":focus-within"));
Assert.False(panel1.IsKeyboardFocusWithin);
Assert.True(root.Child.Classes.Contains(":focus-within"));
Assert.True(root.Child.IsKeyboardFocusWithin);
Assert.True(root.Classes.Contains(":focus-within"));
Assert.True(root.IsKeyboardFocusWithin);
Assert.True(target2.IsFocused);
Assert.True(target2.Classes.Contains(":focus-within"));
Assert.True(target2.IsKeyboardFocusWithin);
Assert.True(panel2.Classes.Contains(":focus-within"));
Assert.True(panel2.IsKeyboardFocusWithin);
}
}
[Fact]
public void Control_FocusWithin_Pseudoclass_Should_Be_Removed_When_Removed_From_Tree()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
target1,
target2
}
}
};
target1.ApplyTemplate();
target2.ApplyTemplate();
FocusManager.Instance?.Focus(target1);
Assert.True(target1.IsFocused);
Assert.True(target1.Classes.Contains(":focus-within"));
Assert.True(target1.IsKeyboardFocusWithin);
Assert.True(root.Child.Classes.Contains(":focus-within"));
Assert.True(root.Child.IsKeyboardFocusWithin);
Assert.True(root.Classes.Contains(":focus-within"));
Assert.True(root.IsKeyboardFocusWithin);
Assert.Equal(KeyboardDevice.Instance.FocusedElement, target1);
root.Child = null;
Assert.Null(KeyboardDevice.Instance.FocusedElement);
Assert.False(target1.IsFocused);
Assert.False(target1.Classes.Contains(":focus-within"));
Assert.False(target1.IsKeyboardFocusWithin);
Assert.False(root.Classes.Contains(":focus-within"));
Assert.False(root.IsKeyboardFocusWithin);
}
}
[Fact]
public void Control_FocusWithin_Pseudoclass_Should_Be_Removed_Focus_Moves_To_Different_Root()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var root1 = new TestRoot
{
Child = new StackPanel
{
Children =
{
target1,
}
}
};
var root2 = new TestRoot
{
Child = new StackPanel
{
Children =
{
target2,
}
}
};
target1.ApplyTemplate();
target2.ApplyTemplate();
FocusManager.Instance?.Focus(target1);
Assert.True(target1.IsFocused);
Assert.True(target1.Classes.Contains(":focus-within"));
Assert.True(target1.IsKeyboardFocusWithin);
Assert.True(root1.Child.Classes.Contains(":focus-within"));
Assert.True(root1.Child.IsKeyboardFocusWithin);
Assert.True(root1.Classes.Contains(":focus-within"));
Assert.True(root1.IsKeyboardFocusWithin);
Assert.Equal(KeyboardDevice.Instance.FocusedElement, target1);
FocusManager.Instance?.Focus(target2);
Assert.False(target1.IsFocused);
Assert.False(target1.Classes.Contains(":focus-within"));
Assert.False(target1.IsKeyboardFocusWithin);
Assert.False(root1.Child.Classes.Contains(":focus-within"));
Assert.False(root1.Child.IsKeyboardFocusWithin);
Assert.False(root1.Classes.Contains(":focus-within"));
Assert.False(root1.IsKeyboardFocusWithin);
Assert.True(target2.IsFocused);
Assert.True(target2.Classes.Contains(":focus-within"));
Assert.True(target2.IsKeyboardFocusWithin);
Assert.True(root2.Child.Classes.Contains(":focus-within"));
Assert.True(root2.Child.IsKeyboardFocusWithin);
Assert.True(root2.Classes.Contains(":focus-within"));
Assert.True(root2.IsKeyboardFocusWithin);
}
}
} }
} }

Loading…
Cancel
Save