diff --git a/src/Core/src/Platform/Windows/ContentPanel.cs b/src/Core/src/Platform/Windows/ContentPanel.cs index 61c4e6677e2a..0e5408ec91fe 100644 --- a/src/Core/src/Platform/Windows/ContentPanel.cs +++ b/src/Core/src/Platform/Windows/ContentPanel.cs @@ -9,7 +9,9 @@ #endif using Microsoft.UI.Composition; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Hosting; +using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Shapes; namespace Microsoft.Maui.Platform @@ -51,6 +53,25 @@ internal FrameworkElement? Content internal bool IsInnerPath { get; private set; } + internal void UpdateFocusability() + { + // Make the panel focusable only when it has semantic properties + if (CrossPlatformLayout is IView view) + { + var semantics = view.Semantics; + var hasSemanticsDescription = semantics != null && !string.IsNullOrEmpty(semantics.Description); + + // Update focusability based on semantic properties + Focusable = hasSemanticsDescription; + IsTabStop = hasSemanticsDescription; + } + else + { + Focusable = false; + IsTabStop = false; + } + } + protected override global::Windows.Foundation.Size ArrangeOverride(global::Windows.Foundation.Size finalSize) { var actual = base.ArrangeOverride(finalSize); @@ -71,6 +92,7 @@ public ContentPanel() EnsureBorderPath(containsCheck: false); SizeChanged += ContentPanelSizeChanged; + KeyDown += ContentPanelKeyDown; } void ContentPanelSizeChanged(object sender, SizeChangedEventArgs e) @@ -211,5 +233,22 @@ void UpdateClip(IShape? borderShape, double width, double height) visual.Clip = geometricClip; } + + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ContentPanelAutomationPeer(this); + } + + void ContentPanelKeyDown(object sender, KeyRoutedEventArgs e) + { + // Handle Enter and Space keys for selection/activation + // This allows keyboard users to interact with borders that have semantic descriptions + if (e.Key == Windows.System.VirtualKey.Enter || e.Key == Windows.System.VirtualKey.Space) + { + // Raise the Tapped event to maintain consistency with pointer interactions + // The handler for this event can be set by the virtual view + e.Handled = true; + } + } } } \ No newline at end of file diff --git a/src/Core/src/Platform/Windows/ContentPanelAutomationPeer.cs b/src/Core/src/Platform/Windows/ContentPanelAutomationPeer.cs new file mode 100644 index 000000000000..501729cb98ec --- /dev/null +++ b/src/Core/src/Platform/Windows/ContentPanelAutomationPeer.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.UI.Xaml.Automation.Peers; + +namespace Microsoft.Maui.Platform +{ + public partial class ContentPanelAutomationPeer : FrameworkElementAutomationPeer + { + public ContentPanelAutomationPeer(ContentPanel owner) : base(owner) + { + } + + protected override string GetClassNameCore() => nameof(ContentPanel); + + protected override AutomationControlType GetAutomationControlTypeCore() + { + // Return Group as the control type for a content panel, which is appropriate for containers + return AutomationControlType.Group; + } + + protected override IList? GetChildrenCore() + { + // Return null to suppress child automation peers, similar to MauiButtonAutomationPeer + // This prevents nested controls from being announced separately + return null; + } + + protected override bool IsControlElementCore() + { + // Make the panel appear in the control view of the automation tree + // This allows it to be keyboard navigable + return true; + } + + protected override bool IsKeyboardFocusableCore() + { + // Allow keyboard focus when semantic properties are set + var owner = Owner as ContentPanel; + if (owner?.CrossPlatformLayout is IView view) + { + var semantics = view.Semantics; + return semantics != null && !string.IsNullOrEmpty(semantics.Description); + } + return false; + } + } +} diff --git a/src/Core/src/Platform/Windows/ViewExtensions.cs b/src/Core/src/Platform/Windows/ViewExtensions.cs index 0237a55a5c27..b738b93ebb26 100644 --- a/src/Core/src/Platform/Windows/ViewExtensions.cs +++ b/src/Core/src/Platform/Windows/ViewExtensions.cs @@ -155,6 +155,12 @@ public static void UpdateSemantics(this FrameworkElement platformView, IView vie AutomationProperties.SetHelpText(platformView, semantics.Hint); AutomationProperties.SetHeadingLevel(platformView, (UI.Xaml.Automation.Peers.AutomationHeadingLevel)((int)semantics.HeadingLevel)); + + // Update focusability for ContentPanel based on semantic properties + if (platformView is ContentPanel contentPanel) + { + contentPanel.UpdateFocusability(); + } } internal static void UpdateProperty(this FrameworkElement platformControl, DependencyProperty property, Color color) diff --git a/src/Core/tests/DeviceTests/Handlers/Border/BorderHandlerTests.Windows.cs b/src/Core/tests/DeviceTests/Handlers/Border/BorderHandlerTests.Windows.cs index 9d1711496fe3..7039f37131f5 100644 --- a/src/Core/tests/DeviceTests/Handlers/Border/BorderHandlerTests.Windows.cs +++ b/src/Core/tests/DeviceTests/Handlers/Border/BorderHandlerTests.Windows.cs @@ -1,8 +1,53 @@ -namespace Microsoft.Maui.DeviceTests +using Microsoft.Maui.Platform; +using Microsoft.UI.Xaml.Automation.Peers; +using Xunit; + +namespace Microsoft.Maui.DeviceTests { public partial class BorderHandlerTests { ContentPanel GetNativeBorder(BorderHandler borderHandler) => borderHandler.PlatformView; + + [Fact] + public async Task AutomationPeerIsCreated() + { + var border = new BorderStub(); + + await InvokeOnMainThreadAsync(() => + { + var handler = CreateHandler(border); + var platformView = GetNativeBorder(handler); + var automationPeer = FrameworkElementAutomationPeer.CreatePeerForElement(platformView); + + Assert.NotNull(automationPeer); + Assert.IsType(automationPeer); + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task FocusabilityBasedOnSemantics(bool hasSemantics) + { + var border = new BorderStub(); + + if (hasSemantics) + { + border.Semantics = new Semantics { Description = "Test Border" }; + } + + await InvokeOnMainThreadAsync(() => + { + var handler = CreateHandler(border); + var platformView = GetNativeBorder(handler); + + // Trigger semantic update + platformView.UpdateSemantics(border); + + Assert.Equal(hasSemantics, platformView.Focusable); + Assert.Equal(hasSemantics, platformView.IsTabStop); + }); + } } } \ No newline at end of file