Skip to content

Commit 16bacff

Browse files
committed
Adds IsInteractiveElement attached property
Introduces an `IsInteractiveElement` attached dependency property to `AutoSuggestBox`, updates `OnLostFocus` to prevent popup closure when the suggestion list is focused, refactors interactivity detection to prioritize the new attached property and perform visual tree traversal, and adds new UI tests to validate this functionality.
1 parent 0cb9ebf commit 16bacff

File tree

4 files changed

+110
-28
lines changed

4 files changed

+110
-28
lines changed

src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ namespace MaterialDesignThemes.Wpf;
99
[TemplatePart(Name = AutoSuggestBoxListPart, Type = typeof(ListBox))]
1010
public class AutoSuggestBox : TextBox
1111
{
12+
public static bool? GetIsInteractiveElement(DependencyObject obj)
13+
=> (bool?)obj.GetValue(IsInteractiveElementProperty);
14+
15+
public static void SetIsInteractiveElement(DependencyObject obj, bool? value)
16+
=> obj.SetValue(IsInteractiveElementProperty, value);
17+
18+
public static readonly DependencyProperty IsInteractiveElementProperty =
19+
DependencyProperty.RegisterAttached("IsInteractiveElement", typeof(bool?), typeof(AutoSuggestBox), new PropertyMetadata(null));
20+
1221
private const string AutoSuggestBoxListPart = "PART_AutoSuggestBoxList";
1322

1423
protected ListBox? _autoSuggestBoxList;
@@ -186,6 +195,11 @@ protected override void OnPreviewKeyDown(KeyEventArgs e)
186195
protected override void OnLostFocus(RoutedEventArgs e)
187196
{
188197
base.OnLostFocus(e);
198+
if (_autoSuggestBoxList is { } list &&
199+
(list.IsKeyboardFocused || list.IsKeyboardFocusWithin))
200+
{
201+
return;
202+
}
189203
CloseAutoSuggestionPopUp();
190204
}
191205
protected override void OnTextChanged(TextChangedEventArgs e)
@@ -209,8 +223,10 @@ private void AutoSuggestionListBox_PreviewMouseDown(object sender, MouseButtonEv
209223
return;
210224

211225
// If the user clicked on an interactive element, let it handle the event.
212-
if (IsInteractiveElement(e.OriginalSource as DependencyObject))
226+
if (IsInteractiveElement(element))
227+
{
213228
return;
229+
}
214230

215231
var selectedItem = element.DataContext;
216232
if (!_autoSuggestBoxList.Items.Contains(selectedItem))
@@ -241,18 +257,28 @@ void OnSelectionChanged(object s, SelectionChangedEventArgs args)
241257
#endregion
242258

243259
#region Methods
244-
private static bool IsInteractiveElement(DependencyObject? element)
260+
private bool IsInteractiveElement(DependencyObject? element)
245261
{
246-
while (element is not null)
262+
return element.GetVisualAncestry()
263+
.Where(x => x != this)
264+
.Select(IsInteractive)
265+
.Where(x => x is not null)
266+
.FirstOrDefault() ?? false;
267+
268+
static bool? IsInteractive(DependencyObject element)
247269
{
270+
if (GetIsInteractiveElement(element) is bool isInteractiveElement)
271+
{
272+
return isInteractiveElement;
273+
}
248274
if (element is ButtonBase or TextBoxBase or ComboBox or Hyperlink)
249275
{
250276
return true;
251277
}
252-
element = VisualTreeHelper.GetParent(element);
278+
return null;
253279
}
254-
return false;
255280
}
281+
256282
private void CloseAutoSuggestionPopUp()
257283
{
258284
IsSuggestionOpen = false;

tests/MaterialDesignThemes.UITests/Samples/AutoSuggestBoxes/AutoSuggestTextBoxWithInteractiveTemplate.xaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
<ColumnDefinition Width="auto" />
2121
</Grid.ColumnDefinitions>
2222
<StackPanel Grid.Column="0" Orientation="Horizontal">
23-
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" />
23+
<TextBlock VerticalAlignment="Center" Text="{Binding Name}"
24+
Background="Transparent"
25+
x:Name="NameTextblock"/>
2426
<TextBlock Margin="8,0"
2527
VerticalAlignment="Center"
2628
Text="{Binding Count}" />

tests/MaterialDesignThemes.UITests/Samples/AutoSuggestBoxes/AutoSuggestTextBoxWithInteractiveTemplate.xaml.cs

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Collections.ObjectModel;
4-
using System.Linq;
5-
using System.Text;
6-
using System.Threading.Tasks;
7-
using System.Windows;
8-
using System.Windows.Controls;
9-
using System.Windows.Data;
10-
using System.Windows.Documents;
11-
using System.Windows.Input;
12-
using System.Windows.Media;
13-
using System.Windows.Media.Imaging;
14-
using System.Windows.Navigation;
15-
using System.Windows.Shapes;
16-
using CommunityToolkit.Mvvm.ComponentModel;
1+
using CommunityToolkit.Mvvm.ComponentModel;
172
using CommunityToolkit.Mvvm.Input;
183

194
namespace MaterialDesignThemes.UITests.Samples.AutoSuggestBoxes;
@@ -32,7 +17,7 @@ public AutoSuggestTextBoxWithInteractiveTemplate()
3217

3318
public partial class AutoSuggestTextBoxWithInteractiveTemplateViewModel : ObservableObject
3419
{
35-
private List<SuggestionThing2> _baseSuggestions;
20+
private readonly List<SuggestionThing2> _baseSuggestions;
3621

3722
[ObservableProperty]
3823
private List<SuggestionThing2> _suggestions = [];
@@ -63,7 +48,7 @@ public AutoSuggestTextBoxWithInteractiveTemplateViewModel()
6348
new("Mtn Dew"),
6449
new("Orange")
6550
];
66-
Suggestions = new(_baseSuggestions);
51+
Suggestions = [.. _baseSuggestions];
6752
}
6853

6954
private static bool IsMatch(string item, string currentText)

tests/MaterialDesignThemes.UITests/WPF/AutoSuggestBoxes/AutoSuggestTextBoxTests.cs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class AutoSuggestBoxTests : TestBase
88
{
99
public AutoSuggestBoxTests()
1010
{
11-
AttachedDebuggerToRemoteProcess = false;
11+
AttachedDebuggerToRemoteProcess = true;
1212
}
1313

1414
[Test]
@@ -245,16 +245,15 @@ public async Task AutoSuggestBox_ClickingButtonInInteractiveItemTemplate_DoesNot
245245
// Act
246246
await suggestBox.MoveKeyboardFocus();
247247
await suggestBox.SendInput(new KeyboardInput("a"));
248-
await Task.Delay(50, TestContext.Current!.CancellationToken);
248+
await Task.Delay(500, TestContext.Current!.CancellationToken);
249249

250250
// Find the button in the first suggestion item
251251
var thirdListBoxItem = await suggestionListBox.GetElement<ListBoxItem>("/ListBoxItem[2]");
252252
var button = await thirdListBoxItem.GetElement<Button>();
253253

254254
// Click the button
255-
await button.MoveCursorTo();
256255
await button.LeftClick();
257-
await Task.Delay(50, TestContext.Current!.CancellationToken);
256+
await Task.Delay(500, TestContext.Current!.CancellationToken);
258257

259258
// Assert
260259
await Assert.That(await suggestBox.GetIsSuggestionOpen()).IsTrue();
@@ -271,6 +270,76 @@ static async Task AssertViewModelProperty(AutoSuggestBox autoSuggestBox)
271270
recorder.Success();
272271
}
273272

273+
[Test]
274+
public async Task AutoSuggestBox_ClickingButtonForcingNonInteractive_SelectsItemAndClosesPopup()
275+
{
276+
await using var recorder = new TestRecorder(App);
277+
278+
// Arrange
279+
IVisualElement<AutoSuggestBox> suggestBox = (await LoadUserControl<AutoSuggestTextBoxWithInteractiveTemplate>()).As<AutoSuggestBox>();
280+
IVisualElement<Popup> popup = await suggestBox.GetElement<Popup>();
281+
IVisualElement<ListBox> suggestionListBox = await popup.GetElement<ListBox>();
282+
283+
// Act
284+
await suggestBox.MoveKeyboardFocus();
285+
await suggestBox.SendInput(new KeyboardInput("a"));
286+
await Task.Delay(500, TestContext.Current!.CancellationToken);
287+
288+
// Find the button in the first suggestion item
289+
var thirdListBoxItem = await suggestionListBox.GetElement<ListBoxItem>("/ListBoxItem[2]");
290+
var button = await thirdListBoxItem.GetElement<Button>();
291+
292+
static void SetNonInteractive(Button button)
293+
=> AutoSuggestBox.SetIsInteractiveElement(button, false);
294+
await button.RemoteExecute(SetNonInteractive);
295+
296+
// Click the button
297+
await button.LeftClick();
298+
await Task.Delay(500, TestContext.Current!.CancellationToken);
299+
300+
// Assert
301+
await Assert.That(await suggestBox.GetIsSuggestionOpen()).IsFalse();
302+
303+
recorder.Success();
304+
}
305+
306+
[Test]
307+
public async Task AutoSuggestBox_ClickingTextblockThatIsInteractive_DoesNotSelectOrClosePopup()
308+
{
309+
await using var recorder = new TestRecorder(App);
310+
311+
// Arrange
312+
IVisualElement<AutoSuggestBox> suggestBox = (await LoadUserControl<AutoSuggestTextBoxWithInteractiveTemplate>()).As<AutoSuggestBox>();
313+
IVisualElement<Popup> popup = await suggestBox.GetElement<Popup>();
314+
IVisualElement<ListBox> suggestionListBox = await popup.GetElement<ListBox>();
315+
316+
// Act
317+
await suggestBox.MoveKeyboardFocus();
318+
await suggestBox.SendInput(new KeyboardInput("a"));
319+
await Task.Delay(500, TestContext.Current!.CancellationToken);
320+
321+
// Find the button in the first suggestion item
322+
var thirdListBoxItem = await suggestionListBox.GetElement<ListBoxItem>("/ListBoxItem[2]");
323+
var textBlock = await thirdListBoxItem.GetElement<TextBlock>("NameTextblock");
324+
325+
static void SetInteractive(TextBlock textBlock)
326+
=> AutoSuggestBox.SetIsInteractiveElement(textBlock, true);
327+
await textBlock.RemoteExecute(SetInteractive);
328+
329+
// Click the button
330+
await textBlock.LeftClick();
331+
await Task.Delay(500, TestContext.Current!.CancellationToken);
332+
333+
// Assert
334+
await Assert.That(await suggestBox.GetIsSuggestionOpen()).IsTrue();
335+
336+
// The list box item should selected because the TextBlock does not handle the click
337+
int selectedIndex = await suggestionListBox.GetSelectedIndex();
338+
await Assert.That(selectedIndex).IsEqualTo(2);
339+
340+
recorder.Success();
341+
}
342+
274343
private static async Task AssertExists(IVisualElement<ListBox> suggestionListBox, string text, bool existsOrNotCheck = true)
275344
{
276345
try

0 commit comments

Comments
 (0)