Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added automation peer class for carousel and item #3507

Merged
merged 27 commits into from
Feb 25, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0ccbedc
Added automation peer class for carousel and item
jamesmcroft Sep 27, 2020
36235df
Moved carousel automation peers into their own namespace
jamesmcroft Sep 29, 2020
2b9bd47
Merge branch 'master' into jamesmcroft/3506-carousel-automation
jamesmcroft Sep 29, 2020
84d9e00
Merge branch 'master' into jamesmcroft/3506-carousel-automation
Rosuavio Oct 9, 2020
d0c6b6f
Merge branch 'master' into jamesmcroft/3506-carousel-automation
Rosuavio Nov 3, 2020
7e14dc2
Merge branch 'master' into jamesmcroft/3506-carousel-automation
Rosuavio Nov 5, 2020
870b743
Merge branch 'master' into jamesmcroft/3506-carousel-automation
michael-hawker Nov 10, 2020
a706ca6
Improved implementation for Carousel automation peer
jamesmcroft Nov 11, 2020
c9825f1
Merge branch 'jamesmcroft/3506-carousel-automation' of https://github…
jamesmcroft Nov 11, 2020
662e501
Merge branch 'master' into jamesmcroft/3506-carousel-automation
Kyaa-dost Nov 13, 2020
411e39d
Added test for Carousel automation peer.
jamesmcroft Nov 18, 2020
b959fbf
Updated carousel automation peer to improve returned name, and select…
jamesmcroft Nov 22, 2020
631f6b2
Updated carousel item automation peer to remove ability to remove sel…
jamesmcroft Nov 22, 2020
ba3c81f
Fixed TextBlock to text property in automation peer
jamesmcroft Nov 22, 2020
f42f236
Fixed carousel automation tests
jamesmcroft Nov 22, 2020
41491ee
Altered GetNameCore to default to return the base implementation when…
jamesmcroft Dec 7, 2020
951774d
Merge branch 'master' into jamesmcroft/3506-carousel-automation
jamesmcroft Dec 7, 2020
8cc836f
Merge branch 'master' into jamesmcroft/3506-carousel-automation
michael-hawker Dec 15, 2020
7c8c126
Merge branch 'master' into jamesmcroft/3506-carousel-automation
jamesmcroft Jan 17, 2021
c544bb0
Merge branch 'master' into jamesmcroft/3506-carousel-automation
jamesmcroft Jan 21, 2021
3b2d965
Merge branch 'master' into jamesmcroft/3506-carousel-automation
jamesmcroft Feb 16, 2021
4dbba32
Updated Carousel tests to include new Visual UI test implementation.
jamesmcroft Feb 18, 2021
ddea9ce
Updated carousel test to not use parsed XAML string
jamesmcroft Feb 18, 2021
79ea866
Added change to rd.xml and cleaned up carousel test
jamesmcroft Feb 18, 2021
32b015d
Removed GetAutomationIdCore implementation from CarouselItem and Blad…
jamesmcroft Feb 24, 2021
df21fca
Merge branch 'master' into jamesmcroft/3506-carousel-automation
jamesmcroft Feb 24, 2021
7547a4f
Removed using directives which no longer exist
jamesmcroft Feb 24, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion Microsoft.Toolkit.Uwp.UI.Controls/Carousel/Carousel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Toolkit.Uwp.UI.Automation.Peers;
using Microsoft.Toolkit.Uwp.UI.Extensions;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Automation;
using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
Expand Down Expand Up @@ -522,6 +523,17 @@ protected override void PrepareContainerForItemOverride(DependencyObject element
{
carouselItem.IsSelected = true;
}

carouselItem.ParentCarousel = this;
michael-hawker marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Creates AutomationPeer (<see cref="UIElement.OnCreateAutomationPeer"/>)
/// </summary>
/// <returns>An automation peer for this <see cref="Carousel"/>.</returns>
protected override AutomationPeer OnCreateAutomationPeer()
{
return new CarouselAutomationPeer(this);
}

private void OnCarouselItemSelected(object sender, EventArgs e)
Expand All @@ -530,5 +542,19 @@ private void OnCarouselItemSelected(object sender, EventArgs e)

SelectedItem = ItemFromContainer(item);
}

