diff --git a/src/Avalonia.Input/ApiCompatBaseline.txt b/src/Avalonia.Input/ApiCompatBaseline.txt new file mode 100644 index 0000000000..d960664c1b --- /dev/null +++ b/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 diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index 12fec82368..7aa9c32bca 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -89,6 +89,11 @@ namespace Avalonia.Input /// value of this control and its parent controls. /// bool IsEffectivelyEnabled { get; } + + /// + /// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements. + /// + bool IsKeyboardFocusWithin { get; } /// /// Gets a value indicating whether the control is focused. diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 9ace7fd92d..1db7db6ba2 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -42,6 +42,14 @@ namespace Avalonia.Input public static readonly StyledProperty CursorProperty = AvaloniaProperty.Register(nameof(Cursor), null, true); + /// + /// Defines the property. + /// + public static readonly DirectProperty IsKeyboardFocusWithinProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsKeyboardFocusWithin), + o => o.IsKeyboardFocusWithin); + /// /// Defines the property. /// @@ -160,6 +168,7 @@ namespace Avalonia.Input private bool _isEffectivelyEnabled = true; private bool _isFocused; + private bool _isKeyboardFocusWithin; private bool _isFocusVisible; private bool _isPointerOver; private GestureRecognizerCollection? _gestureRecognizers; @@ -343,6 +352,15 @@ namespace Avalonia.Input get { return GetValue(CursorProperty); } set { SetValue(CursorProperty, value); } } + + /// + /// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements. + /// + public bool IsKeyboardFocusWithin + { + get => _isKeyboardFocusWithin; + internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value); + } /// /// Gets a value indicating whether the control is focused. @@ -423,7 +441,7 @@ namespace Avalonia.Input base.OnAttachedToVisualTreeCore(e); UpdateIsEffectivelyEnabled(); } - + /// /// Called before the event occurs. /// @@ -544,6 +562,10 @@ namespace Avalonia.Input { UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); } + else if (change.Property == IsKeyboardFocusWithinProperty) + { + PseudoClasses.Set(":focus-within", _isKeyboardFocusWithin); + } } /// diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 187670a26b..758a398f3a 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -31,6 +31,74 @@ namespace Avalonia.Input RaisePropertyChanged(); } } + + private void ClearFocusWithin(IInputElement element, bool clearRoot) + { + foreach (IInputElement el in element.VisualChildren) + { + if (el.IsKeyboardFocusWithin) + { + ClearFocusWithin(el, true); + break; + } + } + + if(clearRoot) + { + if (element is InputElement ie) + { + ie.IsKeyboardFocusWithin = false; + } + } + } + + private void SetIsFocusWithin(InputElement oldElement, InputElement newElement) + { + InputElement? branch = null; + + InputElement el = newElement; + + while (el != null) + { + if (el.IsKeyboardFocusWithin) + { + branch = el; + break; + } + + if ((el as IInputElement).VisualParent is InputElement ie) + { + el = ie; + } + else + { + break; + } + } + + el = oldElement; + + if (el != null && branch != null) + { + ClearFocusWithin(branch, false); + } + + el = newElement; + + while (el != null && el != branch) + { + el.IsKeyboardFocusWithin = true; + + if ((el as IInputElement).VisualParent is InputElement ie) + { + el = ie; + } + else + { + break; + } + } + } public void SetFocusedElement( IInputElement? element, @@ -40,6 +108,9 @@ namespace Avalonia.Input if (element != FocusedElement) { var interactive = FocusedElement as IInteractive; + + SetIsFocusWithin(FocusedElement as InputElement, element as InputElement); + FocusedElement = element; interactive?.RaiseEvent(new RoutedEventArgs