Browse Source

Fix AutoCompleteBox popup not closing on focus lost (#21749)

* Add failing test for AutoCompleteBox

* Fix AutoCompleteBox popup not closing on focus lost
pull/21750/head
Julien Lebosquain 3 months ago
committed by GitHub
parent
commit
984b6428c6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 108
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs
  2. 34
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

108
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs

@ -212,8 +212,6 @@ namespace Avalonia.Controls
private bool _itemTemplateIsFromValueMemberBinding = true;
private bool _settingItemTemplateFromValueMemberBinding;
private bool _isFocused = false;
private string? _searchText = string.Empty;
private readonly EventHandler _populateDropDownHandler;
@ -751,28 +749,34 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Provides handling for the
/// <see cref="E:Avalonia.Controls.Control.GotFocus" /> event.
/// </summary>
/// <param name="e">A <see cref="T:RoutedEventArgs" />
/// that contains the event data.</param>
/// <inheritdoc/>
protected override void OnGotFocus(FocusChangedEventArgs e)
{
if (TextBox is not null && TextBoxSelectionLength <= 0)
{
TextBox.Focus();
TextBox.SelectAll();
}
base.OnGotFocus(e);
FocusChanged(HasFocus());
}
/// <summary>
/// Provides handling for the
/// <see cref="E:Avalonia.Controls.Control.LostFocus" /> event.
/// </summary>
/// <param name="e">A <see cref="T:RoutedEventArgs" />
/// that contains the event data.</param>
/// <inheritdoc/>
protected override void OnLostFocus(FocusChangedEventArgs e)
{
SetCurrentValue(IsDropDownOpenProperty, false);
_userCalledPopulate = false;
var textBoxContextMenuIsOpen = TextBox?.ContextFlyout?.IsOpen == true || TextBox?.ContextMenu?.IsOpen == true;
var contextMenuIsOpen = ContextFlyout?.IsOpen == true || ContextMenu?.IsOpen == true;
if (!textBoxContextMenuIsOpen && !contextMenuIsOpen && ClearSelectionOnLostFocus)
{
ClearTextBoxSelection();
}
base.OnLostFocus(e);
FocusChanged(HasFocus());
}
protected override AutomationPeer OnCreateAutomationPeer() => new AutoCompleteBoxAutomationPeer(this);
@ -785,79 +789,9 @@ namespace Avalonia.Controls
/// <returns>true to indicate the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus;
/// otherwise, false.</returns>
[Obsolete($"Use {nameof(IsKeyboardFocusWithin)} instead.")]
protected bool HasFocus() => IsKeyboardFocusWithin;
/// <summary>
/// Handles the FocusChanged event.
/// </summary>
/// <param name="hasFocus">A value indicating whether the control
/// currently has the focus.</param>
private void FocusChanged(bool hasFocus)
{
// The OnGotFocus & OnLostFocus are asynchronously and cannot
// reliably tell you that have the focus. All they do is let you
// know that the focus changed sometime in the past. To determine
// if you currently have the focus you need to do consult the
// FocusManager (see HasFocus()).
bool wasFocused = _isFocused;
_isFocused = hasFocus;
if (hasFocus)
{
if (!wasFocused && TextBox != null && TextBoxSelectionLength <= 0)
{
TextBox.Focus();
TextBox.SelectAll();
}
}
else
{
// Check if we still have focus in the parent's focus scope
if (GetFocusScope() is { } scope &&
(FocusManager.GetFocusManager(this)?.GetFocusedElement(scope) is not { } focused ||
(focused != this &&
(focused is Visual v && !this.IsVisualAncestorOf(v)))))
{
SetCurrentValue(IsDropDownOpenProperty, false);
}
_userCalledPopulate = false;
var textBoxContextMenuIsOpen = TextBox?.ContextFlyout?.IsOpen == true || TextBox?.ContextMenu?.IsOpen == true;
var contextMenuIsOpen = ContextFlyout?.IsOpen == true || ContextMenu?.IsOpen == true;
if (!textBoxContextMenuIsOpen && !contextMenuIsOpen && ClearSelectionOnLostFocus)
{
ClearTextBoxSelection();
}
}
_isFocused = hasFocus;
IFocusScope? GetFocusScope()
{
IInputElement? c = this;
while (c != null)
{
if (c is IFocusScope scope &&
c is Visual v &&
v.VisualRoot is Visual root &&
root.IsVisible)
{
return scope;
}
c = (c as Visual)?.GetVisualParent<IInputElement>() ??
((c as IHostedVisualTreeRoot)?.Host as IInputElement);
}
return null;
}
}
/// <summary>
/// Occurs asynchronously when the text in the <see cref="TextBox"/> portion of the
/// <see cref="AutoCompleteBox" /> changes.

34
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@ -584,6 +584,40 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Losing_Focus_Closes_DropDown()
{
using var app = UnitTestApplication.Start(FocusServices);
var target1 = CreateControl();
target1.ItemsSource = CreateSimpleStringArray();
var textBox1 = GetTextBox(target1);
var target2 = CreateControl();
target1.ApplyTemplate();
target2.ApplyTemplate();
_ = new TestRoot
{
Child = new StackPanel
{
Children = { target1, target2 }
}
};
target1.Focus();
textBox1.Text = "a";
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.True(target1.IsDropDownOpen);
target2.Focus();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.False(target1.IsFocused);
Assert.False(target1.IsDropDownOpen);
}
/// <summary>
/// Retrieves a defined predicate filter through a new AutoCompleteBox
/// control instance.

Loading…
Cancel
Save