Browse Source

IsEditable combox box (#18094)

* IsEditable combox box with text bindings

* Update after #18405 api review

* ComboBox tests for IsEditable

* remove unneeded error being throw when combox is editable and doesn't have text binding properties

* Changes after code review

* only do naviagtion check if combo box is editable

* Fix editable ComboBox tab navigation

* combobox code review improvements

---------

Co-authored-by: Julien Lebosquain <[email protected]>
Julian 2 months ago
parent
commit
105ba1aa42

+ 13 - 1
samples/ControlCatalog/Pages/ComboBoxPage.xaml

@@ -124,7 +124,7 @@
                     </ComboBox.SelectionBoxItemTemplate>
                     </ComboBox.SelectionBoxItemTemplate>
                 </ComboBox>
                 </ComboBox>
 
 
-                <ComboBox WrapSelection="{Binding WrapSelection}" ItemsSource="{Binding Values}" >
+                <ComboBox WrapSelection="{Binding WrapSelection}" ItemsSource="{Binding Values}">
                     <ComboBox.ItemTemplate>
                     <ComboBox.ItemTemplate>
                         <DataTemplate>
                         <DataTemplate>
                             <StackPanel Orientation="Horizontal">
                             <StackPanel Orientation="Horizontal">
@@ -134,6 +134,18 @@
                         </DataTemplate>
                         </DataTemplate>
                     </ComboBox.ItemTemplate>
                     </ComboBox.ItemTemplate>
                 </ComboBox>
                 </ComboBox>
+
+              <StackPanel Spacing="5">
+                <ComboBox WrapSelection="{Binding WrapSelection}" PlaceholderText="Editable" 
+                          ItemsSource="{Binding Values}" DisplayMemberBinding="{Binding Name}"
+                          IsEditable="True" Text="{Binding TextValue}"
+                          TextSearch.TextBinding="{Binding SearchText, DataType=viewModels:IdAndName}"
+                          SelectedItem="{Binding SelectedItem}" />
+
+                <TextBlock Text="Editable text is bound to SearchText. Display is bound to Name" />
+                <TextBlock Text="{Binding TextValue, StringFormat=Text Value: {0}}" />
+                <TextBlock Text="{Binding SelectedItem.Name, StringFormat=Selected Item: {0}}" />
+              </StackPanel>
             </WrapPanel>
             </WrapPanel>
         </StackPanel>
         </StackPanel>
     </StackPanel>
     </StackPanel>

+ 14 - 0
samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs

@@ -10,6 +10,8 @@ namespace ControlCatalog.ViewModels
     public class ComboBoxPageViewModel : ViewModelBase
     public class ComboBoxPageViewModel : ViewModelBase
     {
     {
         private bool _wrapSelection;
         private bool _wrapSelection;
+        private string _textValue = string.Empty;
+        private IdAndName? _selectedItem = null;
 
 
         public bool WrapSelection
         public bool WrapSelection
         {
         {
@@ -17,6 +19,18 @@ namespace ControlCatalog.ViewModels
             set => this.RaiseAndSetIfChanged(ref _wrapSelection, value);
             set => this.RaiseAndSetIfChanged(ref _wrapSelection, value);
         }
         }
 
 
+        public string TextValue
+        {
+            get => _textValue;
+            set => this.RaiseAndSetIfChanged(ref _textValue, value);
+        }
+
+        public IdAndName? SelectedItem
+        {
+            get => _selectedItem;
+            set => this.RaiseAndSetIfChanged(ref _selectedItem, value);
+        }
+
         public ObservableCollection<IdAndName> Values { get; set; } = new ObservableCollection<IdAndName>
         public ObservableCollection<IdAndName> Values { get; set; } = new ObservableCollection<IdAndName>
         {
         {
             new IdAndName(){ Id = "Id 1", Name = "Name 1", SearchText = "A" },
             new IdAndName(){ Id = "Id 1", Name = "Name 1", SearchText = "A" },

+ 163 - 3
src/Avalonia.Controls/ComboBox.cs

@@ -5,9 +5,9 @@ using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
 using Avalonia.Controls.Templates;
+using Avalonia.Controls.Utils;
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Input;
-using Avalonia.Interactivity;
 using Avalonia.Layout;
 using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Metadata;
 using Avalonia.Metadata;
@@ -20,6 +20,7 @@ namespace Avalonia.Controls
     /// A drop-down list control.
     /// A drop-down list control.
     /// </summary>
     /// </summary>
     [TemplatePart("PART_Popup", typeof(Popup), IsRequired = true)]
     [TemplatePart("PART_Popup", typeof(Popup), IsRequired = true)]
+    [TemplatePart("PART_EditableTextBox", typeof(TextBox), IsRequired = false)]
     [PseudoClasses(pcDropdownOpen, pcPressed)]
     [PseudoClasses(pcDropdownOpen, pcPressed)]
     public class ComboBox : SelectingItemsControl
     public class ComboBox : SelectingItemsControl
     {
     {
@@ -38,6 +39,12 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<bool> IsDropDownOpenProperty =
         public static readonly StyledProperty<bool> IsDropDownOpenProperty =
             AvaloniaProperty.Register<ComboBox, bool>(nameof(IsDropDownOpen));
             AvaloniaProperty.Register<ComboBox, bool>(nameof(IsDropDownOpen));
 
 
+        /// <summary>
+        /// Defines the <see cref="IsEditable"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsEditableProperty =
+            AvaloniaProperty.Register<ComboBox, bool>(nameof(IsEditable));
+
         /// <summary>
         /// <summary>
         /// Defines the <see cref="MaxDropDownHeight"/> property.
         /// Defines the <see cref="MaxDropDownHeight"/> property.
         /// </summary>
         /// </summary>
@@ -73,7 +80,13 @@ namespace Avalonia.Controls
         /// </summary>
         /// </summary>
         public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
         public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
             ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>();
             ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>();
-        
+
+        /// <summary>
+        /// Defines the <see cref="Text"/> property
+        /// </summary>
+        public static readonly StyledProperty<string?> TextProperty =
+            TextBlock.TextProperty.AddOwner<ComboBox>(new(string.Empty, BindingMode.TwoWay));
+
         /// <summary>
         /// <summary>
         /// Defines the <see cref="SelectionBoxItemTemplate"/> property.
         /// Defines the <see cref="SelectionBoxItemTemplate"/> property.
         /// </summary>
         /// </summary>
@@ -95,6 +108,10 @@ namespace Avalonia.Controls
         private object? _selectionBoxItem;
         private object? _selectionBoxItem;
         private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
         private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
 
 
+        private TextBox? _inputTextBox;
+        private BindingEvaluator<string?>? _textValueBindingEvaluator = null;
+        private bool _skipNextTextChanged = false;
+
         /// <summary>
         /// <summary>
         /// Initializes static members of the <see cref="ComboBox"/> class.
         /// Initializes static members of the <see cref="ComboBox"/> class.
         /// </summary>
         /// </summary>
@@ -124,6 +141,15 @@ namespace Avalonia.Controls
             set => SetValue(IsDropDownOpenProperty, value);
             set => SetValue(IsDropDownOpenProperty, value);
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets a value indicating whether the control is editable
+        /// </summary>
+        public bool IsEditable
+        {
+            get => GetValue(IsEditableProperty);
+            set => SetValue(IsEditableProperty, value);
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the maximum height for the dropdown list.
         /// Gets or sets the maximum height for the dropdown list.
         /// </summary>
         /// </summary>
@@ -188,6 +214,16 @@ namespace Avalonia.Controls
             set => SetValue(SelectionBoxItemTemplateProperty, value);
             set => SetValue(SelectionBoxItemTemplateProperty, value);
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets the text used when <see cref="IsEditable"/> is true.
+        /// Does nothing if not <see cref="IsEditable"/>.
+        /// </summary>
+        public string? Text
+        {
+            get => GetValue(TextProperty);
+            set => SetValue(TextProperty, value);
+        }
+
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {
         {
             base.OnAttachedToVisualTree(e);
             base.OnAttachedToVisualTree(e);
@@ -229,7 +265,7 @@ namespace Avalonia.Controls
                 SetCurrentValue(IsDropDownOpenProperty, false);
                 SetCurrentValue(IsDropDownOpenProperty, false);
                 e.Handled = true;
                 e.Handled = true;
             }
             }
-            else if (!IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space))
+            else if (!IsDropDownOpen && !IsEditable && (e.Key == Key.Enter || e.Key == Key.Space))
             {
             {
                 SetCurrentValue(IsDropDownOpenProperty, true);
                 SetCurrentValue(IsDropDownOpenProperty, true);
                 e.Handled = true;
                 e.Handled = true;
@@ -315,6 +351,15 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         /// <inheritdoc/>
         protected override void OnPointerReleased(PointerReleasedEventArgs e)
         protected override void OnPointerReleased(PointerReleasedEventArgs e)
         {
         {
+            //if the user clicked in the input text we don't want to open the dropdown
+            if (_inputTextBox != null
+                && !e.Handled
+                && e.Source is StyledElement styledSource
+                && styledSource.TemplatedParent == _inputTextBox)
+            {
+                return;
+            }
+
             if (!e.Handled && e.Source is Visual source)
             if (!e.Handled && e.Source is Visual source)
             {
             {
                 if (_popup?.IsInsidePopup(source) == true)
                 if (_popup?.IsInsidePopup(source) == true)
@@ -348,6 +393,8 @@ namespace Avalonia.Controls
             _popup = e.NameScope.Get<Popup>("PART_Popup");
             _popup = e.NameScope.Get<Popup>("PART_Popup");
             _popup.Opened += PopupOpened;
             _popup.Opened += PopupOpened;
             _popup.Closed += PopupClosed;
             _popup.Closed += PopupClosed;
+
+            _inputTextBox = e.NameScope.Find<TextBox>("PART_EditableTextBox");
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
@@ -357,6 +404,7 @@ namespace Avalonia.Controls
             {
             {
                 UpdateSelectionBoxItem(change.NewValue);
                 UpdateSelectionBoxItem(change.NewValue);
                 TryFocusSelectedItem();
                 TryFocusSelectedItem();
+                UpdateInputTextFromSelection(change.NewValue);
             }
             }
             else if (change.Property == IsDropDownOpenProperty)
             else if (change.Property == IsDropDownOpenProperty)
             {
             {
@@ -366,6 +414,30 @@ namespace Avalonia.Controls
             {
             {
                 CoerceValue(SelectionBoxItemTemplateProperty);
                 CoerceValue(SelectionBoxItemTemplateProperty);
             }
             }
+            else if (change.Property == IsEditableProperty && change.GetNewValue<bool>())
+            {
+                UpdateInputTextFromSelection(SelectedItem);
+            }
+            else if (change.Property == TextProperty)
+            {
+                TextChanged(change.GetNewValue<string>());
+            }
+            else if (change.Property == ItemsSourceProperty)
+            {
+                //the base handler deselects the current item (and resets Text) so we want to run the base first, then try match by text
+                string? text = Text;
+                base.OnPropertyChanged(change);
+                SetCurrentValue(TextProperty, text);
+                return;
+            }
+            else if (change.Property == DisplayMemberBindingProperty)
+            {
+                HandleTextValueBindingValueChanged(null, change);
+            }
+            else if (change.Property == TextSearch.TextBindingProperty)
+            {
+                HandleTextValueBindingValueChanged(change, null);
+            }
             base.OnPropertyChanged(change);
             base.OnPropertyChanged(change);
         }
         }
 
 
@@ -374,6 +446,17 @@ namespace Avalonia.Controls
             return new ComboBoxAutomationPeer(this);
             return new ComboBoxAutomationPeer(this);
         }
         }
 
 
+        protected override void OnGotFocus(GotFocusEventArgs e)
+        {
+            if (IsEditable && _inputTextBox != null)
+            {
+                _inputTextBox.Focus();
+                _inputTextBox.SelectAll();
+            }
+
+            base.OnGotFocus(e);
+        }
+
         internal void ItemFocused(ComboBoxItem dropDownItem)
         internal void ItemFocused(ComboBoxItem dropDownItem)
         {
         {
             if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
             if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
@@ -386,6 +469,11 @@ namespace Avalonia.Controls
         {
         {
             _subscriptionsOnOpen.Clear();
             _subscriptionsOnOpen.Clear();
 
 
+            if(IsEditable && CanFocus(this))
+            {
+                Focus();
+            }
+
             DropDownClosed?.Invoke(this, EventArgs.Empty);
             DropDownClosed?.Invoke(this, EventArgs.Empty);
         }
         }
 
 
@@ -502,6 +590,14 @@ namespace Avalonia.Controls
             }
             }
         }
         }
 
 
+        private void UpdateInputTextFromSelection(object? item)
+        {
+            //if we are modifying the text box which has deselected a value we don't want to update the textbox value
+            if (_skipNextTextChanged)
+                return;
+            SetCurrentValue(TextProperty, GetItemTextValue(item));
+        }
+
         private void SelectFocusedItem()
         private void SelectFocusedItem()
         {
         {
             foreach (var dropdownItem in GetRealizedContainers())
             foreach (var dropdownItem in GetRealizedContainers())
@@ -561,5 +657,69 @@ namespace Avalonia.Controls
             SelectedItem = null;
             SelectedItem = null;
             SelectedIndex = -1;
             SelectedIndex = -1;
         }
         }
+
+        private void HandleTextValueBindingValueChanged(AvaloniaPropertyChangedEventArgs? textSearchPropChange,
+            AvaloniaPropertyChangedEventArgs? displayMemberPropChange)
+        {
+            IBinding? textValueBinding;
+            //prioritise using the TextSearch.TextBindingProperty if possible
+            if (textSearchPropChange == null && TextSearch.GetTextBinding(this) is IBinding textSearchBinding)
+                textValueBinding = textSearchBinding;
+
+            else if (textSearchPropChange != null && textSearchPropChange.NewValue is IBinding eventTextSearchBinding)
+                textValueBinding = eventTextSearchBinding;
+
+            else if (displayMemberPropChange != null && displayMemberPropChange.NewValue is IBinding eventDisplayMemberBinding)
+                textValueBinding = eventDisplayMemberBinding;
+
+            else
+                textValueBinding = null;
+
+            if (_textValueBindingEvaluator == null)
+                _textValueBindingEvaluator = BindingEvaluator<string?>.TryCreate(textValueBinding);
+            else if (textValueBinding == null)
+                _textValueBindingEvaluator = null;
+            else
+                _textValueBindingEvaluator.UpdateBinding(textValueBinding);
+
+            //if the binding is set we want to set the initial value for the selected item so the text box has the correct value
+            if (_textValueBindingEvaluator != null)
+                _textValueBindingEvaluator.Value = GetItemTextValue(SelectedValue);
+        }
+
+        private void TextChanged(string? newValue)
+        {
+            if (!IsEditable || _skipNextTextChanged)
+                return;
+
+            int selectedIdx = -1;
+            object? selectedItem = null;
+            int i = -1;
+            foreach (object? item in Items)
+            {
+                i++;
+                string itemText = GetItemTextValue(item);
+                if (string.Equals(newValue, itemText, StringComparison.CurrentCultureIgnoreCase))
+                {
+                    selectedIdx = i;
+                    selectedItem = item;
+                    break;
+                }
+            }
+
+            _skipNextTextChanged = true;
+            try
+            {
+                SelectedIndex = selectedIdx;
+                SelectedItem = selectedItem;
+            }
+            finally
+            {
+                _skipNextTextChanged = false;
+            }
+        }
+
+        private string GetItemTextValue(object? item) 
+            => TextSearch.GetEffectiveText(item, _textValueBindingEvaluator);
     }
     }
 }
 }

+ 10 - 0
src/Avalonia.Controls/Utils/BindingEvaluator.cs

@@ -19,6 +19,15 @@ internal sealed class BindingEvaluator<T> : StyledElement, IDisposable
     public static readonly StyledProperty<T> ValueProperty =
     public static readonly StyledProperty<T> ValueProperty =
         AvaloniaProperty.Register<BindingEvaluator<T>, T>("Value");
         AvaloniaProperty.Register<BindingEvaluator<T>, T>("Value");
 
 
+    /// <summary>
+    /// Gets or sets the data item value.
+    /// </summary>
+    public T Value
+    {
+        get => GetValue(ValueProperty);
+        set => SetValue(ValueProperty, value);
+    }
+
     public T Evaluate(object? dataContext)
     public T Evaluate(object? dataContext)
     {
     {
         // Only update the DataContext if necessary
         // Only update the DataContext if necessary
@@ -49,6 +58,7 @@ internal sealed class BindingEvaluator<T> : StyledElement, IDisposable
         DataContext = null;
         DataContext = null;
     }
     }
 
 
+    [return: NotNullIfNotNull(nameof(binding))]
     public static BindingEvaluator<T>? TryCreate(IBinding? binding)
     public static BindingEvaluator<T>? TryCreate(IBinding? binding)
     {
     {
         if (binding is null)
         if (binding is null)

+ 53 - 3
src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

@@ -16,6 +16,7 @@
           <ComboBoxItem>Item 1</ComboBoxItem>
           <ComboBoxItem>Item 1</ComboBoxItem>
           <ComboBoxItem>Item 2</ComboBoxItem>
           <ComboBoxItem>Item 2</ComboBoxItem>
         </ComboBox>
         </ComboBox>
+        
         <ComboBox PlaceholderText="Error">
         <ComboBox PlaceholderText="Error">
           <DataValidationErrors.Error>
           <DataValidationErrors.Error>
             <sys:Exception>
             <sys:Exception>
@@ -25,6 +26,25 @@
             </sys:Exception>
             </sys:Exception>
           </DataValidationErrors.Error>
           </DataValidationErrors.Error>
         </ComboBox>
         </ComboBox>
+
+        <ComboBox SelectedIndex="1" IsEditable="True">
+          <ComboBoxItem>Item A</ComboBoxItem>
+          <ComboBoxItem>Item b</ComboBoxItem>
+          <ComboBoxItem>Item c</ComboBoxItem>
+        </ComboBox>
+
+        <ComboBox SelectedIndex="0">
+          <ComboBox.SelectionBoxItemTemplate>
+            <DataTemplate>
+              <Border Padding="20" BorderBrush="Red" BorderThickness="1">
+                <TextBlock Text="{ReflectionBinding}"/>
+              </Border>
+            </DataTemplate>
+          </ComboBox.SelectionBoxItemTemplate>
+          <ComboBoxItem>Item A</ComboBoxItem>
+          <ComboBoxItem>Item b</ComboBoxItem>
+          <ComboBoxItem>Item c</ComboBoxItem>
+        </ComboBox>
       </StackPanel>
       </StackPanel>
     </Border>
     </Border>
   </Design.PreviewWith>
   </Design.PreviewWith>
@@ -80,17 +100,42 @@
                        VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                        VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                        Margin="{TemplateBinding Padding}"
                        Margin="{TemplateBinding Padding}"
                        Text="{TemplateBinding PlaceholderText}"
                        Text="{TemplateBinding PlaceholderText}"
-                       Foreground="{TemplateBinding PlaceholderForeground}"
-                       IsVisible="{TemplateBinding SelectionBoxItem, Converter={x:Static ObjectConverters.IsNull}}" />
+                       Foreground="{TemplateBinding PlaceholderForeground}">
+              <TextBlock.IsVisible>
+                <MultiBinding Converter="{x:Static BoolConverters.And}">
+                  <Binding Path="SelectionBoxItem" RelativeSource="{RelativeSource TemplatedParent}" Converter="{x:Static ObjectConverters.IsNull}" />
+                  <Binding Path="!IsEditable" RelativeSource="{RelativeSource TemplatedParent}" />
+                </MultiBinding>
+              </TextBlock.IsVisible>
+            </TextBlock>
             <ContentControl x:Name="ContentPresenter"
             <ContentControl x:Name="ContentPresenter"
                             Content="{TemplateBinding SelectionBoxItem}"
                             Content="{TemplateBinding SelectionBoxItem}"
                             Grid.Column="0"
                             Grid.Column="0"
                             Margin="{TemplateBinding Padding}"
                             Margin="{TemplateBinding Padding}"
                             HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                             HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                             ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
                             ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
-                            VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">
+                            VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
+                            IsVisible="{TemplateBinding IsEditable, Converter={x:Static BoolConverters.Not}}">
             </ContentControl>
             </ContentControl>
 
 
+            <TextBox Name="PART_EditableTextBox"
+                     Grid.Column="0"
+                     Padding="{TemplateBinding Padding}"
+                     HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                     VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
+                     Foreground="{TemplateBinding Foreground}"
+                     Background="Transparent"
+                     Text="{TemplateBinding Text, Mode=TwoWay}"
+                     Watermark="{TemplateBinding PlaceholderText}"
+                     BorderThickness="0"
+                     IsVisible="{TemplateBinding IsEditable}">
+              <TextBox.Resources>
+                <SolidColorBrush x:Key="TextControlBackgroundFocused">Transparent</SolidColorBrush>
+                <SolidColorBrush x:Key="TextControlBackgroundPointerOver">Transparent</SolidColorBrush>
+                <Thickness x:Key="TextControlBorderThemeThicknessFocused">0</Thickness>
+              </TextBox.Resources>
+            </TextBox>
+
             <Border x:Name="DropDownOverlay"
             <Border x:Name="DropDownOverlay"
                     Grid.Column="1"
                     Grid.Column="1"
                     Background="Transparent"
                     Background="Transparent"
@@ -203,6 +248,11 @@
         <Setter Property="Foreground" Value="{DynamicResource ComboBoxDropDownGlyphForegroundDisabled}" />
         <Setter Property="Foreground" Value="{DynamicResource ComboBoxDropDownGlyphForegroundDisabled}" />
       </Style>
       </Style>
     </Style>
     </Style>
+
+    <Style Selector="^[IsEditable=true]">
+      <Setter Property="IsTabStop" Value="False" />
+      <Setter Property="KeyboardNavigation.TabNavigation" Value="Local" />
+    </Style>
   </ControlTheme>
   </ControlTheme>
 
 
 </ResourceDictionary>
 </ResourceDictionary>

+ 33 - 4
src/Avalonia.Themes.Simple/Controls/ComboBox.xaml

@@ -27,15 +27,37 @@
                        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                        VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                        VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                        Foreground="{TemplateBinding PlaceholderForeground}"
                        Foreground="{TemplateBinding PlaceholderForeground}"
-                       IsVisible="{TemplateBinding SelectionBoxItem,
-                                                   Converter={x:Static ObjectConverters.IsNull}}"
-                       Text="{TemplateBinding PlaceholderText}" />
+                       Text="{TemplateBinding PlaceholderText}">
+              <TextBlock.IsVisible>
+                <MultiBinding Converter="{x:Static BoolConverters.And}">
+                  <Binding Path="SelectionBoxItem" RelativeSource="{RelativeSource TemplatedParent}" Converter="{x:Static ObjectConverters.IsNull}" />
+                  <Binding Path="!IsEditable" RelativeSource="{RelativeSource TemplatedParent}" />
+                </MultiBinding>
+              </TextBlock.IsVisible>
+            </TextBlock>
             <ContentControl Margin="{TemplateBinding Padding}"
             <ContentControl Margin="{TemplateBinding Padding}"
                             HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                             HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                             VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                             VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                             Content="{TemplateBinding SelectionBoxItem}"
                             Content="{TemplateBinding SelectionBoxItem}"
-                            ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}">
+                            ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
+                            IsVisible="{TemplateBinding IsEditable, Converter={x:Static BoolConverters.Not}}">
             </ContentControl>
             </ContentControl>
+            <TextBox Name="PART_EditableTextBox"
+                     Grid.Column="0"
+                     Padding="{TemplateBinding Padding}"
+                     HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                     VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
+                     Foreground="{TemplateBinding Foreground}"
+                     Background="Transparent"
+                     Text="{TemplateBinding Text, Mode=TwoWay}"
+                     BorderThickness="0"
+                     IsVisible="{TemplateBinding IsEditable}">
+              <TextBox.Resources>
+                <SolidColorBrush x:Key="TextControlBackgroundFocused">Transparent</SolidColorBrush>
+                <SolidColorBrush x:Key="TextControlBackgroundPointerOver">Transparent</SolidColorBrush>
+                <Thickness x:Key="TextControlBorderThemeThicknessFocused">0</Thickness>
+              </TextBox.Resources>
+            </TextBox>
             <ToggleButton Name="toggle"
             <ToggleButton Name="toggle"
                           Grid.Column="1"
                           Grid.Column="1"
                           Background="Transparent"
                           Background="Transparent"
@@ -75,11 +97,18 @@
         </Border>
         </Border>
       </ControlTemplate>
       </ControlTemplate>
     </Setter>
     </Setter>
+
     <Style Selector="^:pointerover /template/ Border#border">
     <Style Selector="^:pointerover /template/ Border#border">
       <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}" />
       <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}" />
     </Style>
     </Style>
+
     <Style Selector="^:disabled /template/ Border#border">
     <Style Selector="^:disabled /template/ Border#border">
       <Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
       <Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
     </Style>
     </Style>
+
+    <Style Selector="^[IsEditable=true]">
+      <Setter Property="IsTabStop" Value="False" />
+      <Setter Property="KeyboardNavigation.TabNavigation" Value="Local" />
+    </Style>
   </ControlTheme>
   </ControlTheme>
 </ResourceDictionary>
 </ResourceDictionary>

+ 80 - 0
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@@ -209,6 +209,10 @@ namespace Avalonia.Controls.UnitTests
                                     ItemsPanel = new FuncTemplate<Panel>(() => new VirtualizingStackPanel()),
                                     ItemsPanel = new FuncTemplate<Panel>(() => new VirtualizingStackPanel()),
                                 }.RegisterInNameScope(scope)
                                 }.RegisterInNameScope(scope)
                             }.RegisterInNameScope(scope)
                             }.RegisterInNameScope(scope)
