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

Commit 9e3ee43

Browse files
rachelkangTheCodeTravelerjsuarezruiz
authored
Implement SetSemanticFocus and Announce APIs (#1727)
* Add SemanticExtensions SetFocus and Announce * Add Windows Announce implementation * Update based on feedback Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Javier Suárez <javiersuarezruiz@hotmail.com>
1 parent 48a287a commit 9e3ee43

File tree

8 files changed

+224
-0
lines changed

8 files changed

+224
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<pages:BasePage x:Name="Page"
3+
x:Class="Xamarin.CommunityToolkit.Sample.Pages.Effects.SemanticExtensionsPage"
4+
xmlns="http://xamarin.com/schemas/2014/forms"
5+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
6+
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
7+
xmlns:pages="clr-namespace:Xamarin.CommunityToolkit.Sample.Pages">
8+
9+
<StackLayout Margin="30" Spacing="30">
10+
<Label
11+
Text="Turn on your screen reader and listen for what is read aloud. Explore the SemanticExtensions below!"
12+
TextColor="RoyalBlue"
13+
FontAttributes="Bold"
14+
FontSize="16"
15+
Margin="0,10"/>
16+
17+
<Button
18+
Text="Click to set semantic focus to label below"
19+
FontSize="14"
20+
Clicked="SetSemanticFocusButton_Clicked"/>
21+
22+
<Label
23+
x:Name="semanticFocusLabel"
24+
Text="Label receiving semantic focus"
25+
FontSize="14"/>
26+
27+
<Button
28+
Text="Activate to hear announcement text"
29+
HorizontalOptions="Center"
30+
Clicked="Announce_Clicked"/>
31+
</StackLayout>
32+
33+
</pages:BasePage>
34+
35+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
using Xamarin.CommunityToolkit.Extensions;
3+
4+
namespace Xamarin.CommunityToolkit.Sample.Pages.Effects
5+
{
6+
public partial class SemanticExtensionsPage
7+
{
8+
public SemanticExtensionsPage()
9+
{
10+
InitializeComponent();
11+
}
12+
13+
void SetSemanticFocusButton_Clicked(object sender, System.EventArgs e)
14+
{
15+
SemanticExtensions.SetSemanticFocus(semanticFocusLabel);
16+
}
17+
18+
void Announce_Clicked(object sender, EventArgs e)
19+
{
20+
SemanticExtensions.Announce("This is the announcement text");
21+
}
22+
}
23+
}

samples/XCT.Sample/ViewModels/Extensions/ExtensionsGalleryViewModel.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ protected override IEnumerable<SectionModel> CreateItems() => new[]
1313
typeof(ImageResourceExtensionPage),
1414
nameof(ImageResourceExtension),
1515
"A XAML extension that helps to display images from embedded resources"),
16+
17+
new SectionModel(
18+
typeof(SemanticExtensionsPage),
19+
nameof(SemanticExtensions),
20+
"An extension that offers further semantic APIs for screen reader accessibility"),
1621
};
1722
}
1823
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
using Android.Views.Accessibility;
3+
using Xamarin.Forms;
4+
using Xamarin.Forms.Platform.Android;
5+
using AView = Android.Views.View;
6+
using AApplication = Android.App.Application;
7+
using AContext = Android.Content.Context;
8+
9+
namespace Xamarin.CommunityToolkit.Extensions
10+
{
11+
public static partial class SemanticExtensions
12+
{
13+
static void PlatformSetSemanticFocus(this VisualElement element)
14+
{
15+
var androidView = Platform.GetRenderer(element);
16+
17+
if (androidView is not AView view)
18+
throw new NullReferenceException("Can't access view");
19+
20+
view.SendAccessibilityEvent(EventTypes.ViewFocused);
21+
}
22+
23+
static void PlatformAnnounce(string text)
24+
{
25+
var manager = AApplication.Context.GetSystemService(AContext.AccessibilityService) as AccessibilityManager;
26+
var announcement = AccessibilityEvent.Obtain();
27+
28+
if (manager == null || announcement == null)
29+
return;
30+
31+
if (!(manager.IsEnabled || manager.IsTouchExplorationEnabled))
32+
return;
33+
34+
announcement.EventType = EventTypes.Announcement;
35+
announcement.Text?.Add(new Java.Lang.String(text));
36+
manager.SendAccessibilityEvent(announcement);
37+
}
38+
}
39+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using Foundation;
3+
using UIKit;
4+
using Xamarin.Forms;
5+
using Xamarin.Forms.Platform.iOS;
6+
7+
namespace Xamarin.CommunityToolkit.Extensions
8+
{
9+
public static partial class SemanticExtensions
10+
{
11+
static void PlatformSetSemanticFocus(this VisualElement element)
12+
{
13+
var iosView = Platform.GetRenderer(element);
14+
15+
if (iosView == null)
16+
throw new NullReferenceException("Can't access view");
17+
18+
if (iosView is not NSObject nativeView)
19+
return;
20+
21+
UIAccessibility.PostNotification(UIAccessibilityPostNotification.LayoutChanged, nativeView);
22+
}
23+
24+
static void PlatformAnnounce(string text)
25+
{
26+
if (!UIAccessibility.IsVoiceOverRunning)
27+
return;
28+
29+
UIAccessibility.PostNotification(UIAccessibilityPostNotification.Announcement, new NSString(text));
30+
}
31+
}
32+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
using Xamarin.Forms;
3+
4+
namespace Xamarin.CommunityToolkit.Extensions
5+
{
6+
public static partial class SemanticExtensions
7+
{
8+
static void PlatformSetSemanticFocus(this VisualElement element) =>
9+
throw new NotSupportedException($"The current platform '{Device.RuntimePlatform}' does not support Xamarin.CommunityToolkit.SemanticExtensions");
10+
11+
static void PlatformAnnounce(string text) =>
12+
throw new NotSupportedException($"The current platform '{Device.RuntimePlatform}' does not support Xamarin.CommunityToolkit.SemanticExtensions");
13+
}
14+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Xamarin.Forms;
2+
3+
namespace Xamarin.CommunityToolkit.Extensions
4+
{
5+
public static partial class SemanticExtensions
6+
{
7+
/// <summary>
8+
/// Force semantic screen reader focus to specified element
9+
/// </summary>
10+
/// <param name="element"></param>
11+
public static void SetSemanticFocus(this VisualElement element) =>
12+
PlatformSetSemanticFocus(element);
13+
14+
public static void Announce(string text) =>
15+
PlatformAnnounce(text);
16+
}
17+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using Xamarin.Forms;
3+
using Windows.UI.Xaml;
4+
using Windows.UI.Xaml.Automation.Peers;
5+
using Windows.UI.Xaml.Media;
6+
7+
namespace Xamarin.CommunityToolkit.Extensions
8+
{
9+
public static partial class SemanticExtensions
10+
{
11+
internal const string ActivityId = "270FA098-C644-40A2-A0BE-A9BEA1222A1E";
12+
13+
static void PlatformSetSemanticFocus(this VisualElement element) =>
14+
throw new NotSupportedException($"The current platform '{Device.RuntimePlatform}' does not support Xamarin.CommunityToolkit.SemanticExtensions");
15+
16+
static void PlatformAnnounce(string text)
17+
{
18+
if (Window.Current == null)
19+
return;
20+
21+
var peer = FindAutomationPeer(Window.Current.Content);
22+
23+
// This GUID correlates to the internal messages used by UIA to perform an announce
24+
// You can extract it by using accessibility insights to monitor UIA events
25+
// If you're curious how this works then do a google search for the GUID
26+
peer?.RaiseNotificationEvent(
27+
AutomationNotificationKind.ActionAborted,
28+
AutomationNotificationProcessing.ImportantMostRecent,
29+
text,
30+
ActivityId);
31+
}
32+
33+
// This isn't great but it's the only way I've found to announce with WinUI.
34+
// You have to locate a control that has an automation peer and then use that
35+
// to perform the announce operation. This creates scenarios where the
36+
// screen might not have any automation peers on it to use but in those cases
37+
// you really shouldn't be using the announce API
38+
static AutomationPeer? FindAutomationPeer(DependencyObject depObj)
39+
{
40+
if (depObj == null)
41+
return null;
42+
43+
var count = VisualTreeHelper.GetChildrenCount(depObj);
44+
for (var i = 0; i < count; i++)
45+
{
46+
var child = VisualTreeHelper.GetChild(depObj, i);
47+
if (child is UIElement element && FrameworkElementAutomationPeer.FromElement(element) != null)
48+
{
49+
return FrameworkElementAutomationPeer.FromElement(element);
50+
}
51+
52+
var childItem = FindAutomationPeer(child);
53+
if (childItem != null)
54+
return childItem;
55+
}
56+
return null;
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)