diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Data/PhotoDataItem.cs b/Microsoft.Toolkit.Uwp.SampleApp/Data/PhotoDataItem.cs index ae16e5eaaf8..37a9eb0f5de 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/Data/PhotoDataItem.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/Data/PhotoDataItem.cs @@ -11,5 +11,10 @@ public class PhotoDataItem public string Category { get; set; } public string Thumbnail { get; set; } + + public override string ToString() + { + return Title; + } } } diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/BladeView/BladeItemAutomationPeer.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/BladeView/BladeItemAutomationPeer.cs index a45a1acf191..f07b3e82581 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/BladeView/BladeItemAutomationPeer.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/BladeView/BladeItemAutomationPeer.cs @@ -93,28 +93,6 @@ protected override string GetNameCore() return string.Empty; } - /// - /// Called by GetAutomationId that gets the **AutomationId** of the element that is associated with the automation peer. - /// - /// - /// The string that contains the automation ID. - /// - protected override string GetAutomationIdCore() - { - string automationId = base.GetAutomationIdCore(); - if (!string.IsNullOrEmpty(automationId)) - { - return automationId; - } - - if (this.OwnerBladeItem != null) - { - return this.GetNameCore(); - } - - return string.Empty; - } - /// /// Returns the size of the set where the element that is associated with the automation peer is located. /// diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/Carousel.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/Carousel.cs index 1d73c58e181..ababf502fe3 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/Carousel.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/Carousel.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Toolkit.Uwp.UI.Automation.Peers; 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; @@ -520,6 +522,17 @@ protected override void PrepareContainerForItemOverride(DependencyObject element { carouselItem.IsSelected = true; } + + carouselItem.ParentCarousel = this; + } + + /// + /// Creates AutomationPeer () + /// + /// An automation peer for this . + protected override AutomationPeer OnCreateAutomationPeer() + { + return new CarouselAutomationPeer(this); } private void OnCarouselItemSelected(object sender, EventArgs e) @@ -528,5 +541,11 @@ private void OnCarouselItemSelected(object sender, EventArgs e) SelectedItem = ItemFromContainer(item); } + + internal void SetSelectedItem(CarouselItem owner) + { + var item = ItemFromContainer(owner); + SelectedItem = item; + } } } diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/CarouselAutomationPeer.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/CarouselAutomationPeer.cs new file mode 100644 index 00000000000..59a310ec3b4 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/CarouselAutomationPeer.cs @@ -0,0 +1,143 @@ +// 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 Microsoft.Toolkit.Uwp.UI.Controls; +using Windows.UI.Xaml.Automation; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Automation.Provider; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// Defines a framework element automation peer for the control. + /// + public class CarouselAutomationPeer : ItemsControlAutomationPeer, ISelectionProvider + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The that is associated with this . + /// + public CarouselAutomationPeer(Carousel owner) + : base(owner) + { + } + + /// Gets a value indicating whether the Microsoft UI Automation provider allows more than one child element to be selected concurrently. + /// True if multiple selection is allowed; otherwise, false. + public bool CanSelectMultiple => false; + + /// Gets a value indicating whether the UI Automation provider requires at least one child element to be selected. + /// True if selection is required; otherwise, false. + public bool IsSelectionRequired => true; + + private Carousel OwningCarousel + { + get + { + return Owner as Carousel; + } + } + + /// Retrieves a UI Automation provider for each child element that is selected. + /// An array of UI Automation providers. + public IRawElementProviderSimple[] GetSelection() + { + return OwningCarousel.ContainerFromItem(this.OwningCarousel.SelectedItem) is CarouselItem selectedCarouselItem + ? new[] { this.ProviderFromPeer(FromElement(selectedCarouselItem)) } + : new IRawElementProviderSimple[] { }; + } + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + /// The control type. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.List; + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + return Owner.GetType().Name; + } + + /// + /// Called by GetName. + /// + /// + /// 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 + /// + protected override string GetNameCore() + { + string name = this.OwningCarousel.Name; + if (!string.IsNullOrEmpty(name)) + { + return name; + } + + name = AutomationProperties.GetName(this.OwningCarousel); + if (!string.IsNullOrEmpty(name)) + { + return name; + } + + return base.GetNameCore(); + } + + /// + /// Gets the control pattern that is associated with the specified Windows.UI.Xaml.Automation.Peers.PatternInterface. + /// + /// A value from the Windows.UI.Xaml.Automation.Peers.PatternInterface enumeration. + /// The object that supports the specified pattern, or null if unsupported. + protected override object GetPatternCore(PatternInterface patternInterface) + { + switch (patternInterface) + { + case PatternInterface.Selection: + return this; + } + + return base.GetPatternCore(patternInterface); + } + + /// + /// Gets the collection of elements that are represented in the UI Automation tree as immediate + /// child elements of the automation peer. + /// + /// The children elements. + protected override IList GetChildrenCore() + { + Carousel owner = OwningCarousel; + + ItemCollection items = owner.Items; + if (items.Count <= 0) + { + return null; + } + + List peers = new List(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; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/CarouselItem.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/CarouselItem.cs index 5404d04c16a..ae97fee86db 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/CarouselItem.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/CarouselItem.cs @@ -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; @@ -22,6 +23,8 @@ public partial class CarouselItem : SelectorItem private const string SelectedState = "Selected"; private const string NormalState = "Normal"; + private WeakReference parentCarousel; + /// /// Initializes a new instance of the class. /// @@ -33,6 +36,16 @@ public CarouselItem() RegisterPropertyChangedCallback(SelectorItem.IsSelectedProperty, OnIsSelectedChanged); } + internal Carousel ParentCarousel + { + get + { + this.parentCarousel.TryGetTarget(out var carousel); + return carousel; + } + set => this.parentCarousel = new WeakReference(value); + } + /// protected override void OnPointerEntered(PointerRoutedEventArgs e) { @@ -57,6 +70,15 @@ protected override void OnPointerPressed(PointerRoutedEventArgs e) VisualStateManager.GoToState(this, IsSelected ? PressedSelectedState : PressedState, true); } + /// + /// Creates AutomationPeer () + /// + /// An automation peer for this . + protected override AutomationPeer OnCreateAutomationPeer() + { + return new CarouselItemAutomationPeer(this); + } + internal event EventHandler Selected; private void OnIsSelectedChanged(DependencyObject sender, DependencyProperty dp) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/CarouselItemAutomationPeer.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/CarouselItemAutomationPeer.cs new file mode 100644 index 00000000000..ca0432d4ee7 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Carousel/CarouselItemAutomationPeer.cs @@ -0,0 +1,188 @@ +// 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 Microsoft.Toolkit.Uwp.UI.Controls; +using Windows.UI.Xaml.Automation; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Automation.Provider; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// Defines a framework element automation peer for the . + /// + public class CarouselItemAutomationPeer : FrameworkElementAutomationPeer, ISelectionItemProvider + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The that is associated with this . + /// + public CarouselItemAutomationPeer(CarouselItem owner) + : base(owner) + { + } + + /// Gets a value indicating whether an item is selected. + /// True if the element is selected; otherwise, false. + public bool IsSelected => this.OwnerCarouselItem.IsSelected; + + /// Gets the UI Automation provider that implements ISelectionProvider and acts as the container for the calling object. + /// The UI Automation provider. + 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; } + } + + /// Adds the current element to the collection of selected items. + public void AddToSelection() + { + CarouselItem owner = this.OwnerCarouselItem; + Carousel parent = owner.ParentCarousel; + parent?.SetSelectedItem(owner); + } + + /// Removes the current element from the collection of selected items. + public void RemoveFromSelection() + { + // Cannot remove the selection of a Carousel control. + } + + /// Clears any existing selection and then selects the current element. + public void Select() + { + CarouselItem owner = this.OwnerCarouselItem; + Carousel parent = owner.ParentCarousel; + parent?.SetSelectedItem(owner); + } + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + /// The control type. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.ListItem; + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + return Owner.GetType().Name; + } + + /// + /// Called by GetName. + /// + /// + /// 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 + /// + protected override string GetNameCore() + { + string name = AutomationProperties.GetName(this.OwnerCarouselItem); + if (!string.IsNullOrEmpty(name)) + { + return name; + } + + name = this.OwnerCarouselItem.Name; + if (!string.IsNullOrEmpty(name)) + { + return name; + } + + var textBlock = this.OwnerCarouselItem.FindDescendant(); + if (textBlock != null) + { + return textBlock.Text; + } + + return base.GetNameCore(); + } + + /// + /// Gets the control pattern that is associated with the specified Windows.UI.Xaml.Automation.Peers.PatternInterface. + /// + /// A value from the Windows.UI.Xaml.Automation.Peers.PatternInterface enumeration. + /// The object that supports the specified pattern, or null if unsupported. + protected override object GetPatternCore(PatternInterface patternInterface) + { + switch (patternInterface) + { + case PatternInterface.SelectionItem: + return this; + } + + return base.GetPatternCore(patternInterface); + } + + /// + /// Returns the size of the set where the element that is associated with the automation peer is located. + /// + /// + /// The size of the set. + /// + protected override int GetSizeOfSetCore() + { + int sizeOfSet = base.GetSizeOfSetCore(); + + if (sizeOfSet != -1) + { + return sizeOfSet; + } + + CarouselItem owner = this.OwnerCarouselItem; + Carousel parent = owner.ParentCarousel; + sizeOfSet = parent.Items.Count; + + return sizeOfSet; + } + + /// + /// Returns the ordinal position in the set for the element that is associated with the automation peer. + /// + /// + /// The ordinal position in the set. + /// + protected override int GetPositionInSetCore() + { + int positionInSet = base.GetPositionInSetCore(); + + if (positionInSet != -1) + { + return positionInSet; + } + + CarouselItem owner = this.OwnerCarouselItem; + Carousel parent = owner.ParentCarousel; + positionInSet = parent.IndexFromContainer(owner) + 1; + + return positionInSet; + } + } +} \ No newline at end of file diff --git a/UnitTests/UnitTests.UWP/Properties/UnitTestApp.rd.xml b/UnitTests/UnitTests.UWP/Properties/UnitTestApp.rd.xml index f89058b5081..592ef8b1d7a 100644 --- a/UnitTests/UnitTests.UWP/Properties/UnitTestApp.rd.xml +++ b/UnitTests/UnitTests.UWP/Properties/UnitTestApp.rd.xml @@ -37,5 +37,7 @@ + + \ No newline at end of file diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_Carousel.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_Carousel.cs new file mode 100644 index 00000000000..227f5af8424 --- /dev/null +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_Carousel.cs @@ -0,0 +1,86 @@ +// 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.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using Windows.UI.Xaml.Automation; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Windows.UI.Xaml.Automation.Peers; +using Microsoft.Toolkit.Uwp; +using Microsoft.Toolkit.Uwp.UI.Automation.Peers; +using Microsoft.Toolkit.Uwp.UI.Controls; + +namespace UnitTests.UWP.UI.Controls +{ + [TestClass] + [TestCategory("Test_Carousel")] + public class Test_Carousel : VisualUITestBase + { + [TestMethod] + public async Task ShouldConfigureCarouselAutomationPeerAsync() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + const int expectedSelectedIndex = 1; + const string expectedCarouselAutomationName = "MyAutomationPhotoItems"; + const string expectedCarouselName = "MyPhotoItems"; + + var items = new ObservableCollection { new PhotoDataItem { Title = "Hello" }, new PhotoDataItem { Title = "World" } }; + + var carousel = new Carousel { ItemsSource = items }; + + await SetTestContentAsync(carousel); + + // Sets the selected item to "World" from the items above. + carousel.SelectedIndex = expectedSelectedIndex; + + var carouselAutomationPeer = + FrameworkElementAutomationPeer.CreatePeerForElement(carousel) as CarouselAutomationPeer; + + Assert.IsNotNull(carouselAutomationPeer, "Verify that the AutomationPeer is CarouselAutomationPeer."); + Assert.IsFalse(carouselAutomationPeer.CanSelectMultiple, "Verify that CarouselAutomationPeer.CanSelectMultiple is false."); + Assert.IsTrue(carouselAutomationPeer.IsSelectionRequired, "Verify that CarouselAutomationPeer.IsSelectionRequired is true."); + + // Asserts the automation peer name based on the Automation Property Name value. + carousel.SetValue(AutomationProperties.NameProperty, expectedCarouselAutomationName); + Assert.IsTrue(carouselAutomationPeer.GetName().Contains(expectedCarouselAutomationName), "Verify that the UIA name contains the given AutomationProperties.Name of the Carousel."); + + // Asserts the automation peer name based on the element Name property. + carousel.Name = expectedCarouselName; + Assert.IsTrue(carouselAutomationPeer.GetName().Contains(expectedCarouselName), "Verify that the UIA name contains the given Name of the Carousel."); + + var carouselItemAutomationPeers = carouselAutomationPeer.GetChildren().Cast().ToList(); + Assert.AreEqual(items.Count, carouselItemAutomationPeers.Count); + + // Asserts the default calculated position in set and size of set values + for (var i = 0; i < carouselItemAutomationPeers.Count; i++) + { + var peer = carouselItemAutomationPeers[i]; + Assert.AreEqual(i + 1, peer.GetPositionInSet()); + Assert.AreEqual(items.Count, peer.GetSizeOfSet()); + } + + // Asserts the CarouselItemAutomationPeer properties + var selectedItemPeer = carouselItemAutomationPeers.FirstOrDefault(peer => peer.IsSelected); + Assert.IsNotNull(selectedItemPeer); + Assert.IsTrue(selectedItemPeer.GetName().Contains(items[expectedSelectedIndex].ToString())); + }); + } + + public class PhotoDataItem + { + public string Title { get; set; } + + public string Category { get; set; } + + public string Thumbnail { get; set; } + + public override string ToString() + { + return Title; + } + } + } +} \ No newline at end of file diff --git a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj index b8986e6452b..eb3c66e4344 100644 --- a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj +++ b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj @@ -194,6 +194,7 @@ +