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

Commit

Permalink
[Android] TalkBack now reads name and helptext on buttons. (#13244) f…
Browse files Browse the repository at this point in the history
…ixes #5150

* Read name and helptext without disrupting automation id.

* - Fix Merge

* - remove if/defs

* - fix Legacy Renderers

* - fix UI Test

* - fix invalid AutomationId with cell reuse

* - fix Container so it's not read by accessibility

Co-authored-by: Ry Lowry <rylowry@gmail.com>
  • Loading branch information
PureWeen and ryl authored Dec 31, 2020
1 parent 324b426 commit 75f9a3f
Show file tree
Hide file tree
Showing 12 changed files with 336 additions and 49 deletions.
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,146 @@
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."
});

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 && __ANDROID__
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)]
[Category(UITestCategories.Accessibility)]
[Ignore("Automated Test not relevant until https://github.com/xamarin/Xamarin.Forms/issues/1529 is fixed")]
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
3 changes: 1 addition & 2 deletions Xamarin.Forms.Platform.Android/AppCompat/ButtonRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public class ButtonRenderer : ViewRenderer<Button, AppCompatButton>,
Typeface _defaultTypeface;
bool _isDisposed;
ButtonLayoutManager _buttonLayoutManager;
string _defaultContentDescription;

public ButtonRenderer(Context context) : base(context)
{
Expand All @@ -43,7 +42,7 @@ public ButtonRenderer()
global::Android.Widget.Button NativeButton => Control;

protected override void SetContentDescription()
=> AutomationPropertiesProvider.SetBasicContentDescription(this, Element, ref _defaultContentDescription);
=> base.SetContentDescription(false);

public override SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint)
{
Expand Down
25 changes: 21 additions & 4 deletions Xamarin.Forms.Platform.Android/AppCompat/FlyoutPageRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
using Android.Content;
using Android.OS;
using Android.Views;
using AndroidX.Core.Widget;
using AndroidX.DrawerLayout.Widget;
using AndroidX.Fragment.App;
using AView = Android.Views.View;

namespace Xamarin.Forms.Platform.Android
{
using Xamarin.Forms.Platform.Android.AppCompat;
using Xamarin.Forms.Platform.Android.FastRenderers;

public class FlyoutPageRenderer : DrawerLayout, IVisualElementRenderer, DrawerLayout.IDrawerListener, IManageFragments, ILifeCycleState
{
Expand All @@ -28,10 +28,10 @@ public class FlyoutPageRenderer : DrawerLayout, IVisualElementRenderer, DrawerLa
bool _disposed;
bool _isPresentingFromCore;
bool _presented;
bool _defaultAutomationSet;
VisualElementTracker _tracker;
FragmentManager _fragmentManager;
string _defaultContentDescription;
string _defaultHint;

public FlyoutPageRenderer(Context context) : base(context)
{
Expand Down Expand Up @@ -210,9 +210,26 @@ void IVisualElementRenderer.UpdateLayout()

bool ILifeCycleState.MarkedForDispose { get; set; } = false;

protected virtual void SetAutomationId(string id) => FastRenderers.AutomationPropertiesProvider.SetAutomationId(this, Element, id);
void SetupAutomationDefaults()
{
if (!_defaultAutomationSet)
{
_defaultAutomationSet = true;
AutomationPropertiesProvider.SetupDefaults(this, ref _defaultContentDescription);
}
}

protected virtual void SetContentDescription() => FastRenderers.AutomationPropertiesProvider.SetContentDescription(this, Element, ref _defaultContentDescription, ref _defaultHint);
protected virtual void SetAutomationId(string id)
{
SetupAutomationDefaults();
AutomationPropertiesProvider.SetAutomationId(this, Element, id);
}

protected virtual void SetContentDescription()
{
SetupAutomationDefaults();
AutomationPropertiesProvider.SetContentDescription(this, Element, _defaultContentDescription, null);
}

protected override void Dispose(bool disposing)
{
Expand Down
5 changes: 2 additions & 3 deletions Xamarin.Forms.Platform.Android/AppCompat/SwitchRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ public class SwitchRenderer : ViewRenderer<Switch, SwitchCompat>, CompoundButton
{
bool _disposed;
Drawable _defaultTrackDrawable;
string _defaultContentDescription;
bool _changedThumbColor;

public SwitchRenderer(Context context) : base(context)
Expand All @@ -27,8 +26,8 @@ public SwitchRenderer()
AutoPackage = false;
}

protected override void SetContentDescription()
=> AutomationPropertiesProvider.SetBasicContentDescription(this, Element, ref _defaultContentDescription);
protected override void SetContentDescription() =>
base.SetContentDescription(false);

void CompoundButton.IOnCheckedChangeListener.OnCheckedChanged(CompoundButton buttonView, bool isChecked)
{
Expand Down
Loading

0 comments on commit 75f9a3f

Please sign in to comment.