diff --git a/src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs b/src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs index 5b3ed42585fc..7df28c256b1d 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs @@ -45,6 +45,7 @@ internal void SelectItem(object selectedItem) CollectionView.PerformBatchUpdates(null, _ => { CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None); + CollectionView.CellForItem(index)?.UpdateSelectedAccessibility(true); }); } } @@ -75,6 +76,8 @@ void FormsSelectItem(NSIndexPath indexPath) ItemsView.SelectedItems.Add(GetItemAtIndex(indexPath)); break; } + + CollectionView.CellForItem(indexPath)?.UpdateSelectedAccessibility(true); } void FormsDeselectItem(NSIndexPath indexPath) @@ -91,6 +94,8 @@ void FormsDeselectItem(NSIndexPath indexPath) ItemsView.SelectedItems.Remove(GetItemAtIndex(indexPath)); break; } + + CollectionView.CellForItem(indexPath)?.UpdateSelectedAccessibility(false); } internal void UpdatePlatformSelection() @@ -130,6 +135,10 @@ internal void UpdateSelectionMode() { var mode = ItemsView.SelectionMode; + // We want to make sure we clear the selection trait before we switch modes. + // If we do this after we switch modes, cells that are selected may not show up as selected anymore. + CollectionView.ClearSelectedAccessibilityTraits(CollectionView.GetIndexPathsForSelectedItems()); + switch (mode) { case SelectionMode.None: diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs index 6909afabc29a..b3dda6f95f87 100644 --- a/src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs +++ b/src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs @@ -45,6 +45,7 @@ internal void SelectItem(object selectedItem) CollectionView.PerformBatchUpdates(null, _ => { CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None); + CollectionView.CellForItem(index)?.UpdateSelectedAccessibility(true); }); } } @@ -75,6 +76,8 @@ void FormsSelectItem(NSIndexPath indexPath) ItemsView.SelectedItems.Add(GetItemAtIndex(indexPath)); break; } + + CollectionView.CellForItem(indexPath)?.UpdateSelectedAccessibility(true); } void FormsDeselectItem(NSIndexPath indexPath) @@ -91,6 +94,8 @@ void FormsDeselectItem(NSIndexPath indexPath) ItemsView.SelectedItems.Remove(GetItemAtIndex(indexPath)); break; } + + CollectionView.CellForItem(indexPath)?.UpdateSelectedAccessibility(false); } internal void UpdatePlatformSelection() @@ -130,6 +135,10 @@ internal void UpdateSelectionMode() { var mode = ItemsView.SelectionMode; + // We want to make sure we clear the selection trait before we switch modes. + // If we do this after we switch modes, cells that are selected may not show up as selected anymore. + CollectionView.ClearSelectedAccessibilityTraits(CollectionView.GetIndexPathsForSelectedItems()); + switch (mode) { case SelectionMode.None: diff --git a/src/Controls/src/Core/Platform/iOS/Extensions/AcessibilityExtensions.cs b/src/Controls/src/Core/Platform/iOS/Extensions/AcessibilityExtensions.cs index 304bdca3ecce..649b5e2ce0f0 100644 --- a/src/Controls/src/Core/Platform/iOS/Extensions/AcessibilityExtensions.cs +++ b/src/Controls/src/Core/Platform/iOS/Extensions/AcessibilityExtensions.cs @@ -2,8 +2,48 @@ namespace Microsoft.Maui.Controls.Platform; -internal static class AcessibilityExtensions +internal static class AccessibilityExtensions { + internal static void UpdateSelectedAccessibility(this UICollectionViewCell cell, bool selected) + { + // Catalyst and iOS Simulators applies/removes the 'Selected' trait to the cell automatically. + // iOS Devices do not apply the 'Selected' trait automatically to the cell unless VoiceOver is on. + // On iOS, the 'Selected' trait needs to be applied to the first child of the cell for VoiceOver to announce it. +#if IOS + if (cell.ContentView is not null && cell.ContentView.Subviews.Length > 0) + { + var firstChild = cell.ContentView.Subviews[0]; + + if (selected) + { + firstChild.AccessibilityTraits |= UIAccessibilityTrait.Selected; + } + else + { + firstChild.AccessibilityTraits &= ~UIAccessibilityTrait.Selected; + } + } +#endif + } + + internal static void ClearSelectedAccessibilityTraits(this UICollectionView collectionView, Foundation.NSIndexPath[] indices) + { + // Catalyst and iOS Simulators applies/removes the 'Selected' trait to the cell automatically. + // iOS Devices do not apply the 'Selected' trait automatically to the cell unless VoiceOver is on. + // On iOS, the 'Selected' trait needs to be applied to the first child of the cell for VoiceOver to announce it. +#if IOS + foreach (var index in indices) + { + var cell = collectionView.CellForItem(index); + if (cell?.ContentView is not null && cell.ContentView.Subviews.Length > 0) + { + var firstChild = cell.ContentView.Subviews[0]; + firstChild.AccessibilityTraits &= ~UIAccessibilityTrait.Selected; + } + } +#endif + } + internal static void UpdateAccessibilityTraits(this UICollectionView collectionView, SelectableItemsView itemsView) { foreach (var subview in collectionView.Subviews) diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue21375.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue21375.xaml new file mode 100644 index 000000000000..7723a38eec01 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue21375.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + +