diff --git a/src/Controls/src/Core/Element/Element.Windows.cs b/src/Controls/src/Core/Element/Element.Windows.cs index aa249666f61e..6a1dfae9a002 100644 --- a/src/Controls/src/Core/Element/Element.Windows.cs +++ b/src/Controls/src/Core/Element/Element.Windows.cs @@ -2,6 +2,9 @@ using System; using System.Collections.Generic; using System.Text; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation.Peers; +using NativeAutomationProperties = Microsoft.UI.Xaml.Automation.AutomationProperties; namespace Microsoft.Maui.Controls { @@ -12,8 +15,22 @@ public static void MapAutomationPropertiesIsInAccessibleTree(IElementHandler han if (handler.IsConnectingHandler() && element.GetValue(AutomationProperties.IsInAccessibleTreeProperty) is null) return; - Platform.AccessibilityExtensions.SetAutomationPropertiesAccessibilityView( - handler.PlatformView as Microsoft.UI.Xaml.FrameworkElement, element); + if (handler.PlatformView is not FrameworkElement platformView) + return; + + var isInAccessibleTree = (bool?)element.GetValue(AutomationProperties.IsInAccessibleTreeProperty); + if (isInAccessibleTree == true) + { + platformView.SetValue(NativeAutomationProperties.AccessibilityViewProperty, AccessibilityView.Content); + } + else if (isInAccessibleTree == false) + { + platformView.SetValue(NativeAutomationProperties.AccessibilityViewProperty, AccessibilityView.Raw); + } + else + { + platformView.ClearValue(NativeAutomationProperties.AccessibilityViewProperty); + } } public static void MapAutomationPropertiesLabeledBy(IElementHandler handler, Element element) diff --git a/src/Controls/tests/DeviceTests/Elements/Layout/LayoutTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/Layout/LayoutTests.Windows.cs index b9d56dcb72cb..7d0916be23c0 100644 --- a/src/Controls/tests/DeviceTests/Elements/Layout/LayoutTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/Layout/LayoutTests.Windows.cs @@ -6,7 +6,9 @@ using Microsoft.Maui.Controls; using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; +using Microsoft.Maui.Hosting; using Microsoft.Maui.Platform; +using Microsoft.UI.Xaml.Automation.Peers; using Xunit; namespace Microsoft.Maui.DeviceTests @@ -26,5 +28,266 @@ void ValidateInputTransparentOnPlatformView(IView view) Assert.Equal(view.InputTransparent, !handler.PlatformView.IsHitTestVisible); } } + void SetupLayoutBuilder() + { + EnsureHandlerCreated(builder => + { + builder.ConfigureMauiHandlers(handlers => + { + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + }); + }); + } + + [Fact(DisplayName = "LayoutPanel creates a MauiLayoutAutomationPeer")] + public async Task LayoutPanelCreatesMauiLayoutAutomationPeer() + { + SetupLayoutBuilder(); + + var grid = new Grid(); + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + Assert.IsType(peer); + }); + } + + [Fact(DisplayName = "LayoutPanel AutomationPeer default control type is Custom")] + public async Task LayoutPanelAutomationPeerDefaultControlTypeIsCustom() + { + SetupLayoutBuilder(); + + var grid = new Grid(); + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + Assert.Equal(AutomationControlType.Custom, peer.GetAutomationControlType()); + Assert.Equal(string.Empty, peer.GetLocalizedControlType()); + }); + } + + [Theory(DisplayName = "LayoutPanel class name reflects cross-platform layout type")] + [InlineData(typeof(Grid), "Grid")] + [InlineData(typeof(VerticalStackLayout), "VerticalStackLayout")] + [InlineData(typeof(HorizontalStackLayout), "HorizontalStackLayout")] + [InlineData(typeof(AbsoluteLayout), "AbsoluteLayout")] + [InlineData(typeof(FlexLayout), "FlexLayout")] + [InlineData(typeof(StackLayout), "StackLayout")] + public async Task LayoutPanelClassNameReflectsCrossPlatformType(Type layoutType, string expectedClassName) + { + SetupLayoutBuilder(); + + var layout = (Layout)Activator.CreateInstance(layoutType)!; + + // FlexLayout._root is only initialized when it has a MAUI parent. + // Wrap it in a VerticalStackLayout so OnParentSet() fires before layout runs. + Layout root = layout is FlexLayout + ? new VerticalStackLayout { layout } + : layout; + + await AttachAndRun(root, (LayoutHandler handler) => + { + // For FlexLayout, find the inner FlexLayout's platform view via its handler + var targetView = layout is FlexLayout + ? (layout.Handler as LayoutHandler)?.PlatformView ?? handler.PlatformView + : handler.PlatformView; + + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(targetView); + Assert.Equal(expectedClassName, peer.GetClassName()); + }); + } + + [Theory(DisplayName = "LayoutPanel AutomationId is exposed via the automation peer")] + [InlineData(true)] + [InlineData(false)] + public async Task LayoutPanelAutomationIdIsExposedViaPeer(bool hasAutomationId) + { + SetupLayoutBuilder(); + + var grid = new Grid(); + if (hasAutomationId) + grid.AutomationId = "TestGrid"; + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + var expected = hasAutomationId ? "TestGrid" : string.Empty; + Assert.Equal(expected, peer.GetAutomationId()); + }); + } + + [Fact(DisplayName = "LayoutPanel AutomationPeer is not keyboard focusable")] + public async Task LayoutPanelAutomationPeerIsNotKeyboardFocusable() + { + SetupLayoutBuilder(); + + var grid = new Grid(); + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + Assert.False(peer.IsKeyboardFocusable()); + }); + } + + [Fact(DisplayName = "LayoutPanel without automation signals is excluded from Control and Content views")] + public async Task LayoutPanelWithoutAutomationSignalsIsExcludedFromControlAndContentViews() + { + SetupLayoutBuilder(); + + var grid = new Grid(); + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + Assert.False(peer.IsControlElement()); + Assert.False(peer.IsContentElement()); + }); + } + + [Fact(DisplayName = "LayoutPanel with AutomationId is included in Control view only")] + public async Task LayoutPanelWithAutomationIdIsIncludedInControlViewOnly() + { + SetupLayoutBuilder(); + + var grid = new Grid { AutomationId = "TestGrid" }; + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + + Assert.Equal("TestGrid", peer.GetAutomationId()); + Assert.True(peer.IsControlElement()); + Assert.False(peer.IsContentElement()); + Assert.Equal(AutomationControlType.Custom, peer.GetAutomationControlType()); + Assert.Equal(string.Empty, peer.GetLocalizedControlType()); + }); + } + + [Fact(DisplayName = "LayoutPanel opts into Control view when AutomationProperties.IsInAccessibleTree is true")] + public async Task LayoutPanelOptsIntoControlViewViaIsInAccessibleTree() + { + SetupLayoutBuilder(); + + var grid = new Grid(); + AutomationProperties.SetIsInAccessibleTree(grid, true); + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + Assert.True(peer.IsControlElement()); + Assert.True(peer.IsContentElement()); + Assert.Equal(AutomationControlType.Pane, peer.GetAutomationControlType()); + }); + } + + [Fact(DisplayName = "LayoutPanel opts into Control view when SemanticProperties.Description is set")] + public async Task LayoutPanelOptsIntoControlViewWhenDescriptionIsSet() + { + SetupLayoutBuilder(); + + var grid = new Grid(); + SemanticProperties.SetDescription(grid, "Welcome card"); + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + Assert.True(peer.IsControlElement()); + Assert.Equal(AutomationControlType.Pane, peer.GetAutomationControlType()); + }); + } + + [Fact(DisplayName = "LayoutPanel opts into Control view when SemanticProperties.Hint is set")] + public async Task LayoutPanelOptsIntoControlViewWhenHintIsSet() + { + SetupLayoutBuilder(); + + var grid = new Grid(); + SemanticProperties.SetHint(grid, "Contains welcome card actions"); + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + Assert.True(peer.IsControlElement()); + Assert.Equal(AutomationControlType.Pane, peer.GetAutomationControlType()); + }); + } + + [Fact(DisplayName = "LayoutPanel explicit accessible-tree opt out overrides AutomationId and SemanticProperties.Description")] + public async Task LayoutPanelAccessibleTreeOptOutOverridesAutomationIdAndDescription() + { + SetupLayoutBuilder(); + + var grid = new Grid { AutomationId = "TestGrid" }; + SemanticProperties.SetDescription(grid, "Welcome card"); + AutomationProperties.SetIsInAccessibleTree(grid, false); + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + Assert.Equal("TestGrid", peer.GetAutomationId()); + Assert.False(peer.IsControlElement()); + Assert.False(peer.IsContentElement()); + }); + } + + [Fact(DisplayName = "LayoutPanel ignores whitespace-only SemanticProperties.Description")] + public async Task LayoutPanelDoesNotOptIntoControlViewWhenDescriptionIsWhitespace() + { + SetupLayoutBuilder(); + + var grid = new Grid(); + SemanticProperties.SetDescription(grid, " "); + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + Assert.False(peer.IsControlElement()); + }); + } + + [Fact(DisplayName = "LayoutPanel ignores whitespace-only SemanticProperties.Hint")] + public async Task LayoutPanelDoesNotOptIntoControlViewWhenHintIsWhitespace() + { + SetupLayoutBuilder(); + + var grid = new Grid(); + SemanticProperties.SetHint(grid, " "); + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + Assert.False(peer.IsControlElement()); + }); + } + + [Fact(DisplayName = "LayoutPanel AutomationId is exposed via the peer when set at runtime")] + public async Task LayoutPanelAutomationPeerUpdatesWhenAutomationIdChangesAtRuntime() + { + SetupLayoutBuilder(); + + var grid = new Grid(); + + await AttachAndRun(grid, (LayoutHandler handler) => + { + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(handler.PlatformView); + + // Initially no AutomationId. + Assert.Equal(string.Empty, peer.GetAutomationId()); + + // Set AutomationId at runtime -- peer should reflect the new value. + // Note: MAUI AutomationId is write-once (Element.cs enforces this), + // so we can only test the transition from unset -> set, not set -> cleared. + grid.AutomationId = "DynamicGrid"; + Assert.Equal("DynamicGrid", peer.GetAutomationId()); + }); + } } } diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue4715.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue4715.cs new file mode 100644 index 000000000000..eb0b4f167227 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue4715.cs @@ -0,0 +1,151 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 4715, "[Windows] Layout containers not visible to UI automation", PlatformAffected.UWP)] +public class Issue4715 : ContentPage +{ + public Issue4715() + { + // Grid with AutomationId only. This verifies UI tests can find layout containers + // by AutomationId without needing explicit accessible-tree opt-in. + var testGrid = new Grid + { + AutomationId = "TestGrid", + BackgroundColor = Colors.LightBlue, + HeightRequest = 60, + Children = + { + new Label { Text = "Grid", VerticalOptions = LayoutOptions.Center, HorizontalOptions = LayoutOptions.Center } + } + }; + + // VerticalStackLayout with AutomationId and explicit accessible-tree opt-in. + var testVerticalStackLayout = new VerticalStackLayout + { + AutomationId = "TestVerticalStackLayout", + BackgroundColor = Colors.LightGreen, + Children = + { + new Label { Text = "VerticalStackLayout", Padding = new Thickness(8) } + } + }; + SemanticProperties.SetDescription(testVerticalStackLayout, "Test vertical stack layout"); + AutomationProperties.SetIsInAccessibleTree(testVerticalStackLayout, true); + + // HorizontalStackLayout with AutomationId and explicit accessible-tree opt-in. + var testHorizontalStackLayout = new HorizontalStackLayout + { + AutomationId = "TestHorizontalStackLayout", + BackgroundColor = Colors.LightYellow, + Children = + { + new Label { Text = "HorizontalStackLayout", Padding = new Thickness(8) } + } + }; + SemanticProperties.SetDescription(testHorizontalStackLayout, "Test horizontal stack layout"); + AutomationProperties.SetIsInAccessibleTree(testHorizontalStackLayout, true); + + // FlexLayout with AutomationId and explicit accessible-tree opt-in. + var testFlexLayout = new FlexLayout + { + AutomationId = "TestFlexLayout", + BackgroundColor = Colors.LightPink, + HeightRequest = 60, + Children = + { + new Label { Text = "FlexLayout", Margin = new Thickness(8) } + } + }; + SemanticProperties.SetDescription(testFlexLayout, "Test flex layout"); + AutomationProperties.SetIsInAccessibleTree(testFlexLayout, true); + + // AbsoluteLayout with AutomationId and explicit accessible-tree opt-in. + var testAbsoluteLayout = new AbsoluteLayout + { + AutomationId = "TestAbsoluteLayout", + BackgroundColor = Colors.LightSteelBlue, + HeightRequest = 60, + Children = + { + new Label + { + Text = "AbsoluteLayout", + Margin = new Thickness(8) + } + } + }; + SemanticProperties.SetDescription(testAbsoluteLayout, "Test absolute layout"); + AutomationProperties.SetIsInAccessibleTree(testAbsoluteLayout, true); + + // Nested layout — outer has AutomationId and explicit accessible-tree opt-in, inner is anonymous. + var testNestedOuterGrid = new Grid + { + AutomationId = "TestNestedOuterGrid", + BackgroundColor = Colors.Lavender, + HeightRequest = 80, + Children = + { + new VerticalStackLayout + { + // No AutomationId — anonymous inner layout + Children = + { + new Label { Text = "Nested: Outer Grid (named)", HorizontalOptions = LayoutOptions.Center }, + new Label { Text = "Inner VerticalStackLayout (anonymous)", HorizontalOptions = LayoutOptions.Center, FontSize = 11 } + } + } + } + }; + SemanticProperties.SetDescription(testNestedOuterGrid, "Test nested outer grid layout"); + AutomationProperties.SetIsInAccessibleTree(testNestedOuterGrid, true); + + // Layout with an AutomationId but an explicit accessible-tree opt-out (IsInAccessibleTree="False"). + // The Raw opt-out removes it from the UIA Control view, so Appium must NOT be able to find it by + // its AutomationId — proving the explicit opt-out takes precedence over the AutomationId test hook. + var testOptedOutGrid = new Grid + { + AutomationId = "OptedOutGrid", + BackgroundColor = Colors.LightGray, + HeightRequest = 60, + Children = + { + new Label { Text = "Opted-out Grid (IsInAccessibleTree=False)", VerticalOptions = LayoutOptions.Center, HorizontalOptions = LayoutOptions.Center } + } + }; + AutomationProperties.SetIsInAccessibleTree(testOptedOutGrid, false); + + var scrollView = new ScrollView + { + Content = new VerticalStackLayout + { + Spacing = 10, + Padding = new Thickness(16), + Children = + { + new Label + { + Text = "Layout Automation Peer Test", + FontSize = 18, + FontAttributes = FontAttributes.Bold, + AutomationId = "PageTitle" + }, + testGrid, + testVerticalStackLayout, + testHorizontalStackLayout, + testFlexLayout, + testAbsoluteLayout, + testNestedOuterGrid, + testOptedOutGrid, + + // Sentinel label to confirm page has loaded + new Label + { + Text = "All layouts rendered", + AutomationId = "WaitForStubControl" + } + } + } + }; + + Content = scrollView; + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue4715.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue4715.cs new file mode 100644 index 000000000000..bb59502f29d9 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue4715.cs @@ -0,0 +1,85 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue4715 : _IssuesUITest +{ + public Issue4715(TestDevice device) : base(device) { } + + public override string Issue => "[Windows] Layout containers not visible to UI automation"; + + [Test] + [Category(UITestCategories.Accessibility)] + public void GridWithAutomationIdOnlyIsFoundByAppium() + { + App.WaitForElement("WaitForStubControl"); + + // AutomationId-only layouts must remain visible to Windows UI tests. + App.WaitForElement("TestGrid"); + } + + [Test] + [Category(UITestCategories.Accessibility)] + public void VerticalStackLayoutWithAccessibleTreeOptInIsFoundByAppium() + { + App.WaitForElement("WaitForStubControl"); + + // VerticalStackLayout with explicit accessible-tree opt-in must be visible in the UIA tree. + App.WaitForElement("TestVerticalStackLayout"); + } + + [Test] + [Category(UITestCategories.Accessibility)] + public void HorizontalStackLayoutWithAccessibleTreeOptInIsFoundByAppium() + { + App.WaitForElement("WaitForStubControl"); + + // HorizontalStackLayout with explicit accessible-tree opt-in must be visible in the UIA tree. + App.WaitForElement("TestHorizontalStackLayout"); + } + + [Test] + [Category(UITestCategories.Accessibility)] + public void FlexLayoutWithAccessibleTreeOptInIsFoundByAppium() + { + App.WaitForElement("WaitForStubControl"); + + // FlexLayout with explicit accessible-tree opt-in must be visible in the UIA tree. + App.WaitForElement("TestFlexLayout"); + } + + [Test] + [Category(UITestCategories.Accessibility)] + public void AbsoluteLayoutWithAccessibleTreeOptInIsFoundByAppium() + { + App.WaitForElement("WaitForStubControl"); + + // AbsoluteLayout with explicit accessible-tree opt-in must be visible in the UIA tree. + App.WaitForElement("TestAbsoluteLayout"); + } + + [Test] + [Category(UITestCategories.Accessibility)] + public void NestedOuterLayoutWithAccessibleTreeOptInIsFoundByAppium() + { + App.WaitForElement("WaitForStubControl"); + + // Outer nested Grid with explicit accessible-tree opt-in must be visible. + App.WaitForElement("TestNestedOuterGrid"); + } + + [Test] + [Category(UITestCategories.Accessibility)] + public void LayoutWithAccessibleTreeOptOutIsNotFoundByAppium() + { + App.WaitForElement("WaitForStubControl"); + + // A layout with an AutomationId but an explicit IsInAccessibleTree="False" opts out of the + // UIA Control view. Appium must NOT find it by its AutomationId, proving the Raw opt-out takes + // precedence over the AutomationId discoverability hook. Anonymous (no-AutomationId) layout + // exclusion is covered authoritatively by the LayoutPanel device tests. + App.WaitForNoElement("OptedOutGrid"); + } +} diff --git a/src/Core/src/Platform/Windows/LayoutPanel.cs b/src/Core/src/Platform/Windows/LayoutPanel.cs index 2ee75ec4df3b..8f1b1f071a23 100644 --- a/src/Core/src/Platform/Windows/LayoutPanel.cs +++ b/src/Core/src/Platform/Windows/LayoutPanel.cs @@ -1,4 +1,5 @@ #nullable enable +using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using WRect = global::Windows.Foundation.Rect; @@ -12,6 +13,11 @@ public partial class LayoutPanel : MauiPanel Canvas? _backgroundLayer; public bool ClipsToBounds { get; set; } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new MauiLayoutAutomationPeer(this); + } + // TODO: Possibly reconcile this code with ViewHandlerExtensions.LayoutVirtualView // If you make changes here please review if those changes should also // apply to ViewHandlerExtensions.LayoutVirtualView diff --git a/src/Core/src/Platform/Windows/MauiLayoutAutomationPeer.cs b/src/Core/src/Platform/Windows/MauiLayoutAutomationPeer.cs new file mode 100644 index 000000000000..afcee9d57275 --- /dev/null +++ b/src/Core/src/Platform/Windows/MauiLayoutAutomationPeer.cs @@ -0,0 +1,135 @@ +#nullable enable +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Automation.Peers; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.Maui.Platform +{ + public partial class MauiLayoutAutomationPeer : FrameworkElementAutomationPeer + { + public MauiLayoutAutomationPeer(LayoutPanel owner) : base(owner) { } + + // Returns the cross-platform layout type name (e.g. "Grid", "VerticalStackLayout") so + // accessibility tools can distinguish between different layout types. Falls back to "Panel" + // when the cross-platform layout is unavailable. + protected override string GetClassNameCore() + { + if (Owner is MauiPanel panel) + { + return panel.CrossPlatformLayout?.GetType().Name ?? nameof(Panel); + } + + return nameof(Panel); + } + + // Layouts with accessibility semantics use Pane to signal a grouping/container element. + // AutomationId-only layouts remain discoverable to Windows UI automation, but report as + // Custom so screen readers do not announce them as meaningful groups. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return HasAccessibilitySemantics() + ? AutomationControlType.Pane + : AutomationControlType.Custom; + } + + protected override string GetLocalizedControlTypeCore() + { + return HasAccessibilitySemantics() + ? base.GetLocalizedControlTypeCore() ?? string.Empty + : string.Empty; + } + + // Layouts are never keyboard-focusable — they are structural containers, not interactive elements. + protected override bool IsKeyboardFocusableCore() => false; + + // Layouts/panels are structural containers. By default we exclude anonymous layouts from + // the UIA Control view so screen readers (Narrator, NVDA) don't stop on every nested Grid / + // StackLayout while a user is navigating a page. + // + // We DO opt in to the Control view when the developer signals that this panel should + // be discoverable through UI Automation: + // * AutomationId on the cross-platform view, so Windows Appium/WinAppDriver can find + // layout containers by their test hook. This is a deliberate trade-off: Windows UI + // test automation does not see these peers unless they participate in Control view, + // so named layout containers become visible to screen readers too. + // * AutomationProperties.IsInAccessibleTree="True" on the cross-platform view + // (mapped to AccessibilityView = Content on the platform view; Control can still be + // set directly by platform code), or + // * SemanticProperties.Description / Hint (mapped to AutomationProperties.Name / + // HelpText on the platform view) -- if the developer wrote a description, they + // want screen readers to announce it. + protected override bool IsControlElementCore() + { + if (Owner is not Panel panel) + { + return false; + } + + var accessibilityView = panel.ReadLocalValue(AutomationProperties.AccessibilityViewProperty); + + // Explicit opt-out wins over any AutomationId or semantic opt-in signal. + if (accessibilityView is AccessibilityView.Raw) + { + return false; + } + else if (accessibilityView is AccessibilityView.Control or AccessibilityView.Content) + { + return true; + } + + if (!string.IsNullOrWhiteSpace(AutomationProperties.GetAutomationId(panel))) + { + return true; + } + + if (!string.IsNullOrWhiteSpace(AutomationProperties.GetName(panel))) + { + return true; + } + + if (!string.IsNullOrWhiteSpace(AutomationProperties.GetHelpText(panel))) + { + return true; + } + + return false; + } + + // Expose in the Content View only when the developer explicitly opted into Content via + // AutomationProperties.IsInAccessibleTree="True" (mapped to AccessibilityView = Content). + // AutomationId keeps the panel discoverable to Windows UI test automation through the + // Control view, but does NOT pull the panel into the Content view. + protected override bool IsContentElementCore() + { + if (Owner is not Panel panel) + { + return false; + } + + var accessibilityView = panel.ReadLocalValue(AutomationProperties.AccessibilityViewProperty); + return accessibilityView is AccessibilityView.Content; + } + + bool HasAccessibilitySemantics() + { + if (Owner is not Panel panel) + { + return false; + } + + var accessibilityView = panel.ReadLocalValue(AutomationProperties.AccessibilityViewProperty); + + if (accessibilityView is AccessibilityView.Raw) + { + return false; + } + else if (accessibilityView is AccessibilityView.Control or AccessibilityView.Content) + { + return true; + } + + return !string.IsNullOrWhiteSpace(AutomationProperties.GetName(panel)) || + !string.IsNullOrWhiteSpace(AutomationProperties.GetHelpText(panel)); + } + } +} diff --git a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index dc3f33edec53..b2f8ce998eb9 100644 --- a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1,4 +1,13 @@ #nullable enable +Microsoft.Maui.Platform.MauiLayoutAutomationPeer +Microsoft.Maui.Platform.MauiLayoutAutomationPeer.MauiLayoutAutomationPeer(Microsoft.Maui.Platform.LayoutPanel! owner) -> void +override Microsoft.Maui.Platform.LayoutPanel.OnCreateAutomationPeer() -> Microsoft.UI.Xaml.Automation.Peers.AutomationPeer! +override Microsoft.Maui.Platform.MauiLayoutAutomationPeer.GetAutomationControlTypeCore() -> Microsoft.UI.Xaml.Automation.Peers.AutomationControlType +override Microsoft.Maui.Platform.MauiLayoutAutomationPeer.GetClassNameCore() -> string! +override Microsoft.Maui.Platform.MauiLayoutAutomationPeer.GetLocalizedControlTypeCore() -> string! +override Microsoft.Maui.Platform.MauiLayoutAutomationPeer.IsContentElementCore() -> bool +override Microsoft.Maui.Platform.MauiLayoutAutomationPeer.IsControlElementCore() -> bool +override Microsoft.Maui.Platform.MauiLayoutAutomationPeer.IsKeyboardFocusableCore() -> bool override Microsoft.Maui.Platform.MauiPasswordTextBox.OnCreateAutomationPeer() -> Microsoft.UI.Xaml.Automation.Peers.AutomationPeer! static Microsoft.Maui.GridLength.implicit operator Microsoft.Maui.GridLength(string! value) -> Microsoft.Maui.GridLength static Microsoft.Maui.SafeAreaEdges.Container.get -> Microsoft.Maui.SafeAreaEdges