generated from unoplatform/template
-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add support for iselectioninfo to itemsrepeater extensions
- Loading branch information
Showing
6 changed files
with
418 additions
and
46 deletions.
There are no files selected for viewing
39 changes: 39 additions & 0 deletions
39
src/Uno.Toolkit.RuntimeTests/Extensions/ItemsRepeaterTestExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using ItemsRepeater = Microsoft.UI.Xaml.Controls.ItemsRepeater; | ||
using Uno.Toolkit.UI; | ||
|
||
#if IS_WINUI | ||
using Microsoft.UI.Xaml.Controls; | ||
using Microsoft.UI.Xaml.Controls.Primitives; | ||
#else | ||
using Windows.UI.Xaml.Controls; | ||
using Windows.UI.Xaml.Controls.Primitives; | ||
#endif | ||
|
||
namespace Uno.Toolkit.RuntimeTests.Extensions | ||
{ | ||
internal static class ItemsRepeaterTestExtensions | ||
{ | ||
public static void FakeTapItemAt(this ItemsRepeater ir, int index) | ||
{ | ||
if (ir.TryGetElement(index) is { } element) | ||
{ | ||
// Fake local tap handler on ToggleButton level. | ||
// For SelectorItem, nothing will happen on tap unless nested under a Selector, which isnt the case here. | ||
(element as ToggleButton)?.Toggle(); | ||
|
||
// This is what's called in ItemsRepeater::Tapped handler. | ||
// Note that the handler will not trigger from a "fake tap" like the line above, so we have to manually invoke here. | ||
ItemsRepeaterExtensions.ToggleItemSelectionAtCoerced(ir, index); | ||
} | ||
else | ||
{ | ||
throw new InvalidOperationException($"Element at index={index} is not yet materialized or out of range."); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
222 changes: 222 additions & 0 deletions
222
src/Uno.Toolkit.RuntimeTests/Tests/ItemsRepeaterExtensionTests.ISelectionInfo.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
using Uno.Toolkit.RuntimeTests.Helpers; | ||
using Uno.Toolkit.UI; | ||
using Uno.UI.RuntimeTests; | ||
using ItemsRepeater = Microsoft.UI.Xaml.Controls.ItemsRepeater; | ||
using static Uno.Toolkit.RuntimeTests.Tests.ItemsRepeaterChipTests; // to borrow helper methods | ||
using Uno.Extensions; | ||
using Uno.Toolkit.RuntimeTests.Extensions; | ||
using Windows.UI.Xaml.Controls.Primitives; | ||
Check failure on line 15 in src/Uno.Toolkit.RuntimeTests/Tests/ItemsRepeaterExtensionTests.ISelectionInfo.cs
|
||
|
||
|
||
|
||
#if IS_WINUI | ||
using Microsoft.UI.Xaml.Data; | ||
#else | ||
using Windows.UI.Xaml.Data; | ||
#endif | ||
|
||
namespace Uno.Toolkit.RuntimeTests.Tests | ||
{ | ||
[TestClass] | ||
[RunsOnUIThread] | ||
partial class ItemsRepeaterExtensionsTests | ||
{ | ||
[TestMethod] | ||
[DataRow(ItemsSelectionMode.SingleOrNone, null, false, null, DisplayName = $"Select: {nameof(ItemsSelectionMode.SingleOrNone)} none")] | ||
[DataRow(ItemsSelectionMode.SingleOrNone, new[] { 0 }, false, new[] { 0 }, DisplayName = $"Select: {nameof(ItemsSelectionMode.SingleOrNone)} 0")] | ||
[DataRow(ItemsSelectionMode.Multiple, new[] { 0, 2 }, false, new[] { 0, 2 }, DisplayName = $"Select: {nameof(ItemsSelectionMode.Multiple)} (0, 2)")] | ||
[DataRow(ItemsSelectionMode.SingleOrNone, null, true, new[] { 0 }, DisplayName = $"Deselect: {nameof(ItemsSelectionMode.SingleOrNone)} none")] | ||
[DataRow(ItemsSelectionMode.SingleOrNone, new[] { 0 }, true, null, DisplayName = $"Deselect: {nameof(ItemsSelectionMode.SingleOrNone)} 0")] | ||
[DataRow(ItemsSelectionMode.Multiple, new[] { 0, 2 }, true, new[] { 1 }, DisplayName = $"Deselect: {nameof(ItemsSelectionMode.Multiple)} (0, 2)")] | ||
public async Task When_Tapped_With_ISelectionInfo(ItemsSelectionMode mode, int[]? tapSequence, bool isDeselecting, int[]? expected) | ||
{ | ||
var selected = isDeselecting ? new HashSet<int>() { 0, 1, 2 } : new HashSet<int>(); | ||
|
||
var source = SelectionSource.Create(3, isPreselected: x => isDeselecting); | ||
source.DeselectRangeOverride = x => DeselectRangeOverride(x, selected); | ||
source.SelectRangeOverride = x => SelectRangeOverride(x, selected); | ||
|
||
var SUT = SetupItemsRepeater(source, mode); | ||
|
||
await UnitTestUIContentHelperEx.SetContentAndWait(SUT); | ||
|
||
foreach (var i in tapSequence.Safe()) | ||
{ | ||
SUT.FakeTapItemAt(i); | ||
} | ||
|
||
Assert.IsTrue(AreEqual(expected.Safe(), selected)); | ||
} | ||
|
||
[TestMethod] | ||
[DataRow(ItemsSelectionMode.None, ItemsSelectionMode.Single, null, new[] { 0 }, DisplayName = $"{nameof(ItemsSelectionMode.None)} to {nameof(ItemsSelectionMode.Single)} with [] selected")] | ||
[DataRow(ItemsSelectionMode.None, ItemsSelectionMode.SingleOrNone, null, null, DisplayName = $"{nameof(ItemsSelectionMode.None)} to {nameof(ItemsSelectionMode.SingleOrNone)} with [] selected")] | ||
[DataRow(ItemsSelectionMode.None, ItemsSelectionMode.Multiple, null, null, DisplayName = $"{nameof(ItemsSelectionMode.None)} to {nameof(ItemsSelectionMode.Multiple)} with [] selected")] | ||
[DataRow(ItemsSelectionMode.Single, ItemsSelectionMode.None, new[] { 1 }, null, DisplayName = $"{nameof(ItemsSelectionMode.Single)} to {nameof(ItemsSelectionMode.None)} with [1] selected")] | ||
[DataRow(ItemsSelectionMode.Single, ItemsSelectionMode.SingleOrNone, new[] { 1 }, new[] { 1 }, DisplayName = $"{nameof(ItemsSelectionMode.Single)} to {nameof(ItemsSelectionMode.SingleOrNone)} with [1] selected")] | ||
[DataRow(ItemsSelectionMode.Single, ItemsSelectionMode.Multiple, new[] { 1 }, new[] { 1 }, DisplayName = $"{nameof(ItemsSelectionMode.Single)} to {nameof(ItemsSelectionMode.Multiple)} with [1] selected")] | ||
[DataRow(ItemsSelectionMode.SingleOrNone, ItemsSelectionMode.None, new[] { 1 }, null, DisplayName = $"{nameof(ItemsSelectionMode.SingleOrNone)} to {nameof(ItemsSelectionMode.None)} with [1] selected")] | ||
[DataRow(ItemsSelectionMode.SingleOrNone, ItemsSelectionMode.Single, new[] { 1 }, new[] { 1 }, DisplayName = $"{nameof(ItemsSelectionMode.SingleOrNone)} to {nameof(ItemsSelectionMode.Single)} with [1] selected")] | ||
[DataRow(ItemsSelectionMode.SingleOrNone, ItemsSelectionMode.Multiple, new[] { 1 }, new[] { 1 }, DisplayName = $"{nameof(ItemsSelectionMode.SingleOrNone)} to {nameof(ItemsSelectionMode.Multiple)} with [1] selected")] | ||
[DataRow(ItemsSelectionMode.SingleOrNone, ItemsSelectionMode.None, null, null, DisplayName = $"{nameof(ItemsSelectionMode.SingleOrNone)} to {nameof(ItemsSelectionMode.None)} with [] selected")] | ||
[DataRow(ItemsSelectionMode.SingleOrNone, ItemsSelectionMode.Single, null, new[] { 0 }, DisplayName = $"{nameof(ItemsSelectionMode.SingleOrNone)} to {nameof(ItemsSelectionMode.Single)} with [] selected")] | ||
[DataRow(ItemsSelectionMode.SingleOrNone, ItemsSelectionMode.Multiple, null, null, DisplayName = $"{nameof(ItemsSelectionMode.SingleOrNone)} to {nameof(ItemsSelectionMode.Multiple)} with [] selected")] | ||
[DataRow(ItemsSelectionMode.Multiple, ItemsSelectionMode.None, new[] { 1, 2 }, null, DisplayName = $"{nameof(ItemsSelectionMode.Multiple)} to {nameof(ItemsSelectionMode.None)} with [1, 2] selected")] | ||
[DataRow(ItemsSelectionMode.Multiple, ItemsSelectionMode.Single, new[] { 1, 2 }, new[] { 1 }, DisplayName = $"{nameof(ItemsSelectionMode.Multiple)} to {nameof(ItemsSelectionMode.Single)} with [1, 2] selected")] | ||
[DataRow(ItemsSelectionMode.Multiple, ItemsSelectionMode.SingleOrNone, new[] { 1, 2 }, new[] { 1 }, DisplayName = $"{nameof(ItemsSelectionMode.Multiple)} to {nameof(ItemsSelectionMode.SingleOrNone)} with [1, 2] selected")] | ||
public async Task When_Mode_Changed_ISelectionInfo(ItemsSelectionMode originalMode, ItemsSelectionMode newMode, int[]? selectedIndexes, int[]? expectedIndexes) | ||
{ | ||
var selected = new HashSet<int>(); | ||
|
||
var source = SelectionSource.Create(3, isPreselected: x => selectedIndexes.Safe().Contains(x)); | ||
source.DeselectRangeOverride = x => DeselectRangeOverride(x, selected); | ||
source.SelectRangeOverride = x => SelectRangeOverride(x, selected); | ||
|
||
var SUT = SetupItemsRepeater(source, originalMode); | ||
|
||
await UnitTestUIContentHelperEx.SetContentAndWait(SUT); | ||
|
||
ItemsRepeaterExtensions.SetSelectionMode(SUT, newMode); | ||
|
||
Assert.IsTrue(AreEqual(expectedIndexes.Safe(), selected)); | ||
} | ||
|
||
[TestMethod] | ||
public async Task When_Source_Changed_With_ISelectionInfo() | ||
{ | ||
var selected = new HashSet<int>(); | ||
|
||
var evenSource = SelectionSource.Create(4, isPreselected: x => x % 2 == 0); | ||
var oddSource = SelectionSource.Create(4, isPreselected: x => x % 2 == 1); | ||
oddSource.DeselectRangeOverride = x => DeselectRangeOverride(x, selected); | ||
oddSource.SelectRangeOverride = x => SelectRangeOverride(x, selected); | ||
|
||
var SUT = SetupItemsRepeater(evenSource, ItemsSelectionMode.Multiple); | ||
|
||
await UnitTestUIContentHelperEx.SetContentAndWait(SUT); | ||
|
||
Assert.IsTrue(ItemsRepeaterExtensions.GetSelectedIndex(SUT) == 0); | ||
Assert.IsTrue(AreEqual(ItemsRepeaterExtensions.GetSelectedIndexes(SUT), new[] { 0, 2 } )); | ||
|
||
SUT.ItemsSource = oddSource; | ||
|
||
Assert.IsTrue(ItemsRepeaterExtensions.GetSelectedIndex(SUT) == 1); | ||
Assert.IsTrue(AreEqual(ItemsRepeaterExtensions.GetSelectedIndexes(SUT), new[] { 1, 3 })); | ||
} | ||
|
||
|
||
// Checks equality of two lists based on values, ignoring order | ||
private static bool AreEqual(IEnumerable<int> expected, IEnumerable<int> actual) | ||
{ | ||
return Enumerable.SequenceEqual(expected.OrderBy(x => x), actual.OrderBy(x => x)); | ||
} | ||
|
||
private static void DeselectRangeOverride(ItemIndexRange range, ICollection<int> selected) | ||
{ | ||
range.ExpandRange().ForEach(idx => selected.Remove(idx)); | ||
} | ||
|
||
private static void SelectRangeOverride(ItemIndexRange range, ICollection<int> selected) | ||
{ | ||
selected.AddRange(range.ExpandRange()); | ||
} | ||
} | ||
} | ||
|
||
public class SelectionData | ||
{ | ||
public int Value { get; set; } | ||
public bool Selected { get; set; } | ||
|
||
public override string ToString() => Value.ToString(); | ||
} | ||
public class SelectionSource : List<SelectionData>, ISelectionInfo | ||
{ | ||
public Action<ItemIndexRange> SelectRangeOverride { get; set; } | ||
public Action<ItemIndexRange> DeselectRangeOverride { get; set; } | ||
public Func<int, bool> IsSelectedOverride { get; set; } | ||
public Func<IReadOnlyList<ItemIndexRange>> GetSelectedRangesOverride { get; set; } | ||
|
||
public SelectionSource(IEnumerable<SelectionData> source) : base(source) | ||
{ | ||
this.SelectRangeOverride = SelectRangeImpl; | ||
this.DeselectRangeOverride = DeselectRangeImpl; | ||
this.IsSelectedOverride = IsSelectedImpl; | ||
this.GetSelectedRangesOverride = GetSelectedRangesImpl; | ||
} | ||
public static SelectionSource Create(int count, Func<int, bool> isPreselected) => Create(Enumerable.Range(0, count), isPreselected); | ||
public static SelectionSource Create(IEnumerable<int> source, Func<int, bool> isPreselected) | ||
{ | ||
return new(source.Select(x => new SelectionData | ||
{ | ||
Value = x, | ||
Selected = isPreselected(x), | ||
})); | ||
} | ||
|
||
// ISelectionInfo | ||
public void SelectRange(ItemIndexRange itemIndexRange) => SelectRangeOverride(itemIndexRange); | ||
public void DeselectRange(ItemIndexRange itemIndexRange) => DeselectRangeOverride(itemIndexRange); | ||
public bool IsSelected(int index) => IsSelectedOverride(index); | ||
public IReadOnlyList<ItemIndexRange> GetSelectedRanges() => GetSelectedRangesOverride(); | ||
|
||
// ISelectionInfo impl | ||
internal void SelectRangeImpl(ItemIndexRange itemIndexRange) | ||
{ | ||
foreach (var index in ExpandRange(itemIndexRange)) | ||
{ | ||
this[index].Selected = true; | ||
} | ||
} | ||
internal void DeselectRangeImpl(ItemIndexRange itemIndexRange) | ||
{ | ||
foreach (var index in ExpandRange(itemIndexRange)) | ||
{ | ||
this[index].Selected = false; | ||
} | ||
} | ||
internal bool IsSelectedImpl(int index) => this[index].Selected; | ||
internal IReadOnlyList<ItemIndexRange> GetSelectedRangesImpl() | ||
{ | ||
return ReduceToRange(this | ||
.Select((x, i) => (Index: i, x.Selected)) | ||
.Where(x => x.Selected) | ||
.Select(x => x.Index) | ||
).ToArray(); | ||
} | ||
|
||
// helper methods | ||
internal static IEnumerable<ItemIndexRange> ReduceToRange(IEnumerable<int> indexes) | ||
{ | ||
int first = int.MinValue; | ||
uint n = 0; | ||
foreach (var i in indexes.OrderBy(x => x)) | ||
{ | ||
if (first + n == i) | ||
{ | ||
n++; | ||
} | ||
else | ||
{ | ||
if (n > 0) yield return new(first, n); | ||
|
||
first = i; | ||
n = 1; | ||
} | ||
} | ||
|
||
if (n > 0) | ||
{ | ||
yield return new(first, n); | ||
} | ||
} | ||
internal static IEnumerable<int> ExpandRange(ItemIndexRange range) => Enumerable.Range(range.FirstIndex, (int)range.Length); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.