+                        }.RegisterInNameScope(scope),
+                        new TextBox
+                        {
+                            Name = "PART_InputText"
                         }.RegisterInNameScope(scope)
                         }.RegisterInNameScope(scope)
                     }
                     }
                 };
                 };
@@ -635,5 +639,81 @@ namespace Avalonia.Controls.UnitTests
         }
         }
 
 
         private sealed record Item(string Value, string Display);
         private sealed record Item(string Value, string Display);
+
+        [Fact]
+        public void When_Editable_Input_Text_Matches_An_Item_It_Is_Selected()
+        {
+            var target = new ComboBox
+            {
+                DisplayMemberBinding = new Binding(),
+                IsEditable = true,
+                ItemsSource = new[] { "foo", "bar" }
+            };
+
+            target.SelectedItem = null;
+            Assert.Null(target.SelectedItem);
+
+            target.Text = "foo";
+            Assert.NotNull(target.SelectedItem);
+            Assert.Equal(target.SelectedItem, "foo");
+        }
+
+        [Fact]
+        public void When_Editable_TextSearch_TextBinding_Is_Prioritised_Over_DisplayMember()
+        {
+            var items = new[]
+            {
+                new Item("Value 1", "Display 1"),
+                new Item("Value 2", "Display 2")
+            };
+            var target = new ComboBox
+            {
+                DisplayMemberBinding = new Binding("Display"),
+                IsEditable = true,
+                ItemsSource = items
+            };
+            TextSearch.SetTextBinding(target, new Binding("Value"));
+
+            target.SelectedItem = null;
+            Assert.Null(target.SelectedItem);
+
+            target.Text = "Value 1";
+            Assert.NotNull(target.SelectedItem);
+            Assert.Equal(target.SelectedItem, items[0]);
+        }
+
+        [Fact]
+        public void When_Items_Source_Changes_It_Selects_An_Item_By_Text()
+        {
+            var items = new[]
+            {
+                new Item("Value 1", "Display 1"),
+                new Item("Value 2", "Display 2")
+            };
+            var items2 = new[]
+            {
+                new Item("Value 1", "Display 3"),
+                new Item("Value 2", "Display 4")
+            };
+            var target = new ComboBox
+            {
+                DisplayMemberBinding = new Binding("Display"),
+                IsEditable = true,
+                ItemsSource = items
+            };
+            TextSearch.SetTextBinding(target, new Binding("Value"));
+
+            target.SelectedItem = null;
+            Assert.Null(target.SelectedItem);
+
+            target.Text = "Value 1";
+            Assert.NotNull(target.SelectedItem);
+            Assert.Equal(target.SelectedItem, items[0]);
+
+            target.ItemsSource = items2;
+            Assert.NotNull(target.SelectedItem);
+            Assert.Equal(target.SelectedItem, items2[0]);
+            Assert.Equal(target.Text, "Value 1");
+        }
     }
     }
 }
 }