Skip to content

Commit a55b52c

Browse files
tj-devel709SuthiYuvaraj
authored andcommitted
Add Accessibility Selected for iOS CollectionView (dotnet#29014)
* work for iOS and Android * sample code to remove later * working when changing selectionMode with iOS and android * UITests * Adds windows to UITest * add tests for CV2 * screenshots 1 * screenshots 2 * screenshots 3 * screenshots 4 * add windows delay and rename similarly named tests * screenshots 5 * screenshots 6 * screenshots 7 * typo in class name * removing android stuff
1 parent 968a73e commit a55b52c

24 files changed

+385
-1
lines changed

src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ internal void SelectItem(object selectedItem)
4545
CollectionView.PerformBatchUpdates(null, _ =>
4646
{
4747
CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None);
48+
CollectionView.CellForItem(index)?.UpdateSelectedAccessibility(true);
4849
});
4950
}
5051
}
@@ -75,6 +76,8 @@ void FormsSelectItem(NSIndexPath indexPath)
7576
ItemsView.SelectedItems.Add(GetItemAtIndex(indexPath));
7677
break;
7778
}
79+
80+
CollectionView.CellForItem(indexPath)?.UpdateSelectedAccessibility(true);
7881
}
7982

8083
void FormsDeselectItem(NSIndexPath indexPath)
@@ -91,6 +94,8 @@ void FormsDeselectItem(NSIndexPath indexPath)
9194
ItemsView.SelectedItems.Remove(GetItemAtIndex(indexPath));
9295
break;
9396
}
97+
98+
CollectionView.CellForItem(indexPath)?.UpdateSelectedAccessibility(false);
9499
}
95100

96101
internal void UpdatePlatformSelection()
@@ -130,6 +135,10 @@ internal void UpdateSelectionMode()
130135
{
131136
var mode = ItemsView.SelectionMode;
132137

138+
// We want to make sure we clear the selection trait before we switch modes.
139+
// If we do this after we switch modes, cells that are selected may not show up as selected anymore.
140+
CollectionView.ClearSelectedAccessibilityTraits(CollectionView.GetIndexPathsForSelectedItems());
141+
133142
switch (mode)
134143
{
135144
case SelectionMode.None:

src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ internal void SelectItem(object selectedItem)
4545
CollectionView.PerformBatchUpdates(null, _ =>
4646
{
4747
CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None);
48+
CollectionView.CellForItem(index)?.UpdateSelectedAccessibility(true);
4849
});
4950
}
5051
}
@@ -75,6 +76,8 @@ void FormsSelectItem(NSIndexPath indexPath)
7576
ItemsView.SelectedItems.Add(GetItemAtIndex(indexPath));
7677
break;
7778
}
79+
80+
CollectionView.CellForItem(indexPath)?.UpdateSelectedAccessibility(true);
7881
}
7982

8083
void FormsDeselectItem(NSIndexPath indexPath)
@@ -91,6 +94,8 @@ void FormsDeselectItem(NSIndexPath indexPath)
9194
ItemsView.SelectedItems.Remove(GetItemAtIndex(indexPath));
9295
break;
9396
}
97+
98+
CollectionView.CellForItem(indexPath)?.UpdateSelectedAccessibility(false);
9499
}
95100

