Skip to content

Commit 5a07173

Browse files
Fix for Preventing Incorrect TalkBack Announcements During CollectionView Deselection on Android (#29818)
* fix code updated * updated test case # Conflicts: # src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue21375.cs * Update SelectableItemsViewAdapter.cs * Update SelectableItemsViewAdapter.cs * removed already reverted test case codes and screenshots * updated test case ignoring ios and mac. * optimised the fix
1 parent 8798e9d commit 5a07173

File tree

12 files changed

+232
-27
lines changed

12 files changed

+232
-27
lines changed

src/Controls/src/Core/Handlers/Items/Android/Adapters/SelectableItemsViewAdapter.cs

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class SelectableItemsViewAdapter<TItemsView, TItemsSource> : StructuredIt
1212
where TItemsSource : IItemsViewSource
1313
{
1414
List<SelectableViewHolder> _currentViewHolders = new List<SelectableViewHolder>();
15+
HashSet<object> _selectedSet = new HashSet<object>();
1516

1617
protected internal SelectableItemsViewAdapter(TItemsView selectableItemsView,
1718
Func<View, Context, ItemContentView> createView = null) : base(selectableItemsView, createView)
@@ -57,21 +58,59 @@ internal void ClearPlatformSelection()
5758
}
5859
}
5960

60-
internal void MarkPlatformSelection(object selectedItem)
61+
internal void MarkPlatformSelection(SelectableItemsView selectableItemsView)
6162
{
62-
if (selectedItem == null)
63+
if (_currentViewHolders.Count == 0)
6364
{
6465
return;
6566
}
6667

67-
var position = GetPositionForItem(selectedItem);
68+
_selectedSet.Clear();
69+
70+
switch (selectableItemsView.SelectionMode)
71+
{
72+
case SelectionMode.None:
73+
ClearPlatformSelection();
74+
return;
75+
76+
case SelectionMode.Single:
77+
var selectedItem = selectableItemsView.SelectedItem;
78+
if (selectedItem == null)
79+
{
80+
ClearPlatformSelection();
81+
return;
82+
}
83+
84+
_selectedSet.Add(selectedItem);
85+
break;
86+
87+
case SelectionMode.Multiple:
88+
var selectedItems = selectableItemsView.SelectedItems;
89+
if (selectedItems == null || selectedItems.Count == 0)
90+
{
91+
ClearPlatformSelection();
92+
return;
93+
}
94+
95+
_selectedSet.UnionWith(selectedItems);
96+
break;
97+
98+
default:
99+
return;
100+
}
68101

69102
for (int i = 0; i < _currentViewHolders.Count; i++)
70103
{
71-
if (_currentViewHolders[i].BindingAdapterPosition == position)
104+
var holder = _currentViewHolders[i];
105+
if (holder.BindingAdapterPosition >= 0)
72106
{
73-
_currentViewHolders[i].IsSelected = true;
74-
return;
107+
var item = ItemsSource.GetItem(holder.BindingAdapterPosition);
108+
bool shouldBeSelected = _selectedSet.Contains(item);
109+
110+
if (holder.IsSelected != shouldBeSelected)
111+
{
112+
holder.IsSelected = shouldBeSelected;
113+
}
75114
}
76115
}
77116
}

src/Controls/src/Core/Handlers/Items/Android/SelectableViewHolder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public bool IsSelected
3434
SetSelectionStates(_isSelected);
3535

3636
ItemView.Activated = _isSelected;
37+
ItemView.Selected = _isSelected;
3738
OnSelectedChanged();
3839
}
3940
}

