Skip to content

[iOS] Inherit AccessibilityTraits for views wrapped inside a WrapperView #27088

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,12 @@ _handler.VirtualView is View v &&
(PlatformView.AccessibilityTraits & UIAccessibilityTrait.Button) != UIAccessibilityTrait.Button)
{
PlatformView.AccessibilityTraits |= UIAccessibilityTrait.Button;

if (PlatformView is WrapperView wrapperView && wrapperView.Subviews.Length > 0)
{
wrapperView.Subviews[0].AccessibilityTraits = wrapperView.AccessibilityTraits;
}

_addedFlags |= UIAccessibilityTrait.Button;
if (OperatingSystem.IsIOSVersionAtLeast(13) || OperatingSystem.IsMacCatalystVersionAtLeast(13)
#if TVOS
Expand Down Expand Up @@ -786,6 +792,11 @@ void GestureRecognizersOnCollectionChanged(object? sender, NotifyCollectionChang
{
PlatformView.AccessibilityTraits &= ~_addedFlags;

if (PlatformView is WrapperView wrapperView && wrapperView.Subviews.Length > 0)
{
wrapperView.Subviews[0].AccessibilityTraits = wrapperView.AccessibilityTraits;
}

if (OperatingSystem.IsIOSVersionAtLeast(13) || OperatingSystem.IsMacCatalystVersionAtLeast(13))
{
if (_defaultAccessibilityRespondsToUserInteraction != null)
Expand Down
23 changes: 22 additions & 1 deletion src/Controls/src/Core/Platform/SemanticExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,42 @@ internal static bool ControlsAccessibilityDelegateNeeded(this View virtualView)
=> virtualView.TapGestureRecognizerNeedsDelegate();

internal static bool TapGestureRecognizerNeedsDelegate(this View virtualView)
=> virtualView.TapGestureRecognizerNeedsButtonAnnouncement();
=> virtualView.TapGestureRecognizerNeedsActionClick();

internal static bool TapGestureRecognizerNeedsActionClick(this View virtualView)
{
foreach (var gesture in virtualView.GestureRecognizers)
{
#if MACCATALYST || ANDROID
// On Catalyst, will appear as "button, you are currently on a button, to click this button, press control + Option + space".
// On Android, will appear as "double tap to activate" in TalkBack.
// You are able to click or activate these TGR multiple times on Android and Catalyst, but we cannot secondary click with Catalyst or Android using the provided prompt.
// Hence, we should not mark as button for the secondary click.
if (gesture is TapGestureRecognizer tgr)
{
return (tgr.Buttons & ButtonsMask.Primary) == ButtonsMask.Primary;
}
#elif IOS
// On iOS, will appear as "Button" in VoiceOver and can tap Secondary button
if (gesture is TapGestureRecognizer tgr)
{
return true;
}
#else
//Accessibility can't handle Tap Recognizers with > 1 tap
if (gesture is TapGestureRecognizer tgr && tgr.NumberOfTapsRequired == 1)
{
return (tgr.Buttons & ButtonsMask.Primary) == ButtonsMask.Primary;
}
#endif
}
return false;
}

/// <summary>
/// On Android, we can have a separate prompt that will label the TapGestureRecognizer as "button".
/// This method is used to determine if we need to add that prompt.
/// </summary>
internal static bool TapGestureRecognizerNeedsButtonAnnouncement(this View virtualView)
{
foreach (var gesture in virtualView.GestureRecognizers)
Expand Down
192 changes: 192 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue26990.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue26990"
Title="Issue26990">
<VerticalStackLayout>
<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Label Text="Single Primary Tap" x:Name="SPT">
<Label.GestureRecognizers>
<TapGestureRecognizer
NumberOfTapsRequired="1" />
</Label.GestureRecognizers>
</Label>
<Label Text="Type: " x:Name="SPT_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Label Text="Multiple Primary Tap" x:Name="MPT">
<Label.GestureRecognizers>
<TapGestureRecognizer
NumberOfTapsRequired="2" />
</Label.GestureRecognizers>
</Label>
<Label Text="Type: " x:Name="MPT_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Label Text="Single Secondary Tap" x:Name="SST">
<Label.GestureRecognizers>
<TapGestureRecognizer
NumberOfTapsRequired="1"
Buttons="Secondary" />
</Label.GestureRecognizers>
</Label>
<Label Text="Type: " x:Name="SST_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Label Text="Multiple Secondary Tap" x:Name="MST">
<Label.GestureRecognizers>
<TapGestureRecognizer
NumberOfTapsRequired="2"
Buttons="Secondary" />
</Label.GestureRecognizers>
</Label>
<Label Text="Type: " x:Name="MST_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Label Text="Single Either Tap" x:Name="SET">
<Label.GestureRecognizers>
<TapGestureRecognizer x:Name="SETGR"
NumberOfTapsRequired="1" />
</Label.GestureRecognizers>
</Label>
<Label Text="Type: " x:Name="SET_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Label Text="Multiple Either Tap" x:Name="MET">
<Label.GestureRecognizers>
<TapGestureRecognizer x:Name="METGR"
NumberOfTapsRequired="2" />
</Label.GestureRecognizers>
</Label>
<Label Text="Type: " x:Name="MET_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Label Text="Single Primary Tap" Background="LightBlue" x:Name="SPTB">
<Label.GestureRecognizers>
<TapGestureRecognizer
NumberOfTapsRequired="1" />
</Label.GestureRecognizers>
</Label>
<Label Text="Type: " x:Name="SPTB_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Label Text="Multiple Primary Tap" Background="LightBlue" x:Name="MPTB">
<Label.GestureRecognizers>
<TapGestureRecognizer
NumberOfTapsRequired="2" />
</Label.GestureRecognizers>
</Label>
<Label Text="Type: " x:Name="MPTB_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Label Text="Single Secondary Tap" Background="LightBlue" x:Name="SSTB">
<Label.GestureRecognizers>
<TapGestureRecognizer
NumberOfTapsRequired="1"
Buttons="Secondary" />
</Label.GestureRecognizers>
</Label>
<Label Text="Type: " x:Name="SSTB_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Label Text="Multiple Secondary Tap" Background="LightBlue" x:Name="MSTB">
<Label.GestureRecognizers>
<TapGestureRecognizer
NumberOfTapsRequired="2"
Buttons="Secondary" />
</Label.GestureRecognizers>
</Label>
<Label Text="Type: " x:Name="MSTB_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Label Text="Single Either Tap" Background="LightBlue" x:Name="SETB">
<Label.GestureRecognizers>
<TapGestureRecognizer x:Name="SEBTGR"
NumberOfTapsRequired="1" />
</Label.GestureRecognizers>
</Label>
<Label Text="Type: " x:Name="SETB_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Label Text="Multiple Either Tap" Background="LightBlue" x:Name="METB">
<Label.GestureRecognizers>
<TapGestureRecognizer x:Name="MEBTGR"
NumberOfTapsRequired="2" />
</Label.GestureRecognizers>
</Label>
<Label Text="Type: " x:Name="METB_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Border x:Name="BNS"
HeightRequest="50"
Background="Red"
WidthRequest="100">
<Border.GestureRecognizers>
<TapGestureRecognizer/>
</Border.GestureRecognizers>
</Border>
<Label Text="Type: " x:Name="BNS_label"/>
</VerticalStackLayout>
</Border>

<Border Stroke="Black" StrokeThickness="1" Margin="0,0,0,5" Padding="0.5">
<VerticalStackLayout>
<Border x:Name="BWS"
HeightRequest="50"
Background="Red"
WidthRequest="100">
<Border.GestureRecognizers>
<TapGestureRecognizer/>
</Border.GestureRecognizers>
<Border.Shadow>
<Shadow Brush="Black"
Offset="0,3"
Radius="10"
Opacity="0.5"/>
</Border.Shadow>
</Border>
<Label Text="Type: " x:Name="BWS_label"/>
</VerticalStackLayout>
</Border>

<Button Text="Reveal Accessibility Types" Clicked="Button_Clicked" AutomationId="RevealButton"/>

</VerticalStackLayout>
</ContentPage>
69 changes: 69 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue26990.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 26990, "Accessibility only gets set on WrapperView", PlatformAffected.iOS & PlatformAffected.macOS & PlatformAffected.Android)]
public partial class Issue26990 : ContentPage
{
public Issue26990()
{
InitializeComponent();
SETGR.Buttons = ButtonsMask.Primary | ButtonsMask.Secondary;
METGR.Buttons = ButtonsMask.Primary | ButtonsMask.Secondary;
SEBTGR.Buttons = ButtonsMask.Primary | ButtonsMask.Secondary;
MEBTGR.Buttons = ButtonsMask.Primary | ButtonsMask.Secondary;
}

private void Button_Clicked(object sender, EventArgs e)
{
SPT_label.Text = FindAccessibilityLabel(SPT);
MPT_label.Text = FindAccessibilityLabel(MPT);
SST_label.Text = FindAccessibilityLabel(SST);
MST_label.Text = FindAccessibilityLabel(MST);
SET_label.Text = FindAccessibilityLabel(SET);
MET_label.Text = FindAccessibilityLabel(MET);

SPTB_label.Text = FindAccessibilityLabel(SPTB);
MPTB_label.Text = FindAccessibilityLabel(MPTB);
SSTB_label.Text = FindAccessibilityLabel(SSTB);
MSTB_label.Text = FindAccessibilityLabel(MSTB);
SETB_label.Text = FindAccessibilityLabel(SETB);
METB_label.Text = FindAccessibilityLabel(METB);

BNS_label.Text = FindAccessibilityLabel(BNS);
BWS_label.Text = FindAccessibilityLabel(BWS);
}

string FindAccessibilityLabel(View v)
{
var plat = v.Handler?.PlatformView;

#if IOS || MACCATALYST
if (plat is UIKit.UIView view)
{
bool isButton = (view.AccessibilityTraits & UIKit.UIAccessibilityTrait.Button) == UIKit.UIAccessibilityTrait.Button;
return $"Type: {(isButton ? "Button" : "Not Button")}";
}
#elif ANDROID
if (plat is Android.Views.View view && OperatingSystem.IsAndroidVersionAtLeast(29))
{
var nodeInfo = AndroidX.Core.View.Accessibility.AccessibilityNodeInfoCompat.Obtain(view);
var del = AndroidX.Core.View.ViewCompat.GetAccessibilityDelegate(view);

// Make sure the delegate has a chance to modify the info
if (del != null && nodeInfo is not null)
{
del.OnInitializeAccessibilityNodeInfo(view, nodeInfo);
}
else
{
return "Type: Could not get node info";
}

string className = nodeInfo?.ClassName?.ToString() ?? "null";
bool isButton = className == "android.widget.Button";

return $"Type: {(isButton ? "Button" : "Not Button")}";
}
#endif
return "Type:";
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#if !WINDOWS
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Issue26990 : _IssuesUITest
{
public Issue26990(TestDevice testDevice) : base(testDevice)
{
}

public override string Issue => "Accessibility only gets set on WrapperView";

[Test]
[Category(UITestCategories.Gestures)]
public void WrapperViewsDoNotBlockTapGestureRecognizerAccessibility()
{
App.WaitForElement("RevealButton").Click();
VerifyScreenshot();
}
}
}
#endif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading