diff --git a/src/MainDemo.Wpf/Domain/MainWindowViewModel.cs b/src/MainDemo.Wpf/Domain/MainWindowViewModel.cs index bbb5e3512f..391faf8a57 100644 --- a/src/MainDemo.Wpf/Domain/MainWindowViewModel.cs +++ b/src/MainDemo.Wpf/Domain/MainWindowViewModel.cs @@ -485,4 +485,19 @@ private bool DemoItemsFilter(object obj) && item.Name.IndexOf(searchKeyword, StringComparison.OrdinalIgnoreCase) >= 0; #endif } + + [RelayCommand] + private async Task OpenQuickSearchAsync() + { + var quickSearchDialog = new QuickSearchDialog + { + DataContext = new QuickSearchDialogViewModel(DemoItems) + }; + var result = await DialogHost.Show(quickSearchDialog, "RootDialog"); + if (result is DemoItem selectedItem) + { + SelectedItem = selectedItem; + SelectedIndex = DemoItems.IndexOf(selectedItem); + } + } } diff --git a/src/MainDemo.Wpf/Domain/QuickSearchDialog.xaml b/src/MainDemo.Wpf/Domain/QuickSearchDialog.xaml new file mode 100644 index 0000000000..211e417c7e --- /dev/null +++ b/src/MainDemo.Wpf/Domain/QuickSearchDialog.xaml @@ -0,0 +1,28 @@ + + + + + diff --git a/src/MainDemo.Wpf/Domain/QuickSearchDialog.xaml.cs b/src/MainDemo.Wpf/Domain/QuickSearchDialog.xaml.cs new file mode 100644 index 0000000000..3f4d127ae1 --- /dev/null +++ b/src/MainDemo.Wpf/Domain/QuickSearchDialog.xaml.cs @@ -0,0 +1,22 @@ +using MaterialDesignThemes.Wpf; + +namespace MaterialDesignDemo; + +/// +/// Interaction logic for QuickSearchDialog.xaml +/// +public partial class QuickSearchDialog : UserControl +{ + public QuickSearchDialog() + { + InitializeComponent(); + } + + private void SearchBox_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + DialogHost.Close("RootDialog", null); + } + } +} diff --git a/src/MainDemo.Wpf/Domain/QuickSearchDialogViewModel.cs b/src/MainDemo.Wpf/Domain/QuickSearchDialogViewModel.cs new file mode 100644 index 0000000000..89ed27eea9 --- /dev/null +++ b/src/MainDemo.Wpf/Domain/QuickSearchDialogViewModel.cs @@ -0,0 +1,32 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using MaterialDesignDemo.Shared.Domain; +using MaterialDesignThemes.Wpf; + +namespace MaterialDesignDemo.Domain; + +public partial class QuickSearchDialogViewModel(IReadOnlyCollection items) : ObservableObject +{ + private readonly IReadOnlyCollection _items = items; + + [ObservableProperty] + private string _searchText = ""; + + partial void OnSearchTextChanged(string value) + => OnPropertyChanged(nameof(FilteredItems)); + + [ObservableProperty] + private DemoItem? _selectedItem; + + partial void OnSelectedItemChanged(DemoItem? value) + { + if (value is not null) + { + DialogHost.Close("RootDialog", value); + } + } + + public IReadOnlyCollection FilteredItems + => string.IsNullOrWhiteSpace(SearchText) + ? _items + : _items.Where(i => i.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase)).ToList(); +} diff --git a/src/MainDemo.Wpf/Fields.xaml b/src/MainDemo.Wpf/Fields.xaml index 575919cbee..68481e35ea 100644 --- a/src/MainDemo.Wpf/Fields.xaml +++ b/src/MainDemo.Wpf/Fields.xaml @@ -709,9 +709,11 @@ VerticalAlignment="Center" Style="{StaticResource MaterialDesignSubtitle1TextBlock}" Text="Simple source list" /> + diff --git a/src/MainDemo.Wpf/Home.xaml b/src/MainDemo.Wpf/Home.xaml index 983f7ba0e6..bc72fb81c3 100644 --- a/src/MainDemo.Wpf/Home.xaml +++ b/src/MainDemo.Wpf/Home.xaml @@ -51,7 +51,7 @@ Style="{StaticResource MaterialDesignFlatButton}"> - + diff --git a/src/MainDemo.Wpf/MainWindow.xaml b/src/MainDemo.Wpf/MainWindow.xaml index 9e3f53fb09..4b223aff67 100644 --- a/src/MainDemo.Wpf/MainWindow.xaml +++ b/src/MainDemo.Wpf/MainWindow.xaml @@ -16,6 +16,11 @@ Style="{StaticResource MaterialDesignWindow}" WindowStartupLocation="CenterScreen" mc:Ignorable="d"> + + + diff --git a/src/MaterialDesign3.Demo.Wpf/Domain/MainWindowViewModel.cs b/src/MaterialDesign3.Demo.Wpf/Domain/MainWindowViewModel.cs index 89a9fae832..9096519557 100644 --- a/src/MaterialDesign3.Demo.Wpf/Domain/MainWindowViewModel.cs +++ b/src/MaterialDesign3.Demo.Wpf/Domain/MainWindowViewModel.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Configuration; using System.Windows.Data; +using CommunityToolkit.Mvvm.Input; using MaterialColorUtilities; using MaterialDesignDemo.Shared.Domain; using MaterialDesignThemes.Wpf; @@ -9,7 +10,7 @@ namespace MaterialDesign3Demo.Domain; -public class MainWindowViewModel : ViewModelBase +public partial class MainWindowViewModel : ViewModelBase { public MainWindowViewModel(ISnackbarMessageQueue snackbarMessageQueue, string? startupPage) { @@ -502,4 +503,19 @@ private bool DemoItemsFilter(object obj) return obj is DemoItem item && item.Name.ToLower().Contains(_searchKeyword!.ToLower()); } + + [RelayCommand] + private async Task OpenQuickSearchAsync() + { + var quickSearchDialog = new QuickSearchDialog + { + DataContext = new QuickSearchDialogViewModel(DemoItems) + }; + var result = await DialogHost.Show(quickSearchDialog, "RootDialog"); + if (result is DemoItem selectedItem) + { + SelectedItem = selectedItem; + SelectedIndex = DemoItems.IndexOf(selectedItem); + } + } } diff --git a/src/MaterialDesign3.Demo.Wpf/Domain/QuickSearchDialog.xaml b/src/MaterialDesign3.Demo.Wpf/Domain/QuickSearchDialog.xaml new file mode 100644 index 0000000000..b1ef54f4b4 --- /dev/null +++ b/src/MaterialDesign3.Demo.Wpf/Domain/QuickSearchDialog.xaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/src/MaterialDesign3.Demo.Wpf/Domain/QuickSearchDialog.xaml.cs b/src/MaterialDesign3.Demo.Wpf/Domain/QuickSearchDialog.xaml.cs new file mode 100644 index 0000000000..4982421184 --- /dev/null +++ b/src/MaterialDesign3.Demo.Wpf/Domain/QuickSearchDialog.xaml.cs @@ -0,0 +1,22 @@ +using MaterialDesignThemes.Wpf; + +namespace MaterialDesign3Demo.Domain; + +/// +/// Interaction logic for QuickSearchDialog.xaml +/// +public partial class QuickSearchDialog : UserControl +{ + public QuickSearchDialog() + { + InitializeComponent(); + } + + private void SearchBox_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + DialogHost.Close("RootDialog", null); + } + } +} diff --git a/src/MaterialDesign3.Demo.Wpf/Domain/QuickSearchDialogViewModel.cs b/src/MaterialDesign3.Demo.Wpf/Domain/QuickSearchDialogViewModel.cs new file mode 100644 index 0000000000..66c27f8b3e --- /dev/null +++ b/src/MaterialDesign3.Demo.Wpf/Domain/QuickSearchDialogViewModel.cs @@ -0,0 +1,30 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using MaterialDesignThemes.Wpf; + +namespace MaterialDesign3Demo.Domain; +public partial class QuickSearchDialogViewModel(IReadOnlyCollection items) : ObservableObject +{ + private readonly IReadOnlyCollection _items = items; + + [ObservableProperty] + private string _searchText = ""; + + partial void OnSearchTextChanged(string value) + => OnPropertyChanged(nameof(FilteredItems)); + + [ObservableProperty] + private DemoItem? _selectedItem; + + partial void OnSelectedItemChanged(DemoItem? value) + { + if (value is not null) + { + DialogHost.Close("RootDialog", value); + } + } + + public IReadOnlyCollection FilteredItems + => string.IsNullOrWhiteSpace(SearchText) + ? _items + : _items.Where(i => i.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase)).ToList(); +} diff --git a/src/MaterialDesign3.Demo.Wpf/Home.xaml b/src/MaterialDesign3.Demo.Wpf/Home.xaml index a24c62fceb..168e2a4066 100644 --- a/src/MaterialDesign3.Demo.Wpf/Home.xaml +++ b/src/MaterialDesign3.Demo.Wpf/Home.xaml @@ -51,7 +51,7 @@ Style="{StaticResource MaterialDesignFlatButton}"> - + diff --git a/src/MaterialDesign3.Demo.Wpf/MainWindow.xaml b/src/MaterialDesign3.Demo.Wpf/MainWindow.xaml index cbbb00b325..2c8df26e62 100644 --- a/src/MaterialDesign3.Demo.Wpf/MainWindow.xaml +++ b/src/MaterialDesign3.Demo.Wpf/MainWindow.xaml @@ -17,6 +17,11 @@ Style="{StaticResource MaterialDesignWindow}" WindowStartupLocation="CenterScreen" mc:Ignorable="d"> + + + diff --git a/src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs b/src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs index 9540efd9d2..e4b176b143 100644 --- a/src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs +++ b/src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs @@ -134,18 +134,40 @@ public event RoutedPropertyChangedEventHandler SuggestionChosen remove => RemoveHandler(SuggestionChosenEvent, value); } + public bool ShowSuggestionsOnFocus + { + get => (bool)GetValue(ShowSuggestionsOnFocusProperty); + set => SetValue(ShowSuggestionsOnFocusProperty, value); + } + + public static readonly DependencyProperty ShowSuggestionsOnFocusProperty = + DependencyProperty.Register( + nameof(ShowSuggestionsOnFocus), + typeof(bool), + typeof(AutoSuggestBox), + new PropertyMetadata(false)); + #endregion + protected override void OnGotFocus(RoutedEventArgs e) + { + base.OnGotFocus(e); + + if (ShowSuggestionsOnFocus && + _autoSuggestBoxList is not null && + _autoSuggestBoxList.Items.Count > 0 && + !IsSuggestionOpen) + { + IsSuggestionOpen = true; + } + } static AutoSuggestBox() => DefaultStyleKeyProperty.OverrideMetadata(typeof(AutoSuggestBox), new FrameworkPropertyMetadata(typeof(AutoSuggestBox))); #region Override methods public override void OnApplyTemplate() { - if (_autoSuggestBoxList is not null) - { - _autoSuggestBoxList.PreviewMouseDown -= AutoSuggestionListBox_PreviewMouseDown; - } + _autoSuggestBoxList?.PreviewMouseDown -= AutoSuggestionListBox_PreviewMouseDown; if (GetTemplateChild(AutoSuggestBoxListPart) is ListBox listBox) { @@ -164,27 +186,18 @@ protected override void OnPreviewKeyDown(KeyEventArgs e) { case Key.Down: IncrementSelection(); - e.Handled = true; break; case Key.Up: DecrementSelection(); - e.Handled = true; break; case Key.Enter: CommitValueSelection(); - e.Handled = true; break; case Key.Escape: CloseAutoSuggestionPopUp(); - e.Handled = true; break; case Key.Tab: - bool wasItemSelected = CommitValueSelection(); - // Only mark the event as handled if the SuggestionList is open and therefore the Selection was successful - if (wasItemSelected) - { - e.Handled = true; - } + CommitValueSelection(); break; default: return; @@ -207,10 +220,27 @@ protected override void OnTextChanged(TextChangedEventArgs e) base.OnTextChanged(e); if (_autoSuggestBoxList is null) return; - if ((Text.Length == 0 || _autoSuggestBoxList.Items.Count == 0) && IsSuggestionOpen) - IsSuggestionOpen = false; - else if (Text.Length > 0 && !IsSuggestionOpen && IsFocused && _autoSuggestBoxList.Items.Count > 0) + + bool hasItems = _autoSuggestBoxList.Items.Count > 0; + bool isEmpty = string.IsNullOrEmpty(Text); + + bool shouldOpen = + IsFocused && + hasItems && + (ShowSuggestionsOnFocus || !isEmpty); + + bool shouldClose = + !hasItems || + (!ShowSuggestionsOnFocus && isEmpty); + + if (shouldOpen) + { IsSuggestionOpen = true; + } + else if (shouldClose) + { + IsSuggestionOpen = false; + } } #endregion @@ -329,7 +359,7 @@ private void IncrementSelection() if (_autoSuggestBoxList is null || Suggestions is null) return; ICollectionView collectionView = CollectionViewSource.GetDefaultView(Suggestions); - int itemCount = collectionView.Cast().Count(); + int itemCount = GetItemCount(collectionView); // If we're at the last item, wrap around to the first. if (collectionView.CurrentPosition == itemCount - 1) @@ -339,5 +369,14 @@ private void IncrementSelection() _autoSuggestBoxList.ScrollIntoView(_autoSuggestBoxList.SelectedItem); } + private static int GetItemCount(ICollectionView collectionView) + { + if (collectionView is ListCollectionView lcv) + { + return lcv.Count; + } + return collectionView.Cast().Count(); + } + #endregion } diff --git a/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml b/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml index b658cc2363..8183e8b482 100644 --- a/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml +++ b/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml @@ -262,8 +262,10 @@ Focusable="False" IsOpen="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSuggestionOpen, Mode=TwoWay}" PopupAnimation="Slide" + Placement="Bottom" + PlacementTarget="{Binding ElementName=OuterBorder}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" - StaysOpen="False" + StaysOpen="True" Visibility="Collapsed"> (); + IVisualElement suggestBox = await userControl.GetElement(); + IVisualElement popup = await suggestBox.GetElement(); + + static void SetShowSuggestionsOnFocus(AutoSuggestBox autoSuggestBox, bool value) + { + autoSuggestBox.ShowSuggestionsOnFocus = value; + } + await suggestBox.RemoteExecute(SetShowSuggestionsOnFocus, showSuggestionsOnFocus); + + // Act + await suggestBox.MoveKeyboardFocus(); + await Task.Delay(100, TestContext.Current!.Execution.CancellationToken); + + // Assert + await Assert.That(await suggestBox.GetIsSuggestionOpen()).IsEqualTo(showSuggestionsOnFocus); + await Assert.That(await popup.GetIsOpen()).IsEqualTo(showSuggestionsOnFocus); + } + + [Test] + [Arguments(true)] + [Arguments(false)] + public async Task ShowSuggestionsOnFocus_Mouse_FollowsSetting(bool showSuggestionsOnFocus) + { + // Arrange + IVisualElement userControl = await LoadUserControl(); + IVisualElement suggestBox = await userControl.GetElement(); + IVisualElement popup = await suggestBox.GetElement(); + + static void SetShowSuggestionsOnFocus(AutoSuggestBox autoSuggestBox, bool value) + { + autoSuggestBox.ShowSuggestionsOnFocus = value; + } + await suggestBox.RemoteExecute(SetShowSuggestionsOnFocus, showSuggestionsOnFocus); + + // Act + await suggestBox.LeftClick(); + await Task.Delay(100, TestContext.Current!.Execution.CancellationToken); + + // Assert + await Assert.That(await suggestBox.GetIsSuggestionOpen()).IsEqualTo(showSuggestionsOnFocus); + await Assert.That(await popup.GetIsOpen()).IsEqualTo(showSuggestionsOnFocus); + } + private static async Task AssertExists(IVisualElement suggestionListBox, string text, bool existsOrNotCheck = true) { try