diff --git a/readme.md b/readme.md
index f8bbc02d07..96c7937559 100644
--- a/readme.md
+++ b/readme.md
@@ -5,7 +5,7 @@
## 📖 About
-Avalonia is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, MacOs. Avalonia is mature and production ready. We also have in beta release support for iOS, Andriod and in early stages support for browser via WASM.
+Avalonia is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, MacOs. Avalonia is mature and production ready. We also have in beta release support for iOS, Android and in early stages support for browser via WASM.

diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml
index decbd763a1..64e80a8e11 100644
--- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml
@@ -1,77 +1,95 @@
-
-
- A drop-down list.
+
+
+ A drop-down list.
-
-
-
-
-
- Inline Items
- Inline Item 2
- Inline Item 3
- Inline Item 4
-
+
+
+
+
+
-
-
-
-
- Hello
- World
-
-
-
-
-
-
-
-
-
-
-
+
+ Inline Items
+ Inline Item 2
+ Inline Item 3
+ Inline Item 4
+
-
-
-
-
- Control Items
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Hello
+ World
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- Inline Items
- Inline Item 2
- Inline Item 3
- Inline Item 4
-
-
-
-
-
+
+
+
+
+ Control Items
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+ Inline Items
+ Inline Item 2
+ Inline Item 3
+ Inline Item 4
+
+
+
+
+
+
+ WrapSelection
+
+
+
diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
index d50b051d9f..d304bf227d 100644
--- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
@@ -2,6 +2,7 @@ using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
+using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
@@ -10,6 +11,7 @@ namespace ControlCatalog.Pages
public ComboBoxPage()
{
this.InitializeComponent();
+ DataContext = new ComboBoxPageViewModel();
}
private void InitializeComponent()
diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml
index 41658329df..433592345a 100644
--- a/samples/ControlCatalog/Pages/ListBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml
@@ -21,6 +21,7 @@
Toggle
AlwaysSelected
AutoScrollToSelectedItem
+ WrapSelection
@@ -30,6 +31,7 @@
+ SelectionMode="{Binding SelectionMode^}"
+ WrapSelection="{Binding WrapSelection}"/>
diff --git a/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs
new file mode 100644
index 0000000000..bbe970afd6
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive;
+using Avalonia.Controls;
+using Avalonia.Controls.Selection;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+ public class ComboBoxPageViewModel : ViewModelBase
+ {
+ private bool _wrapSelection;
+
+ public bool WrapSelection
+ {
+ get => _wrapSelection;
+ set => this.RaiseAndSetIfChanged(ref _wrapSelection, value);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
index 7f2d6e9572..59489ebcc0 100644
--- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
@@ -14,6 +14,7 @@ namespace ControlCatalog.ViewModels
private bool _toggle;
private bool _alwaysSelected;
private bool _autoScrollToSelectedItem = true;
+ private bool _wrapSelection;
private int _counter;
private IObservable _selectionMode;
@@ -85,6 +86,12 @@ namespace ControlCatalog.ViewModels
set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value);
}
+ public bool WrapSelection
+ {
+ get => _wrapSelection;
+ set => this.RaiseAndSetIfChanged(ref _wrapSelection, value);
+ }
+
public MiniCommand AddItemCommand { get; }
public MiniCommand RemoveItemCommand { get; }
public MiniCommand SelectRandomItemCommand { get; }
diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs
index e9eca97e13..72b09b7a3c 100644
--- a/src/Avalonia.Controls/ComboBox.cs
+++ b/src/Avalonia.Controls/ComboBox.cs
@@ -10,9 +10,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
-using Avalonia.LogicalTree;
using Avalonia.Media;
-using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls
@@ -91,7 +89,7 @@ namespace Avalonia.Controls
{
ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
FocusableProperty.OverrideDefaultValue(true);
- SelectedItemProperty.Changed.AddClassHandler((x,e) => x.SelectedItemChanged(e));
+ SelectedItemProperty.Changed.AddClassHandler((x, e) => x.SelectedItemChanged(e));
KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel);
IsTextSearchEnabledProperty.OverrideDefaultValue(true);
}
@@ -221,8 +219,9 @@ namespace Avalonia.Controls
e.Handled = true;
}
}
+ // This part of code is needed just to acquire initial focus, subsequent focus navigation will be done by ItemsControl.
else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 &&
- (e.Key == Key.Up || e.Key == Key.Down))
+ (e.Key == Key.Up || e.Key == Key.Down) && IsFocused == true)
{
var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c));
if (firstChild != null)
@@ -430,7 +429,18 @@ namespace Avalonia.Controls
int next = SelectedIndex + 1;
if (next >= ItemCount)
- next = 0;
+ {
+ if (WrapSelection == true)
+ {
+ next = 0;
+ }
+ else
+ {
+ return;
+ }
+ }
+
+
SelectedIndex = next;
}
@@ -440,7 +450,16 @@ namespace Avalonia.Controls
int prev = SelectedIndex - 1;
if (prev < 0)
- prev = ItemCount - 1;
+ {
+ if (WrapSelection == true)
+ {
+ prev = ItemCount - 1;
+ }
+ else
+ {
+ return;
+ }
+ }
SelectedIndex = prev;
}
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 10e12a1ae0..ed8f9efb2e 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -143,6 +143,8 @@ namespace Avalonia.Controls
protected set;
}
+ private protected bool WrapFocus { get; set; }
+
event EventHandler? IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
@@ -315,7 +317,7 @@ namespace Avalonia.Controls
{
if (current.VisualParent == container && current is IInputElement inputElement)
{
- var next = GetNextControl(container, direction.Value, inputElement, false);
+ var next = GetNextControl(container, direction.Value, inputElement, WrapFocus);
if (next != null)
{
diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
index a34e5d6438..361febf305 100644
--- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
@@ -233,11 +233,6 @@ namespace Avalonia.Controls.Presenters
var itemIndex = generator.IndexFromContainer(from);
var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical;
- if (itemIndex == -1)
- {
- return null;
- }
-
var newItemIndex = -1;
switch (direction)
@@ -250,6 +245,16 @@ namespace Avalonia.Controls.Presenters
newItemIndex = ItemCount - 1;
break;
+ default:
+ if (itemIndex == -1)
+ {
+ return null;
+ }
+ break;
+ }
+
+ switch (direction)
+ {
case NavigationDirection.Up:
if (vertical)
{
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index 840a5ac1dc..b4cfd9404c 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -114,6 +114,12 @@ namespace Avalonia.Controls.Primitives
"SelectionChanged",
RoutingStrategies.Bubble);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty WrapSelectionProperty =
+ AvaloniaProperty.Register(nameof(WrapSelection), defaultValue: false);
+
private static readonly IList Empty = Array.Empty