diff --git a/eng/pipelines/ci-official.yml b/eng/pipelines/ci-official.yml index ee7812b2d540..4fa55bb22840 100644 --- a/eng/pipelines/ci-official.yml +++ b/eng/pipelines/ci-official.yml @@ -104,6 +104,7 @@ extends: onlyAndroidPlatformDefaultApis: true skipAndroidEmulatorImages: true skipAndroidCreateAvds: true + skipSimulatorSetup: true skipProvisioning: true skipXcode: false base64Encode: true diff --git a/src/Controls/tests/DeviceTests/Elements/ContentView/ContentViewTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/ContentView/ContentViewTests.Windows.cs index 2e32a119dae1..61f777a5432e 100644 --- a/src/Controls/tests/DeviceTests/Elements/ContentView/ContentViewTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/ContentView/ContentViewTests.Windows.cs @@ -1,5 +1,10 @@ -using Microsoft.Maui.Handlers; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Handlers; using Microsoft.Maui.Platform; +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Automation.Peers; +using Xunit; namespace Microsoft.Maui.DeviceTests { @@ -17,5 +22,76 @@ static int GetContentChildCount(ContentViewHandler contentViewHandler) else return 0; } + + static AutomationPeer GetOrCreateAutomationPeer(ContentPanel contentPanel) + => new ContentPanel.ContentPanelAutomationPeer(contentPanel); + + [Fact(DisplayName = "ContentView With Description Prevents Duplicate Narrator Announcements")] + public async Task ContentViewWithDescriptionHidesChildrenFromNarrator() + { + SetupBuilder(); + + var label = new Label { Text = "Child Label Text" }; + var contentView = new ContentView { Content = label }; + SemanticProperties.SetDescription(contentView, "ContentView Description"); + + await CreateHandlerAndAddToWindow(contentView, async (handler) => + { + var contentPanel = handler.PlatformView as ContentPanel; + Assert.NotNull(contentPanel); + + // Call UpdateSemantics manually because handler.UpdateValue uses handler.VirtualView + // which doesn't have Semantics populated in the test context + var view = contentView as IView; + contentPanel.UpdateSemantics(view); + + var peer = GetOrCreateAutomationPeer(contentPanel); + Assert.NotNull(peer); + + var name = Microsoft.UI.Xaml.Automation.AutomationProperties.GetName(contentPanel); + Assert.Equal("ContentView Description", name); + + // ControlType should be Text (not Group) to prevent Narrator from saying "group" + var controlType = peer.GetAutomationControlType(); + Assert.Equal(AutomationControlType.Text, controlType); + + // Children should be hidden to prevent duplicate announcements + var children = peer.GetChildren(); + Assert.Null(children); + + // LocalizedControlType should be empty to prevent suffix + var localizedControlType = peer.GetLocalizedControlType(); + Assert.Equal(string.Empty, localizedControlType); + + await Task.CompletedTask; + }); + } + + [Fact(DisplayName = "ContentView Without Description Shows Children Normally")] + public async Task ContentViewWithoutDescriptionShowsChildren() + { + SetupBuilder(); + + var label = new Label { Text = "Child Label Text" }; + var contentView = new ContentView { Content = label }; + + await CreateHandlerAndAddToWindow(contentView, async (handler) => + { + var contentPanel = handler.PlatformView as ContentPanel; + Assert.NotNull(contentPanel); + + var peer = GetOrCreateAutomationPeer(contentPanel); + Assert.NotNull(peer); + + var controlType = peer.GetAutomationControlType(); + Assert.Equal(AutomationControlType.Custom, controlType); + + var children = peer.GetChildren(); + Assert.NotNull(children); + Assert.NotEmpty(children); + + await Task.CompletedTask; + }); + } } } diff --git a/src/Core/src/Platform/Windows/ContentPanel.cs b/src/Core/src/Platform/Windows/ContentPanel.cs index 61c4e6677e2a..b6465846f81a 100644 --- a/src/Core/src/Platform/Windows/ContentPanel.cs +++ b/src/Core/src/Platform/Windows/ContentPanel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Numerics; using Microsoft.Graphics.Canvas; using Microsoft.Maui.Graphics; @@ -9,6 +10,8 @@ #endif using Microsoft.UI.Composition; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Hosting; using Microsoft.UI.Xaml.Shapes; @@ -73,6 +76,25 @@ public ContentPanel() SizeChanged += ContentPanelSizeChanged; } + // Custom automation peer prevents duplicate announcements when AutomationProperties.Name is set + protected override AutomationPeer OnCreateAutomationPeer() => new ContentPanelAutomationPeer(this); + + internal partial class ContentPanelAutomationPeer : FrameworkElementAutomationPeer + { + internal ContentPanelAutomationPeer(ContentPanel owner) : base(owner) { } + + bool HasDescription => !string.IsNullOrWhiteSpace(AutomationProperties.GetName(Owner)); + + protected override AutomationControlType GetAutomationControlTypeCore() => + HasDescription ? AutomationControlType.Text : AutomationControlType.Custom; + + protected override string GetLocalizedControlTypeCore() => + HasDescription ? string.Empty : base.GetLocalizedControlTypeCore() ?? string.Empty; + + protected override IList? GetChildrenCore() => + HasDescription ? null : base.GetChildrenCore(); + } + void ContentPanelSizeChanged(object sender, SizeChangedEventArgs e) { if (_borderPath is null) diff --git a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index f02df3a74f88..cb86b4077b38 100644 --- a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1,2 +1,3 @@ #nullable enable override Microsoft.Maui.Platform.MauiPasswordTextBox.OnCreateAutomationPeer() -> Microsoft.UI.Xaml.Automation.Peers.AutomationPeer! +override Microsoft.Maui.Platform.ContentPanel.OnCreateAutomationPeer() -> Microsoft.UI.Xaml.Automation.Peers.AutomationPeer!