internal IEnumerable<CarouselItem> GetCarouselItems()
{
return Enumerable
.Range(0, Items.Count)
.Select(idx => (CarouselItem)ContainerFromIndex(idx))
.Where(i => i != null);
}

internal void SetSelectedItem(CarouselItem owner)
{
var item = ItemFromContainer(owner);
SelectedItem = item;
}
}
}
148 changes: 148 additions & 0 deletions Microsoft.Toolkit.Uwp.UI.Controls/Carousel/CarouselAutomationPeer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Linq;
using Microsoft.Toolkit.Uwp.UI.Controls;
using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Automation.Provider;
using Windows.UI.Xaml.Controls;

namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers
{
/// <summary>
/// Defines a framework element automation peer for the <see cref="Carousel"/> control.
/// </summary>
public class CarouselAutomationPeer : ItemsControlAutomationPeer, ISelectionProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="CarouselAutomationPeer"/> class.
/// </summary>
/// <param name="owner">
/// The <see cref="Carousel" /> that is associated with this <see cref="T:Windows.UI.Xaml.Automation.Peers.CarouselAutomationPeer" />.
/// </param>
public CarouselAutomationPeer(Carousel owner)
: base(owner)
{
}

/// <summary>Gets a value indicating whether the Microsoft UI Automation provider allows more than one child element to be selected concurrently.</summary>
/// <returns>True if multiple selection is allowed; otherwise, false.</returns>
public bool CanSelectMultiple => false;

/// <summary>Gets a value indicating whether the UI Automation provider requires at least one child element to be selected.</summary>
/// <returns>True if selection is required; otherwise, false.</returns>
public bool IsSelectionRequired => false;
Copy link
Contributor

Choose a reason for hiding this comment

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

How would a Carousel without any selection look like (ignoring empty carousels as there isn't really reasonable to treat that with selection).

Copy link
Member Author

Choose a reason for hiding this comment

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

Made this true, an item should always be selected.


private Carousel OwningCarousel
{
get
{
return Owner as Carousel;
}
}

/// <summary>Retrieves a UI Automation provider for each child element that is selected.</summary>
/// <returns>An array of UI Automation providers.</returns>
public IRawElementProviderSimple[] GetSelection()
{
CarouselItem selectedCarouselItem = OwningCarousel.GetCarouselItems().FirstOrDefault(x => x.IsSelected);
michael-hawker marked this conversation as resolved.
Show resolved Hide resolved
return selectedCarouselItem != null
? new[] { this.ProviderFromPeer(FromElement(selectedCarouselItem)) }
: new IRawElementProviderSimple[] { };
}

/// <summary>
/// Gets the control type for the element that is associated with the UI Automation peer.
/// </summary>
/// <returns>The control type.</returns>
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.List;
}

/// <summary>
/// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType,
/// differentiates the control represented by this AutomationPeer.
/// </summary>
/// <returns>The string that contains the name.</returns>
protected override string GetClassNameCore()
{
return Owner.GetType().Name;
}

/// <summary>
/// Called by GetName.
/// </summary>
/// <returns>
/// Returns the first of these that is not null or empty:
/// - Value returned by the base implementation
/// - Name of the owning Carousel
/// - Carousel class name
/// </returns>
protected override string GetNameCore()
{
string name = base.GetNameCore();
if (!string.IsNullOrEmpty(name))
{
return name;
}

if (this.OwningCarousel != null)
{
name = this.OwningCarousel.Name;
}

if (string.IsNullOrEmpty(name))
{
name = this.GetClassName();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we returning the class name? Narrator should already announce the class/type of the control, having the name of the control be the same makes it more confusing for users relying on that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Interesting. This is the standard that I've seen across a lot of Windows controls and even based this one off the DataGrid within the toolkit.

Do agree though that this could be confusing.

Copy link
Contributor

Choose a reason for hiding this comment

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

From what I have seen, WinUI 2 controls (mostly) don't do that, they try to get the a name from AutomationProperties.Name or their content; if that fails they return an empty string.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a good compromise. I'll do some testing with the inspect tool. I don't know what the outcome would be for the name of the control if it is null or an empty string from the automation peer.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated this to return the name of the control if provided, otherwise it will fallback to the base implementation.

}

return name;
}

/// <summary>
/// Gets the control pattern that is associated with the specified Windows.UI.Xaml.Automation.Peers.PatternInterface.
/// </summary>
/// <param name="patternInterface">A value from the Windows.UI.Xaml.Automation.Peers.PatternInterface enumeration.</param>
/// <returns>The object that supports the specified pattern, or null if unsupported.</returns>
protected override object GetPatternCore(PatternInterface patternInterface)
{
switch (patternInterface)
{
case PatternInterface.Selection:
return this;
}

return base.GetPatternCore(patternInterface);
}

/// <summary>
/// Gets the collection of elements that are represented in the UI Automation tree as immediate
/// child elements of the automation peer.
/// </summary>
/// <returns>The children elements.</returns>
protected override IList<AutomationPeer> GetChildrenCore()
{
Carousel owner = OwningCarousel;

ItemCollection items = owner.Items;
if (items.Count <= 0)
{
return null;
}

List<AutomationPeer> peers = new List<AutomationPeer>(items.Count);
for (int i = 0; i < items.Count; i++)
{
if (owner.ContainerFromIndex(i) is CarouselItem element)
{
peers.Add(FromElement(element) ?? CreatePeerForElement(element));
}
}

return peers;
}
}
}
14 changes: 13 additions & 1 deletion Microsoft.Toolkit.Uwp.UI.Controls/Carousel/CarouselItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
// See the LICENSE file in the project root for more information.

