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

Convert to using list of accessibility elements for iOS tab indexes #11077

Merged
merged 4 commits into from
Jul 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;


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

namespace Xamarin.Forms.Controls.Issues
{
[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 7606, "[Bug] When a view appears it is not accessible via VoiceOver",
PlatformAffected.iOS)]
#if UITEST
[NUnit.Framework.Category(Core.UITests.UITestCategories.Github10000)]
[NUnit.Framework.Category(UITestCategories.ManualReview)]
[NUnit.Framework.Category(UITestCategories.Accessibility)]
#endif
public class Issue7606 : TestContentPage
{
protected override void Init()
{
Label visibilityLabel = new Label()
{
Text = "Swipe right and I should be read by voice over.",
IsVisible = false
};

Content = new StackLayout()
{
Children =
{
new Button()
{
Text = "Click Me",
Command = new Command(() => visibilityLabel.IsVisible = !visibilityLabel.IsVisible)
},
visibilityLabel
}
};
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;


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

namespace Xamarin.Forms.Controls.Issues
{
[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 8613, "[Bug] Accessibility, screenreader ignores or skips items in nested stacklayout",
PlatformAffected.iOS)]
#if UITEST
[NUnit.Framework.Category(Core.UITests.UITestCategories.Github10000)]
[NUnit.Framework.Category(UITestCategories.ManualReview)]
[NUnit.Framework.Category(UITestCategories.Accessibility)]
#endif
public class Issue8613 : TestContentPage
{
Label CreateLabel(bool? IsInAccessibleTree, string text)
{
Label label = new Label()
{
Text = text
};

if(IsInAccessibleTree.HasValue)
AutomationProperties.SetIsInAccessibleTree(label, IsInAccessibleTree);

return label;
}

Entry CreateEntry(bool IsInAccessibleTree, string placeholderText)
{
Entry entry = new Entry()
{
Placeholder = placeholderText
};

AutomationProperties.SetIsInAccessibleTree(entry, IsInAccessibleTree);
return entry;
}

protected override void Init()
{
// Based on Sample
// https://github.com/xamarin/xamarin-forms-samples/blob/master/UserInterface/Accessibility/Accessibility/AccessibilityPage.xaml
Content = new ScrollView()
{
Content =
new StackLayout()
{
Children =
{
CreateLabel(true, "Voice Over Swiping should progress sequentially through all visible elements"),
CreateLabel(true, "Second Label IsInAccessibleTree = true"),
new StackLayout()
{
Children =
{
new Label()
{
Text = "Enter Your Name: ",
},
CreateEntry(true, "If Voice Over swiping gets stuck here test has failed"),
},
},
CreateLabel(true, "Label After the Entry")
},
},
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;


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

namespace Xamarin.Forms.Controls.Issues
{
[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 8691, "[Bug] TabIndex is ignored for first element on page for VoiceOver",
PlatformAffected.iOS)]
#if UITEST
[NUnit.Framework.Category(Core.UITests.UITestCategories.Github10000)]
[NUnit.Framework.Category(UITestCategories.ManualReview)]
[NUnit.Framework.Category(UITestCategories.Accessibility)]
#endif
public class Issue8691 : TestContentPage
{
protected override void Init()
{

Content = new StackLayout()
{
Children =
{
new Label()
{
Text = "2nd TabIndex",
TabIndex = 20
},
new Label()
{
Text = "I should be the first element focused when voice over is on",
TabIndex = 10
},
new Label()
{
Text = "3rd TabIndex",
TabIndex = 30
},
}
};
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;


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

namespace Xamarin.Forms.Controls.Issues
{
[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 9137, "A11y: Image in a11y tree stops voiceover from hopping to the next element",
PlatformAffected.iOS)]
#if UITEST
[NUnit.Framework.Category(Core.UITests.UITestCategories.Github10000)]
[NUnit.Framework.Category(UITestCategories.ManualReview)]
[NUnit.Framework.Category(UITestCategories.Accessibility)]
#endif
public class Issue9137 : TestContentPage
{
Label CreateLabel(bool IsInAccessibleTree, string text)
{
Label label = new Label()
{
Text = text
};
AutomationProperties.SetIsInAccessibleTree(label, IsInAccessibleTree);
return label;
}

protected override void Init()
{
Content = new StackLayout()
{
Children =
{
new StackLayout()
{
Children =
{
CreateLabel(false, "IsInAccessibleTree is false")
},
},
new StackLayout()
{
Children =
{
new StackLayout()
{
Children =
{
new Label()
{
Text = "Turn Voice Over on and verify that you can swipe all the way forward and then backwards. If you get stuck toggling between the same two elements test has failed",
TabIndex = 10
},
new Image()
{
TabIndex = 20,
Source = "coffee.png"
},
},
},
new Label()
{
Text = "Tab Index 70",
TabIndex = 70
}
},
},
}
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Issue10744.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue10909.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8613.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue9137.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8691.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue7606.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue11137.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue11106.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8291.cs" />
Expand Down
1 change: 1 addition & 0 deletions Xamarin.Forms.Core.UITests.Shared/UITestCategories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ internal static class UITestCategories
public const string Github5000 = "Github5000";
public const string Github10000 = "Github10000";
public const string RadioButton = "RadioButton";
public const string Accessibility = "Accessibility";
public const string Shape = "Shape";
}
}
59 changes: 23 additions & 36 deletions Xamarin.Forms.Platform.iOS/Renderers/PageContainer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Foundation;
using ObjCRuntime;
using System;
using System.Collections.Generic;
using UIKit;
Expand All @@ -8,8 +9,9 @@ namespace Xamarin.Forms.Platform.iOS
internal class PageContainer : UIView, IUIAccessibilityContainer
{
readonly IAccessibilityElementsController _parent;
List<NSObject> _accessibilityElements = null;
NSArray _accessibilityElements = null;
bool _disposed;
bool _loaded;

public PageContainer(IAccessibilityElementsController parent)
{
Expand All @@ -22,22 +24,40 @@ public PageContainer()
IsAccessibilityElement = false;
}

List<NSObject> AccessibilityElements
public override bool IsAccessibilityElement
{
get => false;
set => base.IsAccessibilityElement = value;
}

[Internals.Preserve(Conditional = true)]
public virtual NSArray AccessibilityElements
{
[Export("accessibilityElements", ArgumentSemantic.Copy)]
get
{
if (_loaded)
return _accessibilityElements;

// lazy-loading this list so that the expensive call to GetAccessibilityElements only happens when VoiceOver is on.
if (_accessibilityElements == null || _accessibilityElements.Count == 0)
{
_accessibilityElements = _parent.GetAccessibilityElements();
var elements =_parent.GetAccessibilityElements();
if(elements != null)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation for this property says the default is null
https://developer.apple.com/documentation/objectivec/nsobject/1615147-accessibilityelements

Containers can implement this property instead of the dynamic methods to support the retrieval of the contained elements. The default value of this property is nil.

When I do that it seems like the accessibility just returns to the default behavior. I added all the repro's from the PR listed here #9702 and I tested the Accessibility demo on xamarin-samples.

We might need to add the setAccessibilityElements side of this as well for certain edge cases.

For now we'll keep it simple and see if that works

{
_accessibilityElements = NSArray.FromNSObjects(elements.ToArray());
}
}

_loaded = true;
return _accessibilityElements;
}
}

public void ClearAccessibilityElements()
{
_accessibilityElements = null;
_loaded = false;
}

protected override void Dispose(bool disposing)
Expand All @@ -49,38 +69,5 @@ protected override void Dispose(bool disposing)
}
base.Dispose(disposing);
}

[Export("accessibilityElementCount")]
[Internals.Preserve(Conditional = true)]
nint AccessibilityElementCount()
{
if (AccessibilityElements == null || AccessibilityElements.Count == 0)
return 0;

// Note: this will only be called when VoiceOver is enabled
return AccessibilityElements.Count;
}

[Export("accessibilityElementAtIndex:")]
[Internals.Preserve(Conditional = true)]
NSObject GetAccessibilityElementAt(nint index)
{
if (AccessibilityElements == null || AccessibilityElements.Count == 0)
return NSNull.Null;

// Note: this will only be called when VoiceOver is enabled
return AccessibilityElements[(int)index];
}

[Export("indexOfAccessibilityElement:")]
[Internals.Preserve(Conditional = true)]
int GetIndexOfAccessibilityElement(NSObject element)
{
if (AccessibilityElements == null || AccessibilityElements.Count == 0)
return int.MaxValue;

// Note: this will only be called when VoiceOver is enabled
return AccessibilityElements.IndexOf(element);
}
}
}