diff --git a/doc/controls/ChipAndChipGroup.md b/doc/controls/ChipAndChipGroup.md
index b2534dd79..5af87d7cd 100644
--- a/doc/controls/ChipAndChipGroup.md
+++ b/doc/controls/ChipAndChipGroup.md
@@ -6,9 +6,10 @@
## Summary
`Chip` is a control that can be used for selection, filtering, or performing an action from a list.
`ChipGroup` is a container that can house a collection of `Chip`s.
+Alternatively, `Chip` can also be used within the data-template of an `ItemsRepeater`.
## Chip
-`Chip` is derived from `ToggleButton`, a control that a user can select (check) or clear (uncheck).
+`Chip` is derived from `ToggleButton`, a control that a user can select (check) or deselect (uncheck).
### C#
```csharp
@@ -181,3 +182,25 @@ xmlns:utu="using:Uno.Toolkit.UI"
SelectionMode="Multiple"
Style="{StaticResource SuggestionChipGroupStyle}" />
```
+
+## Chip with ItemsRepeater
+
+`Chip` can also be used within the data-template of `ItemsRepeater`, as shown below:
+
+```xml
+xmlns:utu="using:Uno.Toolkit.UI"
+xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
+
+
+
+
+
+
+
+
+```
+
+### Remarks
+- For `ChipExtensions.ChipSelectionMode`, refer to the `ChipGroup.SelectionMode` property, as they function exactly in the same way. Except for the default value which is `SingleOrNone`.
+- The `Chip` must be the root-level element of the data-template.
diff --git a/src/Uno.Toolkit.RuntimeTests/Helpers/XamlHelper.cs b/src/Uno.Toolkit.RuntimeTests/Helpers/XamlHelper.cs
new file mode 100644
index 000000000..a7133c5a6
--- /dev/null
+++ b/src/Uno.Toolkit.RuntimeTests/Helpers/XamlHelper.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Uno.Toolkit.RuntimeTests.Helpers
+{
+ internal static class XamlHelper
+ {
+ ///
+ /// XamlReader.Load the xaml and type-check result.
+ ///
+ /// Xaml with single or double quots
+ /// The default xmlns to inject; use null to not inject one.
+ public static T LoadXaml(string sanitizedXaml, string? defaultXmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation") where T : class =>
+ LoadXaml(sanitizedXaml, new Dictionary { [string.Empty] = defaultXmlns });
+
+ ///
+ /// XamlReader.Load the xaml and type-check result.
+ ///
+ /// Xaml with single or double quots
+ /// Xmlns to inject; use string.Empty for the default xmlns' key
+ public static T LoadXaml(string xaml, Dictionary xmlnses) where T : class
+ {
+ var injection = " " + string.Join(" ", xmlnses
+ .Where(x => x.Value != null)
+ .Select(x => $"xmlns{(string.IsNullOrEmpty(x.Key) ? "" : $":{x.Key}")}=\"{x.Value}\"")
+ );
+
+ xaml = new Regex(@"(?=\\?>)").Replace(xaml, injection, 1);
+
+ var result = Windows.UI.Xaml.Markup.XamlReader.Load(xaml);
+ Assert.IsNotNull(result, "XamlReader.Load returned null");
+ Assert.IsInstanceOfType(result, typeof(T), "XamlReader.Load did not return the expected type");
+
+ return (T)result;
+ }
+ }
+}
diff --git a/src/Uno.Toolkit.RuntimeTests/Tests/ItemsRepeaterChipTests.cs b/src/Uno.Toolkit.RuntimeTests/Tests/ItemsRepeaterChipTests.cs
new file mode 100644
index 000000000..ce905180d
--- /dev/null
+++ b/src/Uno.Toolkit.RuntimeTests/Tests/ItemsRepeaterChipTests.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Uno.Extensions;
+using Uno.Toolkit.RuntimeTests.Extensions;
+using Uno.Toolkit.RuntimeTests.Helpers;
+using Uno.Toolkit.UI;
+using Uno.UI.RuntimeTests;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Markup;
+using ChipControl = Uno.Toolkit.UI.Chip; // ios/macos: to avoid collision with `global::Chip` namespace...
+
+
+namespace Uno.Toolkit.RuntimeTests.Tests;
+
+[TestClass]
+[RunsOnUIThread]
+internal class ItemsRepeaterChipTests_Asd
+{
+ // note: the default state of Chip.IsChecked (inherited from ToggleButton) is false, since we don't use IsThreeState
+
+ #region Selection via Toggle
+
+ [TestMethod]
+ [DataRow(ChipSelectionMode.None, new[] { 1 }, null)]
+ [DataRow(ChipSelectionMode.SingleOrNone, new[] { 1 }, 1)]
+ [DataRow(ChipSelectionMode.SingleOrNone, new[] { 1, 1 }, null)] // deselection
+ [DataRow(ChipSelectionMode.SingleOrNone, new[] { 1, 2 }, 2)] // reselection
+ [DataRow(ChipSelectionMode.Single, new int[0], 0)] // selection enforced by 'Single'
+ [DataRow(ChipSelectionMode.Single, new[] { 1 }, 1)]
+ [DataRow(ChipSelectionMode.Single, new[] { 1, 1 }, 1)] // deselection denied
+ [DataRow(ChipSelectionMode.Single, new[] { 1, 2 }, 2)] // reselection
+ [DataRow(ChipSelectionMode.Multiple, new[] { 1 }, new object[] { 1 })]
+ [DataRow(ChipSelectionMode.Multiple, new[] { 1, 2 }, new object[] { 1, 2 })] // multi-select@1,2
+ [DataRow(ChipSelectionMode.Multiple, new[] { 1, 2, 2 }, new object[] { 1 })] // multi-select@1,2, deselection@2
+ public async Task VariousMode_TapSelection(ChipSelectionMode mode, int[] selectionSequence, object expectation)
+ {
+ var source = Enumerable.Range(0, 3).ToArray();
+ var SUT = SetupItemsRepeater(source, mode);
+ bool?[] expected, actual;
+
+ await UnitTestUIContentHelperEx.SetContentAndWait(SUT);
+ expected = mode is ChipSelectionMode.Single
+ ? new bool?[] { true, false, false }
+ : new bool?[] { false, false, false };
+ actual = GetChipsSelectionState(SUT);
+ CollectionAssert.AreEqual(expected, actual);
+
+ foreach (var i in selectionSequence)
+ {
+ ((ChipControl)SUT.TryGetElement(i)).Toggle();
+ }
+ expected = (expectation switch
+ {
+ object[] array => source.Select(x => array.Contains(x)),
+ int i => source.Select(x => x == i),
+ null => Enumerable.Repeat(false, 3),
+
+ _ => throw new ArgumentOutOfRangeException(nameof(expectation)),
+ }).Cast().ToArray();
+ actual = GetChipsSelectionState(SUT);
+ CollectionAssert.AreEqual(expected, actual);
+ }
+
+ [TestMethod]
+ public async Task SingleMode_Selection()
+ {
+ var source = Enumerable.Range(0, 3).ToArray();
+ var SUT = SetupItemsRepeater(source, ChipSelectionMode.SingleOrNone);
+ bool?[] expected = new bool?[] { false, true, false }, actual;
+
+ await UnitTestUIContentHelperEx.SetContentAndWait(SUT);
+ actual = GetChipsSelectionState(SUT);
+ Assert.IsTrue(actual.All(x => x == false));
+
+ ((ChipControl)SUT.TryGetElement(1)).Toggle();
+ actual = GetChipsSelectionState(SUT);
+ CollectionAssert.AreEqual(expected, actual);
+ }
+
+ #endregion
+
+ #region Changing SelectionMode
+
+ [TestMethod]
+ public async Task SingleOrNoneToSingle_NoneSelected_ShouldAutoSelect()
+ {
+ var source = Enumerable.Range(0, 3).ToArray();
+ var SUT = SetupItemsRepeater(source, ChipSelectionMode.SingleOrNone);
+ bool?[] expected = new bool?[] { true, false, false }, actual;
+
+ await UnitTestUIContentHelperEx.SetContentAndWait(SUT);
+ actual = GetChipsSelectionState(SUT);
+ Assert.IsTrue(actual.All(x => x == false));
+
+ ChipExtensions.SetChipSelectionMode(SUT, ChipSelectionMode.Single);
+ actual = GetChipsSelectionState(SUT);
+ CollectionAssert.AreEqual(expected, actual);
+ }
+
+ [TestMethod]
+ public async Task SingleOrNoneToSingle_Selected_ShouldPreserveSelection()
+ {
+ var source = Enumerable.Range(0, 3).ToArray();
+ var SUT = SetupItemsRepeater(source, ChipSelectionMode.SingleOrNone);
+ bool?[] expected = new bool?[] { false, false, true }, actual;
+
+ await UnitTestUIContentHelperEx.SetContentAndWait(SUT);
+ ((ChipControl)SUT.TryGetElement(2)).Toggle();
+ actual = GetChipsSelectionState(SUT);
+ CollectionAssert.AreEqual(expected, actual);
+
+ ChipExtensions.SetChipSelectionMode(SUT, ChipSelectionMode.Single);
+ actual = GetChipsSelectionState(SUT);
+ CollectionAssert.AreEqual(expected, actual);
+ }
+
+ [TestMethod]
+ public async Task MultiToSingle_Selected_ShouldPreserveFirstSelection()
+ {
+ var source = Enumerable.Range(0, 3).ToArray();
+ var SUT = SetupItemsRepeater(source, ChipSelectionMode.Multiple);
+ bool?[] expected = new bool?[] { false, true, true }, actual;
+
+ await UnitTestUIContentHelperEx.SetContentAndWait(SUT);
+ ((ChipControl)SUT.TryGetElement(1)).Toggle();
+ ((ChipControl)SUT.TryGetElement(2)).Toggle();
+ actual = GetChipsSelectionState(SUT);
+ CollectionAssert.AreEqual(expected, actual);
+
+ ChipExtensions.SetChipSelectionMode(SUT, ChipSelectionMode.Single);
+ expected = new bool?[] { false, true, false };
+ actual = GetChipsSelectionState(SUT);
+ CollectionAssert.AreEqual(expected, actual);
+ }
+
+ #endregion
+
+ private static ItemsRepeater SetupItemsRepeater(object source, ChipSelectionMode mode)
+ {
+ var SUT = new ItemsRepeater
+ {
+ ItemsSource = source,
+ ItemTemplate = XamlHelper.LoadXaml(@"
+
+
+
+ "),
+ };
+ ChipExtensions.SetChipSelectionMode(SUT, mode);
+
+ return SUT;
+ }
+
+ private static bool? IsChipSelectedAt(ItemsRepeater ir, int index)
+ {
+ return (ir.TryGetElement(index) as ChipControl)?.IsChecked;
+ }
+
+ // since we are not using IsThreeState=True, the values can only be true/false.
+ // if any of them is null, that means there other problem and should be thrown.
+ // therefore, only == check should be used in assertion.
+ private static bool?[] GetChipsSelectionState(ItemsRepeater ir)
+ {
+ return (ir.ItemsSource as IEnumerable).Cast