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

Commit 54e3e94

Browse files
jamesmontemagnoPureWeenjsuarezruizrachelkangjfversluis
authored
Base implementations for SemanticEffect and SemanticOrderView (#1240)
* Base implementations for SemanticEffect and SemanticOrderView * - uwp and iOS fix * - uwp tab index * - hint and description * - fix iOS to auto set to important for accessibility * Add samples and fix up crashes * - for UI Test reason switch to using a delegate for reading content Description on Android * - use the delegate if the automation id is set * - fixes * - set yes less often * Remove IsInAccessibleTree from PancakeView doesn't make a big difference on Android, but the property worsens accessibility on iOS * Update SemanticEffectPage.xaml * Update SemanticOrderViewPage.xaml * Update SemanticOrderViewPage.xaml.cs * Fix Semantic Description on Android * Reword SemanticEffect about * - added temporary workaround for making a screen clickable * Fix SemanticOrderViewPage sample * Fix NRE thrown on back nav from iOS SemanticOrderView page * - fix UWP router * - fix null checks * - changed first button to button so uwp sample is interesting * Clean up code - Pedro's feedback Co-authored-by: Shane Neuville <[email protected]> Co-authored-by: Javier Suárez <[email protected]> Co-authored-by: Rachel Kang <[email protected]> Co-authored-by: Gerald Versluis <[email protected]>
1 parent 9902e85 commit 54e3e94

22 files changed

+778
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Android.Content;
2+
using Xamarin.CommunityToolkit.Sample.Droid;
3+
using Xamarin.CommunityToolkit.Sample.Pages.Views;
4+
using Xamarin.Forms;
5+
using Xamarin.Forms.Platform.Android;
6+
7+
// This is a temporary fix for an issue in forms that will be fixed in a later release of 5.0
8+
// https://github.com/xamarin/Xamarin.Forms/pull/14089
9+
[assembly: ExportRenderer(typeof(SemanticOrderViewPage), typeof(AccessiblePageRenderer))]
10+
11+
namespace Xamarin.CommunityToolkit.Sample.Droid
12+
{
13+
public class AccessiblePageRenderer : PageRenderer
14+
{
15+
public AccessiblePageRenderer(Context context)
16+
: base(context)
17+
{
18+
}
19+
20+
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
21+
{
22+
base.OnElementChanged(e);
23+
Clickable = false;
24+
}
25+
26+
protected override void OnAttachedToWindow()
27+
{
28+
base.OnAttachedToWindow();
29+
DisableFocusableInTouchMode();
30+
}
31+
32+
protected override void AttachViewToParent(global::Android.Views.View? child, int index, LayoutParams? @params)
33+
{
34+
base.AttachViewToParent(child, index, @params);
35+
DisableFocusableInTouchMode();
36+
}
37+
38+
void DisableFocusableInTouchMode()
39+
{
40+
var view = Parent;
41+
var className = $"{view?.GetType().Name}";
42+
43+
while (!className.Contains("PlatformRenderer") && view != null)
44+
{
45+
view = view.Parent;
46+
className = $"{view?.GetType().Name}";
47+
}
48+
49+
if (view is global::Android.Views.View androidView)
50+
{
51+
androidView.Focusable = false;
52+
androidView.FocusableInTouchMode = false;
53+
}
54+
}
55+
}
56+
}

samples/XCT.Sample.Android/Xamarin.CommunityToolkit.Sample.Android.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,4 @@
130130
<UserProperties XamarinHotReloadXFormsNugetUpgradeInfoBarXamarinCommunityToolkitSampleAndroidHideInfoBar="True" />
131131
</VisualStudio>
132132
</ProjectExtensions>
133-
</Project>
133+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<pages:BasePage xmlns="http://xamarin.com/schemas/2014/forms"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
x:Class="Xamarin.CommunityToolkit.Sample.Pages.Effects.SemanticEffectPage"
5+
xmlns:pages="clr-namespace:Xamarin.CommunityToolkit.Sample.Pages"
6+
xmlns:xct="http://xamarin.com/schemas/2020/toolkit">
7+
<ContentPage.Content>
8+
<ScrollView>
9+
<StackLayout Padding="20">
10+
<Label Text="I have no heading" xct:SemanticEffect.HeadingLevel="None"/>
11+
<Label Text="I am a heading 1" xct:SemanticEffect.HeadingLevel="Level1"/>
12+
<Label Text="I am a heading 2" xct:SemanticEffect.HeadingLevel="Level2"/>
13+
<Label Text="I am a heading 3" xct:SemanticEffect.HeadingLevel="Level3"/>
14+
<Label Text="I am a heading 4" xct:SemanticEffect.HeadingLevel="Level4"/>
15+
<Label Text="I am a heading 5" xct:SemanticEffect.HeadingLevel="Level5"/>
16+
<Label Text="I am a heading 6" xct:SemanticEffect.HeadingLevel="Level6"/>
17+
<Label Text="I am a heading 7" xct:SemanticEffect.HeadingLevel="Level7"/>
18+
<Label Text="I am a heading 8" xct:SemanticEffect.HeadingLevel="Level8"/>
19+
<Label Text="I am a heading 9" xct:SemanticEffect.HeadingLevel="Level9"/>
20+
21+
<Label Text="I am a label with an automation ID" AutomationId="labelAutomationIdTest" xct:SemanticEffect.Description="This is a semantic description" />
22+
23+
<Label Text="The button below has a semantic hint"/>
24+
<Button
25+
Text="Button with hint"
26+
xct:SemanticEffect.Hint="This is a hint that describes the button. For example, 'sends a message'"/>
27+
28+
<Label Text="The image below has a semantic description"/>
29+
<Image
30+
Source="{xct:ImageResource Id=Xamarin.CommunityToolkit.Sample.Images.logo.png}"
31+
xct:SemanticEffect.Description="This is a description that describes the image. For example, 'image of xamarin community toolkit logo'"/>
32+
</StackLayout>
33+
</ScrollView>
34+
</ContentPage.Content>
35+
</pages:BasePage>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+

2+
using Xamarin.Forms;
3+
4+
namespace Xamarin.CommunityToolkit.Sample.Pages.Effects
5+
{
6+
public partial class SemanticEffectPage : BasePage
7+
{
8+
public SemanticEffectPage()
9+
{
10+
InitializeComponent();
11+
}
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<pages:BasePage xmlns="http://xamarin.com/schemas/2014/forms"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
xmlns:pages="clr-namespace:Xamarin.CommunityToolkit.Sample.Pages"
5+
x:Class="Xamarin.CommunityToolkit.Sample.Pages.Views.SemanticOrderViewPage"
6+
xmlns:xct="http://xamarin.com/schemas/2020/toolkit">
7+
<ContentPage.Content>
8+
<StackLayout Margin="20">
9+
<Button Text="Element outside the Semantic View"></Button>
10+
<xct:SemanticOrderView x:Name="acv">
11+
<StackLayout>
12+
<Label x:Name="second" Text="Second" Margin="0,20" />
13+
<Button x:Name="third" Text="Third" Margin="0,20" />
14+
<Label x:Name="fourth" Text="Fourth" Margin="0,20" />
15+
<Button x:Name="fifth" Text="Fifth and last" Margin="0,20" />
16+
<Button x:Name="first" Text="First" Margin="0,20" />
17+
</StackLayout>
18+
</xct:SemanticOrderView>
19+
</StackLayout>
20+
</ContentPage.Content>
21+
</pages:BasePage>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Collections.Generic;
2+
3+
using Xamarin.Forms;
4+
5+
namespace Xamarin.CommunityToolkit.Sample.Pages.Views
6+
{
7+
public partial class SemanticOrderViewPage : BasePage
8+
{
9+
public SemanticOrderViewPage()
10+
{
11+
InitializeComponent();
12+
acv.ViewOrder = new List<View> { first, second, third, fourth, fifth };
13+
}
14+
}
15+
}

samples/XCT.Sample/ViewModels/Effects/EffectsGalleryViewModel.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ protected override IEnumerable<SectionModel> CreateItems() => new[]
1212
new SectionModel(
1313
typeof(SafeAreaEffectPage),
1414
nameof(SafeAreaEffect),
15-
"The SafeAreaEffect is an effectwill help to make sure that content isn't clipped by rounded device corners, the home indicator, or the sensor housing on an iPhone X (or alike)"),
15+
"The SafeAreaEffect is an effect that will help to make sure that content isn't clipped by rounded device corners, the home indicator, or the sensor housing on an iPhone X (or alike)"),
1616

1717
new SectionModel(
1818
typeof(RemoveBorderEffectPage),
@@ -42,7 +42,12 @@ protected override IEnumerable<SectionModel> CreateItems() => new[]
4242
new SectionModel(
4343
typeof(ShadowEffectPage),
4444
nameof(ShadowEffect),
45-
"The ShadowEffect allows all views to display shadow.")
45+
"The ShadowEffect allows all views to display shadow."),
46+
47+
new SectionModel(
48+
typeof(SemanticEffectPage),
49+
nameof(SemanticEffect),
50+
"The SemanticEffect allows you to set semantic properties for accessibility.")
4651
};
4752
}
4853
}

