diff --git a/src/MainDemo.Wpf/Domain/FieldsViewModel.cs b/src/MainDemo.Wpf/Domain/FieldsViewModel.cs index 805ad8f07f..38f1a7e515 100644 --- a/src/MainDemo.Wpf/Domain/FieldsViewModel.cs +++ b/src/MainDemo.Wpf/Domain/FieldsViewModel.cs @@ -1,60 +1,35 @@ using System.Collections.ObjectModel; using System.Windows.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using MaterialDesignDemo.Shared.Domain; namespace MaterialDesignDemo.Domain; -public class FieldsViewModel : ViewModelBase +public partial class FieldsViewModel : ObservableObject { - private string? _name; - private string? _name2; - private string? _password1 = string.Empty; - private string? _password2 = "pre-filled"; private string? _password1Validated = "pre-filled"; private string? _password2Validated = "pre-filled"; - private string? _text1; - private string? _text2; - private ObservableCollection? _autoSuggestBox1Suggestions; - private string? _autoSuggestBox1Text; private readonly List? _originalAutoSuggestBox1Suggestions; - private ObservableCollection>? _autoSuggestBox2Suggestions; - private string? _autoSuggestBox2Text; private readonly List>? _originalAutoSuggestBox2Suggestions; + private readonly List _originalAutoSuggestBox3Suggestions; - public string? Name - { - get => _name; - set => SetProperty(ref _name, value); - } + [ObservableProperty] + private string? _name; - public string? Name2 - { - get => _name2; - set => SetProperty(ref _name2, value); - } + [ObservableProperty] + private string? _name2; - public string? Text1 - { - get => _text1; - set => SetProperty(ref _text1, value); - } + [ObservableProperty] + private string? _text1; - public string? Text2 - { - get => _text2; - set => SetProperty(ref _text2, value); - } + [ObservableProperty] + private string? _text2; - public string? Password1 - { - get => _password1; - set => SetProperty(ref _password1, value); - } + [ObservableProperty] + private string? _password1 = string.Empty; - public string? Password2 - { - get => _password2; - set => SetProperty(ref _password2, value); - } + [ObservableProperty] + private string? _password2 = "pre-filled"; public string? Password1Validated { @@ -80,43 +55,64 @@ public string? Password2Validated public FieldsTestObject TestObject => new() { Name = "Mr. Test" }; - public ObservableCollection? AutoSuggestBox1Suggestions + [ObservableProperty] + private ObservableCollection? _autoSuggestBox1Suggestions; + + [ObservableProperty] + private ObservableCollection>? _autoSuggestBox2Suggestions; + + [ObservableProperty] + private List? _autoSuggestBox3Suggestions; + + + [ObservableProperty] + private string? _autoSuggestBox1Text; + + partial void OnAutoSuggestBox1TextChanged(string? value) { - get => _autoSuggestBox1Suggestions; - set => SetProperty(ref _autoSuggestBox1Suggestions, value); + if (_originalAutoSuggestBox1Suggestions != null && value != null) + { + var searchResult = _originalAutoSuggestBox1Suggestions.Where(x => IsMatch(x, value)); + AutoSuggestBox1Suggestions = new(searchResult); + } } - public ObservableCollection>? AutoSuggestBox2Suggestions + [ObservableProperty] + private string? _autoSuggestBox2Text; + + partial void OnAutoSuggestBox2TextChanged(string? value) { - get => _autoSuggestBox2Suggestions; - set => SetProperty(ref _autoSuggestBox2Suggestions, value); + if (_originalAutoSuggestBox2Suggestions != null && value != null) + { + var searchResult = _originalAutoSuggestBox2Suggestions.Where(x => IsMatch(x.Key, value)); + AutoSuggestBox2Suggestions = new(searchResult); + } } - public string? AutoSuggestBox1Text + [ObservableProperty] + private string? _autoSuggestBox3Text; + + partial void OnAutoSuggestBox3TextChanged(string? value) { - get => _autoSuggestBox1Text; - set + if (value is not null) { - if (SetProperty(ref _autoSuggestBox1Text, value) && - _originalAutoSuggestBox1Suggestions != null && value != null) - { - var searchResult = _originalAutoSuggestBox1Suggestions.Where(x => IsMatch(x, value)); - AutoSuggestBox1Suggestions = new ObservableCollection(searchResult); - } + var searchResult = _originalAutoSuggestBox3Suggestions.Where(x => IsMatch(x, value)); + AutoSuggestBox3Suggestions = new(searchResult); } } - public string? AutoSuggestBox2Text + [RelayCommand] + private void RemoveAutoSuggestBox3Suggestion(string suggestion) { - get => _autoSuggestBox2Text; - set + _originalAutoSuggestBox3Suggestions.Remove(suggestion); + if (string.IsNullOrEmpty(AutoSuggestBox3Text)) + { + AutoSuggestBox3Suggestions = new(_originalAutoSuggestBox3Suggestions); + } + else { - if (SetProperty(ref _autoSuggestBox2Text, value) && - _originalAutoSuggestBox2Suggestions != null && value != null) - { - var searchResult = _originalAutoSuggestBox2Suggestions.Where(x => IsMatch(x.Key, value)); - AutoSuggestBox2Suggestions = new ObservableCollection>(searchResult); - } + var searchResult = _originalAutoSuggestBox3Suggestions.Where(x => IsMatch(x, AutoSuggestBox3Text!)); + AutoSuggestBox3Suggestions = new(searchResult); } } @@ -128,12 +124,16 @@ public FieldsViewModel() SetPassword1FromViewModelCommand = new AnotherCommandImplementation(_ => Password1 = "Set from ViewModel!"); SetPassword2FromViewModelCommand = new AnotherCommandImplementation(_ => Password2 = "Set from ViewModel!"); - _originalAutoSuggestBox1Suggestions = new List() - { + _originalAutoSuggestBox1Suggestions = + [ "Burger", "Fries", "Shake", "Lettuce" - }; + ]; - _originalAutoSuggestBox2Suggestions = new List>(GetColors()); + _originalAutoSuggestBox2Suggestions = new(GetColors()); + _originalAutoSuggestBox3Suggestions = + [ + "jsmith", "jdoe", "mscott", "pparker", "bwilliams", "ljohnson", "abrown", "dlee", "cmiller", "tmoore" + ]; AutoSuggestBox1Suggestions = new ObservableCollection(_originalAutoSuggestBox1Suggestions); } @@ -158,20 +158,10 @@ private static IEnumerable> GetColors() } } -public class FieldsTestObject : ViewModelBase +public partial class FieldsTestObject : ObservableObject { + [ObservableProperty] private string? _name; + [ObservableProperty] private string? _content; - - public string? Name - { - get => _name; - set => SetProperty(ref _name, value); - } - - public string? Content - { - get => _content; - set => SetProperty(ref _content, value); - } } diff --git a/src/MainDemo.Wpf/Fields.xaml b/src/MainDemo.Wpf/Fields.xaml index 432e141ac0..d8a4a38311 100644 --- a/src/MainDemo.Wpf/Fields.xaml +++ b/src/MainDemo.Wpf/Fields.xaml @@ -692,36 +692,36 @@ + VerticalAlignment="Center" + Style="{StaticResource MaterialDesignSubtitle1TextBlock}" + Text="Simple source list" /> + Suggestions="{Binding AutoSuggestBox1Suggestions}" + Text="{Binding AutoSuggestBox1Text, UpdateSourceTrigger=PropertyChanged}" /> + Style="{StaticResource MaterialDesignSubtitle1TextBlock}" + Text="AutoSuggestBox with ItemTemplate" /> + materialDesign:HintAssist.Hint="Color" + materialDesign:TextFieldAssist.HasClearButton="True" + DropDownElevation="Dp0" + Suggestions="{Binding AutoSuggestBox2Suggestions}" + Text="{Binding AutoSuggestBox2Text, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" + ValueMember="Key"> + Height="20" + Background="{Binding Value, Converter={StaticResource ColorToBrushConverter}}" + CornerRadius="10" /> @@ -733,23 +733,23 @@ + Style="{StaticResource MaterialDesignSubtitle1TextBlock}" + Text="Filled AutoSuggestBox" /> + materialDesign:TextFieldAssist.HasClearButton="True" + DropDownElevation="Dp0" + Style="{StaticResource MaterialDesignFilledAutoSuggestBox}" + Suggestions="{Binding AutoSuggestBox2Suggestions}" + Text="{Binding AutoSuggestBox2Text, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" + ValueMember="Key"> + Height="20" + Background="{Binding Value, Converter={StaticResource ColorToBrushConverter}}" + CornerRadius="10" /> @@ -760,23 +760,23 @@ + Style="{StaticResource MaterialDesignSubtitle1TextBlock}" + Text="Outlined AutoSuggestBox" /> + materialDesign:TextFieldAssist.HasClearButton="True" + DropDownElevation="Dp0" + Style="{StaticResource MaterialDesignOutlinedAutoSuggestBox}" + Suggestions="{Binding AutoSuggestBox2Suggestions}" + Text="{Binding AutoSuggestBox2Text, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" + ValueMember="Key"> + Height="20" + Background="{Binding Value, Converter={StaticResource ColorToBrushConverter}}" + CornerRadius="10" /> @@ -784,6 +784,50 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs b/src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs index 71d028d35e..9540efd9d2 100644 --- a/src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs +++ b/src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs @@ -1,6 +1,7 @@ using System.Collections; using System.ComponentModel; using System.Windows.Data; +using System.Windows.Documents; using System.Windows.Media; namespace MaterialDesignThemes.Wpf; @@ -8,6 +9,15 @@ namespace MaterialDesignThemes.Wpf; [TemplatePart(Name = AutoSuggestBoxListPart, Type = typeof(ListBox))] public class AutoSuggestBox : TextBox { + public static bool? GetIsInteractiveElement(DependencyObject obj) + => (bool?)obj.GetValue(IsInteractiveElementProperty); + + public static void SetIsInteractiveElement(DependencyObject obj, bool? value) + => obj.SetValue(IsInteractiveElementProperty, value); + + public static readonly DependencyProperty IsInteractiveElementProperty = + DependencyProperty.RegisterAttached("IsInteractiveElement", typeof(bool?), typeof(AutoSuggestBox), new PropertyMetadata(null)); + private const string AutoSuggestBoxListPart = "PART_AutoSuggestBoxList"; protected ListBox? _autoSuggestBoxList; @@ -185,6 +195,11 @@ protected override void OnPreviewKeyDown(KeyEventArgs e) protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); + if (_autoSuggestBoxList is { } list && + (list.IsKeyboardFocused || list.IsKeyboardFocusWithin)) + { + return; + } CloseAutoSuggestionPopUp(); } protected override void OnTextChanged(TextChangedEventArgs e) @@ -207,6 +222,12 @@ private void AutoSuggestionListBox_PreviewMouseDown(object sender, MouseButtonEv if (_autoSuggestBoxList is null || e.OriginalSource is not FrameworkElement element) return; + // If the user clicked on an interactive element, let it handle the event. + if (IsInteractiveElement(element)) + { + return; + } + var selectedItem = element.DataContext; if (!_autoSuggestBoxList.Items.Contains(selectedItem)) return; @@ -236,6 +257,27 @@ void OnSelectionChanged(object s, SelectionChangedEventArgs args) #endregion #region Methods + private bool IsInteractiveElement(DependencyObject? element) + { + return element.GetVisualAncestry() + .Where(x => x != this) + .Select(IsInteractive) + .Where(x => x is not null) + .FirstOrDefault() ?? false; + + static bool? IsInteractive(DependencyObject element) + { + if (GetIsInteractiveElement(element) is bool isInteractiveElement) + { + return isInteractiveElement; + } + if (element is ButtonBase or TextBoxBase or ComboBox or Hyperlink) + { + return true; + } + return null; + } + } private void CloseAutoSuggestionPopUp() { diff --git a/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml b/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml index ca15834f6b..9a3bc6c955 100644 --- a/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml +++ b/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml @@ -273,6 +273,8 @@ Style="{StaticResource MaterialDesignElevatedCard}"> + + + + + + + + + + + + + + + + + + diff --git a/tests/MaterialDesignThemes.UITests/Samples/AutoSuggestBoxes/AutoSuggestTextBoxWithInteractiveTemplate.xaml.cs b/tests/MaterialDesignThemes.UITests/Samples/AutoSuggestBoxes/AutoSuggestTextBoxWithInteractiveTemplate.xaml.cs new file mode 100644 index 0000000000..3c18ec6aca --- /dev/null +++ b/tests/MaterialDesignThemes.UITests/Samples/AutoSuggestBoxes/AutoSuggestTextBoxWithInteractiveTemplate.xaml.cs @@ -0,0 +1,73 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace MaterialDesignThemes.UITests.Samples.AutoSuggestBoxes; + +/// +/// Interaction logic for AutoSuggestTextBoxWithInteractiveTemplate.xaml +/// +public partial class AutoSuggestTextBoxWithInteractiveTemplate : UserControl +{ + public AutoSuggestTextBoxWithInteractiveTemplate() + { + DataContext = new AutoSuggestTextBoxWithInteractiveTemplateViewModel(); + InitializeComponent(); + } +} + +public partial class AutoSuggestTextBoxWithInteractiveTemplateViewModel : ObservableObject +{ + private readonly List _baseSuggestions; + + [ObservableProperty] + private List _suggestions = []; + + [ObservableProperty] + private string? _autoSuggestText; + + partial void OnAutoSuggestTextChanged(string? value) + { + if (!string.IsNullOrWhiteSpace(value)) + { + var searchResult = _baseSuggestions.Where(x => IsMatch(x.Name, value)); + Suggestions = new(searchResult); + } + else + { + Suggestions = new(_baseSuggestions); + } + } + + public AutoSuggestTextBoxWithInteractiveTemplateViewModel() + { + _baseSuggestions = + [ + new("Apples"), + new("Bananas"), + new("Beans"), + new("Mtn Dew"), + new("Orange") + ]; + Suggestions = [.. _baseSuggestions]; + } + + private static bool IsMatch(string item, string currentText) + { +#if NET6_0_OR_GREATER + return item.Contains(currentText, StringComparison.OrdinalIgnoreCase); +#else + return item.IndexOf(currentText, StringComparison.OrdinalIgnoreCase) >= 0; +#endif + } +} + +public partial class SuggestionThing2(string name) : ObservableObject +{ + public string Name { get; } = name; + + [ObservableProperty] + private int _count = 0; + + [RelayCommand] + private void IncrementCount() => Count++; +} diff --git a/tests/MaterialDesignThemes.UITests/WPF/AutoSuggestBoxes/AutoSuggestTextBoxTests.cs b/tests/MaterialDesignThemes.UITests/WPF/AutoSuggestBoxes/AutoSuggestTextBoxTests.cs index f1302b56bf..bf6945b564 100644 --- a/tests/MaterialDesignThemes.UITests/WPF/AutoSuggestBoxes/AutoSuggestTextBoxTests.cs +++ b/tests/MaterialDesignThemes.UITests/WPF/AutoSuggestBoxes/AutoSuggestTextBoxTests.cs @@ -8,7 +8,7 @@ public class AutoSuggestBoxTests : TestBase { public AutoSuggestBoxTests() { - AttachedDebuggerToRemoteProcess = false; + AttachedDebuggerToRemoteProcess = true; } [Test] @@ -232,6 +232,114 @@ static async Task AssertViewModelProperty(AutoSuggestBox autoSuggestBox) recorder.Success(); } + [Test] + public async Task AutoSuggestBox_ClickingButtonInInteractiveItemTemplate_DoesNotSelectOrClosePopup() + { + await using var recorder = new TestRecorder(App); + + // Arrange + IVisualElement suggestBox = (await LoadUserControl()).As(); + IVisualElement popup = await suggestBox.GetElement(); + IVisualElement suggestionListBox = await popup.GetElement(); + + // Act + await suggestBox.MoveKeyboardFocus(); + await suggestBox.SendInput(new KeyboardInput("a")); + await Task.Delay(500, TestContext.Current!.CancellationToken); + + // Find the button in the first suggestion item + var thirdListBoxItem = await suggestionListBox.GetElement("/ListBoxItem[2]"); + var button = await thirdListBoxItem.GetElement