Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

[Android] TalkBack now reads name and helptext on buttons. #9728

Closed
wants to merge 5 commits into from
Closed
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
@@ -0,0 +1,56 @@
using System.ComponentModel;
using AViews = Android.Views;
using Xamarin.Forms;
using Xamarin.Forms.ControlGallery.Android;
using Xamarin.Forms.Controls.Issues;
using Xamarin.Forms.Platform.Android;
using AndroidX.Core.View.Accessibiity;
using AndroidX.Core.View;

[assembly: ExportEffect(typeof(ContentDescriptionEffectRenderer), ContentDescriptionEffect.EffectName)]
namespace Xamarin.Forms.ControlGallery.Android
{
public class ContentDescriptionEffectRenderer : PlatformEffect
{
protected override void OnAttached()
{
}

protected override void OnDetached()
{
}

protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
{
System.Diagnostics.Debug.WriteLine("OnElementPropertyChanged" + args.PropertyName);

var viewGroup = Control as AViews.ViewGroup;
var nativeView = Control as AViews.View;

if (nativeView != null && viewGroup != null && viewGroup.ChildCount > 0)
{
nativeView = viewGroup.GetChildAt(0);
}

if (nativeView == null)
{
return;
}

var info = AccessibilityNodeInfoCompat.Obtain(nativeView);
ViewCompat.OnInitializeAccessibilityNodeInfo(nativeView, info);

System.Diagnostics.Debug.WriteLine(info.ContentDescription);
System.Diagnostics.Debug.WriteLine(nativeView.ContentDescription);

Element.SetValue(
ContentDescriptionEffectProperties.NameAndHelpTextProperty,
info.ContentDescription);

Element.SetValue(
ContentDescriptionEffectProperties.ContentDescriptionProperty,
nativeView.ContentDescription);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
<Compile Include="_58406EffectRenderer.cs" />
<Compile Include="_59457CustomRenderer.cs" />
<Compile Include="_60122ImageRenderer.cs" />
<Compile Include="ContentDescriptionEffectRenderer.cs" />
<Compile Include="..\Xamarin.Forms.Controls\GalleryPages\OpenGLGalleries\AdvancedOpenGLGallery.cs">
<Link>GalleryPages\AdvancedOpenGLGallery.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using System.ComponentModel;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;

#if UITEST
using CategoryAttribute = NUnit.Framework.CategoryAttribute;
using Xamarin.Forms.Core.UITests;
using Xamarin.UITest;
using NUnit.Framework;
#endif

namespace Xamarin.Forms.Controls.Issues
{

public static class ContentDescriptionEffectProperties
{
public static readonly BindableProperty ContentDescriptionProperty = BindableProperty.CreateAttached(
"ContentDescription",
typeof(string),
typeof(ContentDescriptionEffectProperties),
null);

public static readonly BindableProperty NameAndHelpTextProperty = BindableProperty.CreateAttached(
"NameAndHelpText",
typeof(string),
typeof(ContentDescriptionEffectProperties),
null);

public static string GetContentDescription(BindableObject view)
{
return (string)view.GetValue(ContentDescriptionProperty);
}

public static string GetNameAndHelpText(BindableObject view)
{
return (string)view.GetValue(NameAndHelpTextProperty);
}
}

public class ContentDescriptionEffect : RoutingEffect
{
public const string EffectName = "ContentDescriptionEffect";

public ContentDescriptionEffect() : base($"{Effects.ResolutionGroupName}.{EffectName}")
{
}
}

[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 5150, "AutomationProperties.Name, AutomationProperties.HelpText on Button not read by Android TalkBack", PlatformAffected.Android)]
public class Issue5150 : TestContentPage // or TestMasterDetailPage, etc ...
{
void AddView(StackLayout layout, View view, string labelPrefix, string automationId, string buttonName = null, string buttonHelp = null)
{
var automationIdLabel = new Label();
automationIdLabel.Text = $"AutomationId = {automationId}";
automationIdLabel.AutomationId = $"{labelPrefix}-automationIdLabel";

var contentDescriptionLabel = new Label();
contentDescriptionLabel.AutomationId = $"{labelPrefix}-contentDescriptionLabel";

var nameAndHelpTextLabel = new Label();
nameAndHelpTextLabel.AutomationId = $"{labelPrefix}-nameAndHelpTextLabel";

view.AutomationId = automationId;
view.Effects.Add(new ContentDescriptionEffect());
view.PropertyChanged += (object sender, PropertyChangedEventArgs e) => {
if (e.PropertyName == ContentDescriptionEffectProperties.ContentDescriptionProperty.PropertyName)
{
contentDescriptionLabel.Text = $"ContentDescription = {ContentDescriptionEffectProperties.GetContentDescription(view)}";
}

if (e.PropertyName == ContentDescriptionEffectProperties.NameAndHelpTextProperty.PropertyName)
{
nameAndHelpTextLabel.Text = $"Name + HelpText = {ContentDescriptionEffectProperties.GetNameAndHelpText(view)}";
}
};
layout.Children.Add(view);
layout.Children.Add(automationIdLabel);
layout.Children.Add(contentDescriptionLabel);
layout.Children.Add(nameAndHelpTextLabel);

AutomationProperties.SetIsInAccessibleTree(view, true);
AutomationProperties.SetName(view, buttonName);
AutomationProperties.SetHelpText(view, buttonHelp);
}

void AddButton(StackLayout layout, string labelPrefix, string automationId, string buttonText, string buttonName = null, string buttonHelp = null)
{
var button = new Button();
button.Text = buttonText;
AddView(layout, button, labelPrefix, automationId, buttonName, buttonHelp);
}

protected override void Init()
{
var scrollView = new ScrollView();
var layout = new StackLayout();
scrollView.Content = layout;

layout.Children.Add(new Label
{
Text = "On the Android platform, the 'Name + Help Text' " +
"labels below each button should match the text read by " +
"TalkBack without interferring with the AutomationId and " +
"ContentDescription."
});

AddButton(layout, "button1prop", "button1", "Button 1", "Name 1");
AddButton(layout, "button2prop", "button2", "Button 2", buttonHelp: "Help 2.");
AddButton(layout, "button3prop", "button3", "Button 3", "Name 3", "Help 3.");
AddButton(layout, "button4prop", "button4", null , buttonHelp: "Help 4.");

AddView(layout, new Switch(), "switch1prop", "switch1", "Switch 1 Name", "Switch Help 1.");

var image = new Image();
image.Source = ImageSource.FromFile("coffee.png");
AddView(layout, image, "image1prop", "image1", "Coffee", "Get some coffee!");

Content = scrollView;
}

#if UITEST
void Verify(string labelPrefix, string automationId, string expectedNameAndHelpText)
{
RunningApp.ScrollTo(automationId);
RunningApp.WaitForElement(q => q.Marked(automationId));
RunningApp.ScrollTo($"{labelPrefix}-nameAndHelpTextLabel");
RunningApp.WaitForElement(q => q.Text($"Name + HelpText = {expectedNameAndHelpText}"));
}

[Test]
[Category(UITestCategories.Button)]
#if !__ANDROID__
[Ignore("This test verifies ContentDescription is set on the Android platform.")]
#endif
public void Issue5150Test()
{
Verify("button1prop", "button1", "Name 1");
Verify("button2prop", "button2", "Button 2. Help 2.");
Verify("button3prop", "button3", "Name 3. Help 3.");
Verify("button4prop", "button4", "Help 4.");
Verify("switch1prop", "switch1", "Switch 1 Name. Switch Help 1.");
Verify("image1prop", "image1", "Coffee. Get some coffee!");
}
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Issue4138.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue4314.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue3318.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue5150.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue4493.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue5172.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue5204.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using AndroidX.Core.View;
using Android.Views;
using Android.Widget;
using AView = Android.Views.View;
Expand Down Expand Up @@ -55,15 +56,31 @@ internal static void SetBasicContentDescription(

if (defaultContentDescription == null)
defaultContentDescription = control.ContentDescription;

string value = ConcatenateNameAndHelpText(bindableObject);
string contentDescription = !string.IsNullOrWhiteSpace(value) ? value : defaultContentDescription;
string automationId = (bindableObject as Element)?.AutomationId;

var contentDescription = !string.IsNullOrWhiteSpace(value) ? value : defaultContentDescription;

if (String.IsNullOrWhiteSpace(contentDescription) && bindableObject is Element element)
contentDescription = element.AutomationId;

control.ContentDescription = contentDescription;
if (!string.IsNullOrWhiteSpace(automationId) && !string.IsNullOrWhiteSpace(contentDescription))
{
var target = control;
if (control is IButtonLayoutRenderer buttonLayoutRenderer)
{
target = buttonLayoutRenderer.View;
}
else if (control is AppCompat.SwitchRenderer switchRenderer)
{
target = switchRenderer.Control;
}
target.ContentDescription = automationId;
ViewCompat.SetAccessibilityDelegate(target, new NameAndHelpTextAccessibilityDelegate {
AccessibilityText = contentDescription
});
}
else
{
control.ContentDescription = string.IsNullOrWhiteSpace(contentDescription) ? automationId : contentDescription;
}
}

internal static void SetContentDescription(
Expand Down Expand Up @@ -238,15 +255,13 @@ internal static void AccessibilitySettingsChanged(AView control, Element element

internal static string ConcatenateNameAndHelpText(BindableObject Element)
{
var name = (string)Element.GetValue(AutomationProperties.NameProperty);
var name = (string)Element.GetValue(AutomationProperties.NameProperty) ?? (Element as Button)?.Text;
var helpText = (string)Element.GetValue(AutomationProperties.HelpTextProperty);

if (string.IsNullOrWhiteSpace(name))
return helpText;
if (string.IsNullOrWhiteSpace(helpText))
return name;

return $"{name}. {helpText}";
return string.IsNullOrWhiteSpace(helpText) ? $"{name}" : $"{name}. {helpText}";
}

void OnElementChanged(object sender, VisualElementChangedEventArgs e)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using Android.Runtime;

using AndroidX.Core.View;
using AndroidX.Core.View.Accessibiity;

namespace Xamarin.Forms.Platform.Android.FastRenderers
{
public class NameAndHelpTextAccessibilityDelegate : AccessibilityDelegateCompat
{

public string AccessibilityText { get; set; }

public NameAndHelpTextAccessibilityDelegate()
{
}

protected NameAndHelpTextAccessibilityDelegate(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
}

public override void OnInitializeAccessibilityNodeInfo(global::Android.Views.View host, AccessibilityNodeInfoCompat info)
{
base.OnInitializeAccessibilityNodeInfo(host, info);
info.ContentDescription = AccessibilityText;
}

}
}