using System;
using Microsoft.Toolkit.Uwp.UI.Automation.Peers;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Input;

Expand Down Expand Up @@ -33,6 +34,8 @@ public CarouselItem()
RegisterPropertyChangedCallback(SelectorItem.IsSelectedProperty, OnIsSelectedChanged);
}

internal Carousel ParentCarousel { get; set; }

/// <inheritdoc/>
protected override void OnPointerEntered(PointerRoutedEventArgs e)
{
Expand All @@ -57,6 +60,15 @@ protected override void OnPointerPressed(PointerRoutedEventArgs e)
VisualStateManager.GoToState(this, IsSelected ? PressedSelectedState : PressedState, true);
}

/// <summary>
/// Creates AutomationPeer (<see cref="UIElement.OnCreateAutomationPeer"/>)
/// </summary>
/// <returns>An automation peer for this <see cref="CarouselItem"/>.</returns>
protected override AutomationPeer OnCreateAutomationPeer()
{
return new CarouselItemAutomationPeer(this);
}

internal event EventHandler Selected;

private void OnIsSelectedChanged(DependencyObject sender, DependencyProperty dp)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Linq;
using Microsoft.Toolkit.Uwp.UI.Controls;
using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Automation.Provider;

namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers
{
/// <summary>
/// Defines a framework element automation peer for the <see cref="CarouselItem"/>.
/// </summary>
public class CarouselItemAutomationPeer : FrameworkElementAutomationPeer, ISelectionItemProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="CarouselItemAutomationPeer"/> class.
/// </summary>
/// <param name="owner">
/// The <see cref="CarouselItem" /> that is associated with this <see cref="T:Windows.UI.Xaml.Automation.Peers.CarouselItemAutomationPeer" />.
/// </param>
public CarouselItemAutomationPeer(CarouselItem owner)
: base(owner)
{
}

/// <summary>Gets a value indicating whether an item is selected.</summary>
/// <returns>True if the element is selected; otherwise, false.</returns>
public bool IsSelected => this.OwnerCarouselItem.IsSelected;

/// <summary>Gets the UI Automation provider that implements ISelectionProvider and acts as the container for the calling object.</summary>
/// <returns>The UI Automation provider.</returns>
public IRawElementProviderSimple SelectionContainer
{
get
{
Carousel parent = this.OwnerCarouselItem.ParentCarousel;
if (parent == null)
{
return null;
}

AutomationPeer peer = FromElement(parent);
return peer != null ? this.ProviderFromPeer(peer) : null;
}
}

private CarouselItem OwnerCarouselItem
{
get { return this.Owner as CarouselItem; }
}

/// <summary>Adds the current element to the collection of selected items.</summary>
public void AddToSelection()
{
CarouselItem owner = this.OwnerCarouselItem;
Carousel parent = owner.ParentCarousel;
parent.SetSelectedItem(owner);
}

/// <summary>Removes the current element from the collection of selected items.</summary>
public void RemoveFromSelection()
Copy link
Contributor

Choose a reason for hiding this comment

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

So UIA users can reach a state that isn't reachable through mouse interaction? Or is there a way to unselect an item as user?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. The Carousel doesn't provide the user functionality for unselecting an item. Easily sorted.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is sorted now, removed the code for this.

{
CarouselItem owner = this.OwnerCarouselItem;
Carousel parent = owner.ParentCarousel;
parent.SelectedItem = null;
}

/// <summary>Clears any existing selection and then selects the current element.</summary>
public void Select()
{
CarouselItem owner = this.OwnerCarouselItem;
Carousel parent = owner.ParentCarousel;
parent.SetSelectedItem(owner);
}

/// <summary>
/// Gets the control type for the element that is associated with the UI Automation peer.
/// </summary>
/// <returns>The control type.</returns>
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.ListItem;
}

