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

Base implementations for SemanticEffect and SemanticOrderView #1240

Merged
merged 28 commits into from May 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0efdc5a
Base implementations for SemanticEffect and SemanticOrderView
jamesmontemagno Apr 26, 2021
c92d669
- uwp and iOS fix
PureWeen Apr 29, 2021
dfdb369
- uwp tab index
PureWeen Apr 29, 2021
616359c
- hint and description
PureWeen Apr 29, 2021
7c8e4b4
- fix iOS to auto set to important for accessibility
PureWeen Apr 29, 2021
c18f37a
Merge branch 'main' into accessibility-views-effects
jsuarezruiz May 4, 2021
7d1193b
Add samples and fix up crashes
jamesmontemagno May 4, 2021
980c8e6
- for UI Test reason switch to using a delegate for reading content D…
PureWeen May 5, 2021
a5b52cf
- use the delegate if the automation id is set
PureWeen May 5, 2021
5c0b0f4
- fixes
PureWeen May 6, 2021
86db533
- set yes less often
PureWeen May 6, 2021
edb5026
Remove IsInAccessibleTree from PancakeView
rachelkang May 6, 2021
46d5f84
Update SemanticEffectPage.xaml
rachelkang May 6, 2021
a7239c7
Update SemanticOrderViewPage.xaml
rachelkang May 6, 2021
6c57f52
Update SemanticOrderViewPage.xaml.cs
rachelkang May 6, 2021
aff986f
Fix Semantic Description on Android
rachelkang May 6, 2021
83cb46f
Reword SemanticEffect about
rachelkang May 6, 2021
09c4640
- added temporary workaround for making a screen clickable
PureWeen May 6, 2021
f5ca514
Merge branch 'accessibility-views-effects' of https://github.com/xama…
rachelkang May 6, 2021
18ae418
Fix SemanticOrderViewPage sample
rachelkang May 6, 2021
300d569
Fix NRE thrown on back nav from iOS SemanticOrderView page
rachelkang May 6, 2021
d200667
- fix UWP router
PureWeen May 6, 2021
c507bad
- fix null checks
PureWeen May 6, 2021
727873e
- changed first button to button so uwp sample is interesting
PureWeen May 6, 2021
626e09b
Clean up code - Pedro's feedback
rachelkang May 7, 2021
36b39e4
Merge branch 'accessibility-views-effects' of https://github.com/xama…
rachelkang May 7, 2021
dfdd78a
Merge branch 'main' into accessibility-views-effects
rachelkang May 7, 2021
b34d677
Merge branch 'main' into accessibility-views-effects
jfversluis May 7, 2021
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
56 changes: 56 additions & 0 deletions samples/XCT.Sample.Android/AccessiblePageRenderer.cs
@@ -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
rachelkang marked this conversation as resolved.
Show resolved Hide resolved
{
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;
}
}
}
}
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
@@ -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
@@ -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
@@ -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
@@ -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 };
}
}
}
Expand Up @@ -12,7 +12,7 @@ public class EffectsGalleryViewModel : BaseGalleryViewModel
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 @@ public class EffectsGalleryViewModel : BaseGalleryViewModel
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
Expand Up @@ -35,6 +35,9 @@ public class ViewsGalleryViewModel : BaseGalleryViewModel
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
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)}";
}
}
@@ -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());
}
}
}
}
@@ -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;
}
}
}
}
}
@@ -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();
}
}
}
}