96101
internal void UpdatePlatformSelection()
@@ -130,6 +135,10 @@ internal void UpdateSelectionMode()
130135
{
131136
var mode = ItemsView.SelectionMode;
132137

138+
// We want to make sure we clear the selection trait before we switch modes.
139+
// If we do this after we switch modes, cells that are selected may not show up as selected anymore.
140+
CollectionView.ClearSelectedAccessibilityTraits(CollectionView.GetIndexPathsForSelectedItems());
141+
133142
switch (mode)
134143
{
135144
case SelectionMode.None:

src/Controls/src/Core/Platform/iOS/Extensions/AcessibilityExtensions.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,48 @@
22

33
namespace Microsoft.Maui.Controls.Platform;
44

5-
internal static class AcessibilityExtensions
5+
internal static class AccessibilityExtensions
66
{
7+
internal static void UpdateSelectedAccessibility(this UICollectionViewCell cell, bool selected)
8+
{
9+
// Catalyst and iOS Simulators applies/removes the 'Selected' trait to the cell automatically.
10+
// iOS Devices do not apply the 'Selected' trait automatically to the cell unless VoiceOver is on.
11+
// On iOS, the 'Selected' trait needs to be applied to the first child of the cell for VoiceOver to announce it.
12+
#if IOS
13+
if (cell.ContentView is not null && cell.ContentView.Subviews.Length > 0)
14+
{
15+
var firstChild = cell.ContentView.Subviews[0];
16+
17+
if (selected)
18+
{
19+
firstChild.AccessibilityTraits |= UIAccessibilityTrait.Selected;
20+
}
21+
else
22+
{
23+
firstChild.AccessibilityTraits &= ~UIAccessibilityTrait.Selected;
24+
}
25+
}
26+
#endif
27+
}
28+
29+
internal static void ClearSelectedAccessibilityTraits(this UICollectionView collectionView, Foundation.NSIndexPath[] indices)
30+
{
31+
// Catalyst and iOS Simulators applies/removes the 'Selected' trait to the cell automatically.
32+
// iOS Devices do not apply the 'Selected' trait automatically to the cell unless VoiceOver is on.
33+
// On iOS, the 'Selected' trait needs to be applied to the first child of the cell for VoiceOver to announce it.
34+
#if IOS
35+
foreach (var index in indices)
36+
{
37+
var cell = collectionView.CellForItem(index);
38+
if (cell?.ContentView is not null && cell.ContentView.Subviews.Length > 0)
39+
{
40+
var firstChild = cell.ContentView.Subviews[0];
41+
firstChild.AccessibilityTraits &= ~UIAccessibilityTrait.Selected;
42+
}
43+
}
44+
#endif
45+
}
46+
747
internal static void UpdateAccessibilityTraits(this UICollectionView collectionView, SelectableItemsView itemsView)
848
{
949
foreach (var subview in collectionView.Subviews)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
</VerticalStackLayout>
24+
</ContentPage>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
public class Item
100+
{
101+
public string Name { get; set; }
102+
public string Description { get; set; }
103+
}
104+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
xmlns:local="clr-namespace:Maui.Controls.Sample"
5+
x:Class="Maui.Controls.Sample.Issues.Issue21375_2">
6+
<VerticalStackLayout>
7+
<local:CollectionView2 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+
</local:CollectionView2>
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+
</VerticalStackLayout>
24+
</ContentPage>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System.Collections.ObjectModel;
2+
using System.Text;
3+
4+
namespace Maui.Controls.Sample.Issues;
5+
6+
[Issue(IssueTracker.Github, 21375_2, "Selected CollectionView2 item is not announced", PlatformAffected.iOS & PlatformAffected.macOS)]
7+
public partial class Issue21375_2 : ContentPage
8+
{
9+
public ObservableCollection<Item> Items { get; set; }
10+
11+
public Issue21375_2()
12+
{
13+
InitializeComponent();
14+
15+
Items = new ObservableCollection<Item>
16+
{
17+
new Item { Name = "Item 1", Description = "Description for item 1" },
18+
new Item { Name = "Item 2", Description = "Description for item 2" },
19+
new Item { Name = "Item 3", Description = "Description for item 3" },
20+
new Item { Name = "Item 4", Description = "Description for item 4" },
21+
new Item { Name = "Item 5", Description = "Description for item 5" }
22+
};
23+
24+
BindingContext = this;
25+
}
26+
27+
void NoneSelectionMode(object sender, EventArgs e)
28+
{
29+
collectionView.SelectionMode = SelectionMode.None;
30+
}
31+
32+
void SingleSelectionMode(object sender, EventArgs e)
33+
{
34+
collectionView.SelectionMode = SelectionMode.Single;
35+
}
36+
37+
void MultipleSelectionMode(object sender, EventArgs e)
38+
{
39+
collectionView.SelectionMode = SelectionMode.Multiple;
40+
}
41+
42+
void Calculate(object sender, EventArgs e)
43+
{
44+
var sb = new StringBuilder();
45+
#if IOS || MACCATALYST
46+
if (collectionView.Handler?.PlatformView is UIKit.UIView vc
47+
&& vc.Subviews is UIKit.UIView[] subviews && subviews.Length > 0
48+
&& subviews[0] is UIKit.UICollectionView uiCollectionView)
49+
{
50+
for (int i = 0; i < uiCollectionView.VisibleCells.Length; i++)
51+
{
52+
var cell = uiCollectionView.VisibleCells[i];
53+
sb.AppendLine($"Item{i} Cell: {cell.AccessibilityTraits}");
54+
if (cell.ContentView is not null && cell.ContentView.Subviews.Length > 0)
55+
{
56+
var firstChild = cell.ContentView.Subviews[0];
57+
sb.AppendLine($"Item{i} FirstChild: {firstChild.AccessibilityTraits}");
58+
}
59+
}
60+
}
61+
#endif
62+
Output.Text = sb.ToString();
63+
}
64+
65+
public class Item
66+
{
67+
public string Name { get; set; }
68+
public string Description { get; set; }
69+
}
70+
}
54.2 KB
Loading
52 KB
Loading
54.2 KB
Loading

0 commit comments

Comments
 (0)