/// <summary>
/// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType,
/// differentiates the control represented by this AutomationPeer.
/// </summary>
/// <returns>The string that contains the name.</returns>
protected override string GetClassNameCore()
{
return Owner.GetType().Name;
}

/// <summary>
/// Called by GetName.
/// </summary>
/// <returns>
/// Returns the first of these that is not null or empty:
/// - Value returned by the base implementation
/// - Name of the owning CarouselItem
/// - Carousel class name
/// </returns>
protected override string GetNameCore()
{
int? index = this.OwnerCarouselItem.ParentCarousel.GetCarouselItems().ToList().IndexOf(this.OwnerCarouselItem);
michael-hawker marked this conversation as resolved.
Show resolved Hide resolved

string name = base.GetNameCore();
if (!string.IsNullOrEmpty(name))
{
return $"{name} {index}";
}

if (this.OwnerCarouselItem != null && !string.IsNullOrEmpty(this.OwnerCarouselItem.Name))
{
return this.OwnerCarouselItem.Name;
}

if (string.IsNullOrEmpty(name))
{
name = this.GetClassName();
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, narrator should already announce the class/type. Announcing it twice doesn't help users much.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated this also to return the name of the control if provided, otherwise it will look for a TextBlock if there is one in the item's template and return that text, otherwise will fallback to the base implementation.

}

return $"{name} {index}";
Copy link
Contributor

Choose a reason for hiding this comment

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

I think using the AutomationProprties.SizeOfSet property is the better way to do this then hardcoding it in the name.

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed this, I don't think this was actually needed. But I have implemented the size of set and position in set methods.

Copy link
Member

Choose a reason for hiding this comment

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

@jamesmcroft I didn't see the new size/position code just 'string.Empty' think it's still useful to return that if we have nothing else.

Copy link
Member Author

Choose a reason for hiding this comment

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

The problem is, if a name is provided, it will return "My Name 1" which isn't actually what you'd want. The size was added in originally because we were returning the class name so it would have been "CarouselItem 1". Based on the feedback, the class name has been removed so not sure this was needed.

I can always add the class name back in though or if nothing else, return the index.

}

/// <summary>
/// Gets the control pattern that is associated with the specified Windows.UI.Xaml.Automation.Peers.PatternInterface.
/// </summary>
/// <param name="patternInterface">A value from the Windows.UI.Xaml.Automation.Peers.PatternInterface enumeration.</param>
/// <returns>The object that supports the specified pattern, or null if unsupported.</returns>
protected override object GetPatternCore(PatternInterface patternInterface)
{
switch (patternInterface)
{
case PatternInterface.SelectionItem:
return this;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not an if case?

Copy link
Member Author

Choose a reason for hiding this comment

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

Could be changed. Wanted to keep this in line with the other automation peers which were created for the DataGrid in the toolkit so as not to cause any confusion in code styling.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would argue that an if case would be better in either case since a few interfaces all result in the same behavior. Having a switch case with 4 cases that all do the same (return this) looks a bit off in my opinion, but that's just my opinion.


return base.GetPatternCore(patternInterface);
}
}
}