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

Commit

Permalink
Base implementations for SemanticEffect and SemanticOrderView (#1240)
Browse files Browse the repository at this point in the history
* 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 <shneuvil@microsoft.com>
Co-authored-by: Javier Suárez <javiersuarezruiz@hotmail.com>
Co-authored-by: Rachel Kang <rachelkang@microsoft.com>
Co-authored-by: Gerald Versluis <gerald.versluis@microsoft.com>
  • Loading branch information
5 people authored May 7, 2021
1 parent 9902e85 commit 54e3e94
Show file tree
Hide file tree
Showing 22 changed files with 778 additions and 3 deletions.
56 changes: 56 additions & 0 deletions samples/XCT.Sample.Android/AccessiblePageRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Android.Content;
using Xamarin.CommunityToolkit.Sample.Droid;
using Xamarin.CommunityToolkit.Sample.Pages.Views;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

// This is a temporary fix for an issue in forms that will be fixed in a later release of 5.0
// https://github.com/xamarin/Xamarin.Forms/pull/14089
[assembly: ExportRenderer(typeof(SemanticOrderViewPage), typeof(AccessiblePageRenderer))]

namespace Xamarin.CommunityToolkit.Sample.Droid
{
public class AccessiblePageRenderer : PageRenderer
{
public AccessiblePageRenderer(Context context)
: base(context)
{
}

protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
Clickable = false;
}

protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
DisableFocusableInTouchMode();
}

protected override void AttachViewToParent(global::Android.Views.View? child, int index, LayoutParams? @params)
{
base.AttachViewToParent(child, index, @params);
DisableFocusableInTouchMode();
}

void DisableFocusableInTouchMode()
{
var view = Parent;
var className = $"{view?.GetType().Name}";

while (!className.Contains("PlatformRenderer") && view != null)
{
view = view.Parent;
className = $"{view?.GetType().Name}";
}

if (view is global::Android.Views.View androidView)
{
androidView.Focusable = false;
androidView.FocusableInTouchMode = false;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,4 @@
<UserProperties XamarinHotReloadXFormsNugetUpgradeInfoBarXamarinCommunityToolkitSampleAndroidHideInfoBar="True" />
</VisualStudio>
</ProjectExtensions>
</Project>
</Project>
35 changes: 35 additions & 0 deletions samples/XCT.Sample/Pages/Effects/SemanticEffectPage.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BasePage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Xamarin.CommunityToolkit.Sample.Pages.Effects.SemanticEffectPage"
xmlns:pages="clr-namespace:Xamarin.CommunityToolkit.Sample.Pages"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit">
<ContentPage.Content>
<ScrollView>
<StackLayout Padding="20">
<Label Text="I have no heading" xct:SemanticEffect.HeadingLevel="None"/>
<Label Text="I am a heading 1" xct:SemanticEffect.HeadingLevel="Level1"/>
<Label Text="I am a heading 2" xct:SemanticEffect.HeadingLevel="Level2"/>
<Label Text="I am a heading 3" xct:SemanticEffect.HeadingLevel="Level3"/>
<Label Text="I am a heading 4" xct:SemanticEffect.HeadingLevel="Level4"/>
<Label Text="I am a heading 5" xct:SemanticEffect.HeadingLevel="Level5"/>
<Label Text="I am a heading 6" xct:SemanticEffect.HeadingLevel="Level6"/>
<Label Text="I am a heading 7" xct:SemanticEffect.HeadingLevel="Level7"/>
<Label Text="I am a heading 8" xct:SemanticEffect.HeadingLevel="Level8"/>
<Label Text="I am a heading 9" xct:SemanticEffect.HeadingLevel="Level9"/>

<Label Text="I am a label with an automation ID" AutomationId="labelAutomationIdTest" xct:SemanticEffect.Description="This is a semantic description" />

<Label Text="The button below has a semantic hint"/>
<Button
Text="Button with hint"
xct:SemanticEffect.Hint="This is a hint that describes the button. For example, 'sends a message'"/>

<Label Text="The image below has a semantic description"/>
<Image
Source="{xct:ImageResource Id=Xamarin.CommunityToolkit.Sample.Images.logo.png}"
xct:SemanticEffect.Description="This is a description that describes the image. For example, 'image of xamarin community toolkit logo'"/>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</pages:BasePage>
13 changes: 13 additions & 0 deletions samples/XCT.Sample/Pages/Effects/SemanticEffectPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

using Xamarin.Forms;

namespace Xamarin.CommunityToolkit.Sample.Pages.Effects
{
public partial class SemanticEffectPage : BasePage
{
public SemanticEffectPage()
{
InitializeComponent();
}
}
}
21 changes: 21 additions & 0 deletions samples/XCT.Sample/Pages/Views/SemanticOrderViewPage.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BasePage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:Xamarin.CommunityToolkit.Sample.Pages"
x:Class="Xamarin.CommunityToolkit.Sample.Pages.Views.SemanticOrderViewPage"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit">
<ContentPage.Content>
<StackLayout Margin="20">
<Button Text="Element outside the Semantic View"></Button>
<xct:SemanticOrderView x:Name="acv">
<StackLayout>
<Label x:Name="second" Text="Second" Margin="0,20" />
<Button x:Name="third" Text="Third" Margin="0,20" />
<Label x:Name="fourth" Text="Fourth" Margin="0,20" />
<Button x:Name="fifth" Text="Fifth and last" Margin="0,20" />
<Button x:Name="first" Text="First" Margin="0,20" />
</StackLayout>
</xct:SemanticOrderView>
</StackLayout>
</ContentPage.Content>
</pages:BasePage>
15 changes: 15 additions & 0 deletions samples/XCT.Sample/Pages/Views/SemanticOrderViewPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Collections.Generic;

using Xamarin.Forms;

namespace Xamarin.CommunityToolkit.Sample.Pages.Views
{
public partial class SemanticOrderViewPage : BasePage
{
public SemanticOrderViewPage()
{
InitializeComponent();
acv.ViewOrder = new List<View> { first, second, third, fourth, fifth };
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ protected override IEnumerable<SectionModel> CreateItems() => new[]
new SectionModel(
typeof(SafeAreaEffectPage),
nameof(SafeAreaEffect),
"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)"),
"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)"),

new SectionModel(
typeof(RemoveBorderEffectPage),
Expand Down Expand Up @@ -42,7 +42,12 @@ protected override IEnumerable<SectionModel> CreateItems() => new[]
new SectionModel(
typeof(ShadowEffectPage),
nameof(ShadowEffect),
"The ShadowEffect allows all views to display shadow.")
"The ShadowEffect allows all views to display shadow."),

new SectionModel(
typeof(SemanticEffectPage),
nameof(SemanticEffect),
"The SemanticEffect allows you to set semantic properties for accessibility.")
};
}
}
3 changes: 3 additions & 0 deletions samples/XCT.Sample/ViewModels/Views/ViewsGalleryViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ protected override IEnumerable<SectionModel> CreateItems() => new[]
new SectionModel(typeof(RangeSliderPage), "RangeSlider",
"The RangeSlider is a slider with two thumbs allowing to select numeric ranges"),

new SectionModel(typeof(SemanticOrderViewPage), "SemanticOrderView",
"Set accessiblity ordering on views"),

new SectionModel(typeof(SnackBarPage), "SnackBar/Toast",
"Show SnackBar, Toast etc"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,10 @@ sealed class EffectIds
/// Effect Id for <see cref="ShadowEffect"/>
/// </summary>
public static string ShadowEffect => $"{effectResolutionGroupName}.{nameof(ShadowEffect)}";

/// <summary>
/// Effect Id for <see cref="SemanticEffect"/>
/// </summary>
public static string Semantic => $"{effectResolutionGroupName}.{nameof(SemanticEffectRouter)}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xamarin.Forms;
using Xamarin.CommunityToolkit.Effects.Semantic;

namespace Xamarin.CommunityToolkit.Effects
{
public static class SemanticEffect
{
public static readonly BindableProperty HeadingLevelProperty =
BindableProperty.CreateAttached("HeadingLevel", typeof(HeadingLevel), typeof(SemanticEffect), HeadingLevel.None, propertyChanged: OnPropertyChanged);

public static HeadingLevel GetHeadingLevel(BindableObject view) => (HeadingLevel)view.GetValue(HeadingLevelProperty);

public static void SetHeadingLevel(BindableObject view, HeadingLevel value) => view.SetValue(HeadingLevelProperty, value);


public static readonly BindableProperty DescriptionProperty = BindableProperty.CreateAttached("Description", typeof(string), typeof(SemanticEffect), default(string), propertyChanged: OnPropertyChanged);

public static string GetDescription(BindableObject bindable) => (string)bindable.GetValue(DescriptionProperty);

public static void SetDescription(BindableObject bindable, string value) => bindable.SetValue(DescriptionProperty, value);

public static readonly BindableProperty HintProperty = BindableProperty.CreateAttached("Hint", typeof(string), typeof(SemanticEffect), default(string), propertyChanged: OnPropertyChanged);

public static string GetHint(BindableObject bindable) => (string)bindable.GetValue(HintProperty);

public static void SetHint(BindableObject bindable, string value) => bindable.SetValue(HintProperty, value);

static void OnPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is not View view)
return;

if (view.Effects.FirstOrDefault(x => x is SemanticEffectRouter) == null)
{
view.Effects.Add(new SemanticEffectRouter());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using AndroidX.Core.View;
using System.ComponentModel;
using Xamarin.Forms;
using Xamarin.CommunityToolkit.Effects;
using Effects = Xamarin.CommunityToolkit.Android.Effects;
using AndroidX.Core.View.Accessibiity;
using Android.Widget;

[assembly: ExportEffect(typeof(Effects.SemanticEffectRouter), nameof(SemanticEffectRouter))]

namespace Xamarin.CommunityToolkit.Android.Effects
{
/// <summary>
/// Android implementation of the <see cref="SemanticEffect" />
/// </summary>
public class SemanticEffectRouter : SemanticEffectRouterBase<SemanticEffectRouter>
{
SemanticAccessibilityDelegate? semanticAccessibilityDelegate;

protected override void Update(global::Android.Views.View view, SemanticEffectRouter effect)
{
var isHeading = SemanticEffect.GetHeadingLevel(Element) != CommunityToolkit.Effects.Semantic.HeadingLevel.None;
ViewCompat.SetAccessibilityHeading(view, isHeading);
var desc = SemanticEffect.GetDescription(Element);
var hint = SemanticEffect.GetHint(Element);

if (!string.IsNullOrEmpty(hint) || !string.IsNullOrEmpty(desc))
{
if (semanticAccessibilityDelegate == null)
{
semanticAccessibilityDelegate = new SemanticAccessibilityDelegate(Element);
ViewCompat.SetAccessibilityDelegate(view, semanticAccessibilityDelegate);
}
}
else if (semanticAccessibilityDelegate != null)
{
semanticAccessibilityDelegate = null;
ViewCompat.SetAccessibilityDelegate(view, null);
}

if (semanticAccessibilityDelegate != null)
{
semanticAccessibilityDelegate.Element = Element;
view.ImportantForAccessibility = global::Android.Views.ImportantForAccessibility.Yes;
}
}

protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
{
base.OnElementPropertyChanged(args);

if (args.PropertyName == SemanticEffect.HeadingLevelProperty.PropertyName ||
args.PropertyName == SemanticEffect.DescriptionProperty.PropertyName ||
args.PropertyName == SemanticEffect.HintProperty.PropertyName)
{
Update();
}
}

class SemanticAccessibilityDelegate : AccessibilityDelegateCompat
{
public Element Element { get; set; }

public SemanticAccessibilityDelegate(Element element)
{
Element = element;
}

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

if (Element == null)
return;

if (info == null)
return;

var hint = SemanticEffect.GetHint(Element);
if (!string.IsNullOrEmpty(hint))
{
info.HintText = hint;

if (host is EditText)
info.ShowingHintText = false;
}

var desc = SemanticEffect.GetDescription(Element);
if (!string.IsNullOrEmpty(desc))
{
info.ContentDescription = desc;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.ComponentModel;
using UIKit;
using Xamarin.CommunityToolkit.Effects;
using Xamarin.Forms;
using Effects = Xamarin.CommunityToolkit.iOS.Effects;

[assembly: ExportEffect(typeof(Effects.SemanticEffectRouter), nameof(SemanticEffectRouter))]

namespace Xamarin.CommunityToolkit.iOS.Effects
{
/// <summary>
/// iOS implementation of the <see cref="SemanticEffect" />
/// </summary>
public class SemanticEffectRouter : SemanticEffectRouterBase<SemanticEffectRouter>
{
public SemanticEffectRouter()
{
}

protected override void Update(UIView view, SemanticEffectRouter effect)
{
var isHeading = SemanticEffect.GetHeadingLevel(Element) != CommunityToolkit.Effects.Semantic.HeadingLevel.None;

if (isHeading)
view.AccessibilityTraits |= UIAccessibilityTrait.Header;
else
view.AccessibilityTraits &= ~UIAccessibilityTrait.Header;

var desc = SemanticEffect.GetDescription(Element);
var hint = SemanticEffect.GetHint(Element);
view.AccessibilityLabel = desc;
view.AccessibilityHint = hint;

// UIControl elements automatically have IsAccessibilityElement set to true
if (view is not UIControl && (!string.IsNullOrWhiteSpace(hint) || !string.IsNullOrWhiteSpace(desc)))
{
view.IsAccessibilityElement = true;
}
}

protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
{
base.OnElementPropertyChanged(args);

if (args.PropertyName == SemanticEffect.HeadingLevelProperty.PropertyName ||
args.PropertyName == SemanticEffect.DescriptionProperty.PropertyName ||
args.PropertyName == SemanticEffect.HintProperty.PropertyName)
{
Update();
}
}
}
}
Loading

0 comments on commit 54e3e94

Please sign in to comment.