samples/XCT.Sample/ViewModels/Views/ViewsGalleryViewModel.cs

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ protected override IEnumerable<SectionModel> CreateItems() => new[]
3535
new SectionModel(typeof(RangeSliderPage), "RangeSlider",
3636
"The RangeSlider is a slider with two thumbs allowing to select numeric ranges"),
3737

38+
new SectionModel(typeof(SemanticOrderViewPage), "SemanticOrderView",
39+
"Set accessiblity ordering on views"),
40+
3841
new SectionModel(typeof(SnackBarPage), "SnackBar/Toast",
3942
"Show SnackBar, Toast etc"),
4043

src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/EffectIds.shared.cs

+5
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,10 @@ sealed class EffectIds
5050
/// Effect Id for <see cref="ShadowEffect"/>
5151
/// </summary>
5252
public static string ShadowEffect => $"{effectResolutionGroupName}.{nameof(ShadowEffect)}";
53+
54+
/// <summary>
55+
/// Effect Id for <see cref="SemanticEffect"/>
56+
/// </summary>
57+
public static string Semantic => $"{effectResolutionGroupName}.{nameof(SemanticEffectRouter)}";
5358
}
5459
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using Xamarin.Forms;
6+
using Xamarin.CommunityToolkit.Effects.Semantic;
7+
8+
namespace Xamarin.CommunityToolkit.Effects
9+
{
10+
public static class SemanticEffect
11+
{
12+
public static readonly BindableProperty HeadingLevelProperty =
13+
BindableProperty.CreateAttached("HeadingLevel", typeof(HeadingLevel), typeof(SemanticEffect), HeadingLevel.None, propertyChanged: OnPropertyChanged);
14+
15+
public static HeadingLevel GetHeadingLevel(BindableObject view) => (HeadingLevel)view.GetValue(HeadingLevelProperty);
16+
17+
public static void SetHeadingLevel(BindableObject view, HeadingLevel value) => view.SetValue(HeadingLevelProperty, value);
18+
19+
20+
public static readonly BindableProperty DescriptionProperty = BindableProperty.CreateAttached("Description", typeof(string), typeof(SemanticEffect), default(string), propertyChanged: OnPropertyChanged);
21+
22+
public static string GetDescription(BindableObject bindable) => (string)bindable.GetValue(DescriptionProperty);
23+
24+
public static void SetDescription(BindableObject bindable, string value) => bindable.SetValue(DescriptionProperty, value);
25+
26+
public static readonly BindableProperty HintProperty = BindableProperty.CreateAttached("Hint", typeof(string), typeof(SemanticEffect), default(string), propertyChanged: OnPropertyChanged);
27+
28+
public static string GetHint(BindableObject bindable) => (string)bindable.GetValue(HintProperty);
29+
30+
public static void SetHint(BindableObject bindable, string value) => bindable.SetValue(HintProperty, value);
31+
32+
static void OnPropertyChanged(BindableObject bindable, object oldValue, object newValue)
33+
{
34+
if (bindable is not View view)
35+
return;
36+
37+
if (view.Effects.FirstOrDefault(x => x is SemanticEffectRouter) == null)
38+
{
39+
view.Effects.Add(new SemanticEffectRouter());
40+
}
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using AndroidX.Core.View;
2+
using System.ComponentModel;
3+
using Xamarin.Forms;
4+
using Xamarin.CommunityToolkit.Effects;
5+
using Effects = Xamarin.CommunityToolkit.Android.Effects;
6+
using AndroidX.Core.View.Accessibiity;
7+
using Android.Widget;
8+
9+
[assembly: ExportEffect(typeof(Effects.SemanticEffectRouter), nameof(SemanticEffectRouter))]
10+
11+
namespace Xamarin.CommunityToolkit.Android.Effects
12+
{
13+
/// <summary>
14+
/// Android implementation of the <see cref="SemanticEffect" />
15+
/// </summary>
16+
public class SemanticEffectRouter : SemanticEffectRouterBase<SemanticEffectRouter>
17+
{
18+
SemanticAccessibilityDelegate? semanticAccessibilityDelegate;
19+
20+
protected override void Update(global::Android.Views.View view, SemanticEffectRouter effect)
21+
{
22+
var isHeading = SemanticEffect.GetHeadingLevel(Element) != CommunityToolkit.Effects.Semantic.HeadingLevel.None;
23+
ViewCompat.SetAccessibilityHeading(view, isHeading);
24+
var desc = SemanticEffect.GetDescription(Element);
25+
var hint = SemanticEffect.GetHint(Element);
26+
27+
if (!string.IsNullOrEmpty(hint) || !string.IsNullOrEmpty(desc))
28+
{
29+
if (semanticAccessibilityDelegate == null)
30+
{
31+
semanticAccessibilityDelegate = new SemanticAccessibilityDelegate(Element);
32+
ViewCompat.SetAccessibilityDelegate(view, semanticAccessibilityDelegate);
33+
}
34+
}
35+
else if (semanticAccessibilityDelegate != null)
36+
{
37+
semanticAccessibilityDelegate = null;
38+
ViewCompat.SetAccessibilityDelegate(view, null);
39+
}
40+
41+
if (semanticAccessibilityDelegate != null)
42+
{
43+
semanticAccessibilityDelegate.Element = Element;
44+
view.ImportantForAccessibility = global::Android.Views.ImportantForAccessibility.Yes;
45+
}
46+
}
47+
48+
protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
49+
{
50+
base.OnElementPropertyChanged(args);
51+
52+
if (args.PropertyName == SemanticEffect.HeadingLevelProperty.PropertyName ||
53+
args.PropertyName == SemanticEffect.DescriptionProperty.PropertyName ||
54+
args.PropertyName == SemanticEffect.HintProperty.PropertyName)
55+
{
56+
Update();
57+
}
58+
}
59+
60+
class SemanticAccessibilityDelegate : AccessibilityDelegateCompat
61+
{
62+
public Element Element { get; set; }
63+
64+
public SemanticAccessibilityDelegate(Element element)
65+
{
66+
Element = element;
67+
}
68+
69+
public override void OnInitializeAccessibilityNodeInfo(global::Android.Views.View host, AccessibilityNodeInfoCompat info)
70+
{
71+
base.OnInitializeAccessibilityNodeInfo(host, info);
72+
73+
if (Element == null)
74+
return;
75+
76+
if (info == null)
77+
return;
78+
79+
var hint = SemanticEffect.GetHint(Element);
80+
if (!string.IsNullOrEmpty(hint))
81+
{
82+
info.HintText = hint;
83+
84+
if (host is EditText)
85+
info.ShowingHintText = false;
86+
}
87+
88+
var desc = SemanticEffect.GetDescription(Element);
89+
if (!string.IsNullOrEmpty(desc))
90+
{
91+
info.ContentDescription = desc;
92+
}
93+
}
94+
}
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.ComponentModel;
2+
using UIKit;
3+
using Xamarin.CommunityToolkit.Effects;
4+
using Xamarin.Forms;
5+
using Effects = Xamarin.CommunityToolkit.iOS.Effects;
6+
7+
[assembly: ExportEffect(typeof(Effects.SemanticEffectRouter), nameof(SemanticEffectRouter))]
8+
9+
namespace Xamarin.CommunityToolkit.iOS.Effects
10+
{
11+
/// <summary>
12+
/// iOS implementation of the <see cref="SemanticEffect" />
13+
/// </summary>
14+
public class SemanticEffectRouter : SemanticEffectRouterBase<SemanticEffectRouter>
15+
{
16+
public SemanticEffectRouter()
17+
{
18+
}
19+
20+
protected override void Update(UIView view, SemanticEffectRouter effect)
21+
{
22+
var isHeading = SemanticEffect.GetHeadingLevel(Element) != CommunityToolkit.Effects.Semantic.HeadingLevel.None;
23+
24+
if (isHeading)
25+
view.AccessibilityTraits |= UIAccessibilityTrait.Header;
26+
else
27+
view.AccessibilityTraits &= ~UIAccessibilityTrait.Header;
28+
29+
var desc = SemanticEffect.GetDescription(Element);
30+
var hint = SemanticEffect.GetHint(Element);
31+
view.AccessibilityLabel = desc;
32+
view.AccessibilityHint = hint;
33+
34+
// UIControl elements automatically have IsAccessibilityElement set to true
35+
if (view is not UIControl && (!string.IsNullOrWhiteSpace(hint) || !string.IsNullOrWhiteSpace(desc)))
36+
{
37+
view.IsAccessibilityElement = true;
38+
}
39+
}
40+
41+
protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
42+
{
43+
base.OnElementPropertyChanged(args);
44+
45+
if (args.PropertyName == SemanticEffect.HeadingLevelProperty.PropertyName ||
46+
args.PropertyName == SemanticEffect.DescriptionProperty.PropertyName ||
47+
args.PropertyName == SemanticEffect.HintProperty.PropertyName)
48+
{
49+
Update();
50+
}
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)