src/Controls/src/Core/Platform/Android/Extensions/RecyclerViewExtensions.cs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,12 @@ public static class RecyclerViewExtensions
1111
{
1212
public static void UpdateSelection(this RecyclerView recyclerView, SelectableItemsView selectableItemsView)
1313
{
14-
var mode = selectableItemsView.SelectionMode;
1514
//TODO: on NET8 implement a ISelectableItemsViewAdapter interface on the adapter
1615
var adapter = recyclerView.GetAdapter() as ReorderableItemsViewAdapter<ReorderableItemsView, IGroupableItemsViewSource>;
1716
if (adapter == null)
1817
return;
1918

20-
adapter.ClearPlatformSelection();
21-
22-
switch (mode)
23-
{
24-
case SelectionMode.None:
25-
return;
26-
27-
case SelectionMode.Single:
28-
var selectedItem = selectableItemsView.SelectedItem;
29-
adapter.MarkPlatformSelection(selectedItem);
30-
return;
31-
32-
case SelectionMode.Multiple:
33-
var selectedItems = selectableItemsView.SelectedItems;
34-
foreach (var item in selectedItems)
35-
{
36-
adapter.MarkPlatformSelection(item);
37-
}
38-
return;
39-
}
19+
adapter.MarkPlatformSelection(selectableItemsView);
4020
}
4121
}
4222
}
91.1 KB
Loading
94.8 KB
Loading
93 KB
Loading
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
x:Class="Maui.Controls.Sample.Issues.Issue21375"
5+
Title="Issue21375">
6+
<VerticalStackLayout>
7+
<CollectionView ItemsSource="{Binding Items}" SelectionMode="Single" x:Name="collectionView" AutomationId="collectionView" Background="LightBlue">
8+
<CollectionView.ItemTemplate>
9+
<DataTemplate>
10+
<StackLayout Padding="10" SemanticProperties.Description="{Binding Name}" HeightRequest="50">
11+
<Label Text="{Binding Name}" FontAttributes="Bold" FontSize="16"/>
12+
<Label Text="{Binding Description}" FontSize="14"/>
13+
</StackLayout>
14+
</DataTemplate>
15+
</CollectionView.ItemTemplate>
16+
</CollectionView>
17+
<Label Margin="0,10,0,0" Text="SelectionMode" />
18+
<Button Text="None" Clicked="NoneSelectionMode" x:Name="noneButton" AutomationId="noneButton"/>
19+
<Button Text="Single" Clicked="SingleSelectionMode" AutomationId="singleButton"/>
20+
<Button Text="Multiple" Clicked="MultipleSelectionMode" AutomationId="multipleButton" />
21+
<Button Text="Calculate" Clicked="Calculate" x:Name="calculateButton" AutomationId="calculateButton"/>
22+
<Editor x:Name="Output" HeightRequest="400"/>
23+
<Button Text="Clear Selection" Clicked="ClearSelection" AutomationId="clearButton"/>
24+
</VerticalStackLayout>
25+
</ContentPage>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System.Collections.ObjectModel;
2+
using System.Text;
3+
4+
#if ANDROID
5+
using AndroidX.RecyclerView.Widget;
6+
#endif
7+
8+
namespace Maui.Controls.Sample.Issues;
9+
10+
[Issue(IssueTracker.Github, 21375, "Selected CollectionView item is not announced", PlatformAffected.iOS & PlatformAffected.macOS & PlatformAffected.Android)]
11+
public partial class Issue21375 : ContentPage
12+
{
13+
public ObservableCollection<Item> Items { get; set; }
14+
15+
public Issue21375()
16+
{
17+
InitializeComponent();
18+
19+
Items = new ObservableCollection<Item>
20+
{
21+
new Item { Name = "Item 1", Description = "Description for item 1" },
22+
new Item { Name = "Item 2", Description = "Description for item 2" },
23+
new Item { Name = "Item 3", Description = "Description for item 3" },
24+
new Item { Name = "Item 4", Description = "Description for item 4" },
25+
new Item { Name = "Item 5", Description = "Description for item 5" }
26+
};
27+
28+
BindingContext = this;
29+
}
30+
31+
void NoneSelectionMode(object sender, EventArgs e)
32+
{
33+
collectionView.SelectionMode = SelectionMode.None;
34+
}
35+
36+
void SingleSelectionMode(object sender, EventArgs e)
37+
{
38+
collectionView.SelectionMode = SelectionMode.Single;
39+
}
40+
41+
void MultipleSelectionMode(object sender, EventArgs e)
42+
{
43+
collectionView.SelectionMode = SelectionMode.Multiple;
44+
}
45+
46+
void Calculate(object sender, EventArgs e)
47+
{
48+
var sb = new StringBuilder();
49+
#if IOS || MACCATALYST
50+
if (collectionView.Handler?.PlatformView is UIKit.UIView vc
51+
&& vc.Subviews is UIKit.UIView[] subviews && subviews.Length > 0
52+
&& subviews[0] is UIKit.UICollectionView uiCollectionView)
53+
{
54+
for (int i = 0; i < uiCollectionView.VisibleCells.Length; i++)
55+
{
56+
var cell = uiCollectionView.VisibleCells[i];
57+
sb.AppendLine($"Item{i} Cell: {cell.AccessibilityTraits}");
58+
if (cell.ContentView is not null && cell.ContentView.Subviews.Length > 0)
59+
{
60+
var firstChild = cell.ContentView.Subviews[0];
61+
sb.AppendLine($"Item{i} FirstChild: {firstChild.AccessibilityTraits}");
62+
}
63+
}
64+
}
65+
#elif ANDROID
66+
var plat = collectionView.Handler?.PlatformView;
67+
if (plat is RecyclerView recyclerView)
68+
{
69+
var adapter = recyclerView.GetAdapter();
70+
var layoutManager = recyclerView.GetLayoutManager();
71+
var childCount = layoutManager.ChildCount;
72+
for (int i = 0; i < childCount; i++)
73+
{
74+
var viewHolder = recyclerView.GetChildAt(i);
75+
if (viewHolder is null)
76+
continue;
77+
78+
var position = recyclerView.GetChildAdapterPosition(viewHolder);
79+
sb.AppendLine($"Item{position} Cell: {viewHolder.Selected}");
80+
}
81+
}
82+
#elif WINDOWS
83+
var plat = collectionView.Handler?.PlatformView;
84+
if (plat is Microsoft.UI.Xaml.Controls.ListView listView)
85+
{
86+
foreach (var item in listView.Items)
87+
{
88+
var container = listView.ContainerFromItem(item) as Microsoft.UI.Xaml.Controls.ListViewItem;
89+
if (container != null)
90+
{
91+
sb.AppendLine($"Item: {item} IsSelected: {container.IsSelected}");
92+
}
93+
}
94+
}
95+
#endif
96+
Output.Text = sb.ToString();
97+
}
98+
99+
void ClearSelection(object sender, EventArgs e)
100+
{
101+
collectionView.SelectedItem = null;
102+
}
103+
104+
public class Item
105+
{
106+
public string Name { get; set; }
107+
public string Description { get; set; }
108+
}
109+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#if !IOS && !MACCATALYST // Test excluded for iOS/macCatalyst as the accessibility implementation was reverted in PR-https://github.com/dotnet/maui/pull/29827
2+
3+
using NUnit.Framework;
4+
using UITest.Appium;
5+
using UITest.Core;
6+
7+
namespace Microsoft.Maui.TestCases.Tests.Issues;
8+
9+
public class Issue21375 : _IssuesUITest
10+
{
11+
public Issue21375(TestDevice device) : base(device) { }
12+
13+
public override string Issue => "Selected CollectionView item is not announced";
14+
15+
[Test]
16+
[Category(UITestCategories.CollectionView)]
17+
public void SelectedItemsShowSelected ()
18+
{
19+
var collectionView = App.WaitForElement("collectionView");
20+
21+
App.Tap("Item 1");
22+
App.WaitForElement("calculateButton").Tap();
23+
24+
VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_single");
25+
26+
App.WaitForElement("multipleButton").Tap();
27+
App.Tap("Item 2");
28+
App.Tap("Item 3");
29+
App.WaitForElement("calculateButton").Tap();
30+
31+
VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_multiple");
32+
33+
App.WaitForElement("noneButton").Tap();
34+
App.WaitForElement("calculateButton").Tap();
35+
VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_none");
36+
37+
App.WaitForElement("singleButton").Tap();
38+
App.WaitForElement("calculateButton").Tap();
39+
40+
#if !WINDOWS // Windows clears out the selected items when we switch back
41+
VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_single");
42+
43+
App.WaitForElement("multipleButton").Tap();
44+
App.WaitForElement("calculateButton").Tap();
45+
46+
VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_multiple");
47+
#endif
48+
49+
}
50+
}
51+
#endif
31.5 KB
Loading

0 commit comments

Comments
 (0)