diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index ca267dc5e9..85e460a25f 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -1,7 +1,7 @@ using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Text; +using Avalonia.Input; +using Avalonia.Input.Raw; using Avalonia.Layout; #nullable enable @@ -20,12 +20,19 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty PlacementProperty = AvaloniaProperty.Register(nameof(Placement)); + public static readonly DirectProperty ShowModeProperty = + AvaloniaProperty.RegisterDirect(nameof(ShowMode), + x => x.ShowMode, (x, v) => x.ShowMode = v); + public static readonly AttachedProperty AttachedFlyoutProperty = AvaloniaProperty.RegisterAttached("AttachedFlyout", null); private bool _isOpen; private Control? _target; protected Popup? _popup; + private FlyoutShowMode _showMode = FlyoutShowMode.Standard; + Rect? enlargedPopupRect; + IDisposable? transientDisposable; public bool IsOpen { @@ -39,6 +46,12 @@ namespace Avalonia.Controls.Primitives set => SetValue(PlacementProperty, value); } + public FlyoutShowMode ShowMode + { + get => _showMode; + set => SetAndRaise(ShowModeProperty, ref _showMode, value); + } + public Control? Target { get => _target; @@ -96,7 +109,11 @@ namespace Avalonia.Controls.Primitives } IsOpen = _popup.IsOpen = false; - + + // Ensure this isn't active + transientDisposable?.Dispose(); + transientDisposable = null; + OnClosed(); } @@ -142,6 +159,72 @@ namespace Avalonia.Controls.Primitives PositionPopup(showAtPointer); IsOpen = _popup.IsOpen = true; OnOpened(); + + if (ShowMode == FlyoutShowMode.Standard) + { + // Try and focus content inside Flyout + if (_popup.Child.Focusable) + { + FocusManager.Instance?.Focus(_popup.Child); + } + else + { + var nextFocus = KeyboardNavigationHandler.GetNext(_popup.Child, NavigationDirection.Next); + if (nextFocus != null) + { + FocusManager.Instance?.Focus(nextFocus); + } + } + } + else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway) + { + transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss); + } + } + + private void HandleTransientDismiss(RawInputEventArgs args) + { + if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move) + { + if (enlargedPopupRect == null) + { + if (_popup?.Host is PopupRoot root) + { + var tmp = root.Bounds.Inflate(100); + var scPt = root.PointToScreen(tmp.TopLeft); + enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height); + } + else if (_popup?.Host is OverlayPopupHost host) + { + // Overlay popups are in Window client coordinates, just use that + enlargedPopupRect = host.Bounds.Inflate(100); + } + + return; + } + + if (_popup?.Host is PopupRoot) + { + var pt = pArgs.Root.PointToScreen(pArgs.Position); + if (!enlargedPopupRect?.Contains(new Point(pt.X, pt.Y)) ?? false) + { + Hide(false); + enlargedPopupRect = null; + transientDisposable?.Dispose(); + transientDisposable = null; + } + } + else if (_popup?.Host is OverlayPopupHost) + { + if (!enlargedPopupRect?.Contains(pArgs.Position) ?? false) + { + Hide(false); + enlargedPopupRect = null; + transientDisposable?.Dispose(); + transientDisposable = null; + } + } + } } protected virtual void OnOpening() diff --git a/src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs b/src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs new file mode 100644 index 0000000000..cb5b5f00c0 --- /dev/null +++ b/src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs @@ -0,0 +1,24 @@ +namespace Avalonia.Controls +{ + // Note: FlyoutShowMode.Auto was removed. MS Docs just say: + // The show mode is determined automatically based on the method used to show the flyout. + // and AFAICT Flyouts generally open with "Standard" behavior + + public enum FlyoutShowMode + { + /// + /// Behavior is typical of a flyout shown reactively, like a context menu. The open flyout takes focus. For a CommandBarFlyout, it opens in it's expanded state. + /// + Standard, + + /// + /// Behavior is typical of a flyout shown proactively. The open flyout does not take focus. For a CommandBarFlyout, it opens in it's collapsed state. + /// + Transient, + + /// + /// The flyout exhibits Transient behavior while the cursor is close to it, but is dismissed when the cursor moves away. + /// + TransientWithDismissOnPointerMoveAway + } +}