From f39847de13b1caa75e2541fc95b507e1589de59f Mon Sep 17 00:00:00 2001 From: Subhiksha Chandrasekaran Date: Mon, 16 Mar 2026 12:04:49 +0530 Subject: [PATCH 1/3] Fix for radiobutton talkback --- .../src/Core/RadioButton/RadioButton.cs | 52 ++++++++++++++++- .../Elements/RadioButton/RadioButtonTests.cs | 57 ++++++++++++++++++- .../Platform/Android/SemanticExtensions.cs | 7 +++ 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/Core/RadioButton/RadioButton.cs b/src/Controls/src/Core/RadioButton/RadioButton.cs index 88441378372d..5a9ae994490a 100644 --- a/src/Controls/src/Core/RadioButton/RadioButton.cs +++ b/src/Controls/src/Core/RadioButton/RadioButton.cs @@ -740,7 +740,7 @@ private protected override Semantics UpdateSemantics() if (ControlTemplate != null) { - string contentAsString = ContentAsString(); + string contentAsString = GetSemanticDescriptionFromContent(); if (!string.IsNullOrWhiteSpace(contentAsString) && string.IsNullOrWhiteSpace(semantics?.Description)) { @@ -752,6 +752,56 @@ private protected override Semantics UpdateSemantics() return semantics; } + string GetSemanticDescriptionFromContent() + { + if (Content is string contentText) + return contentText; + + if (Content is IView contentView && TryGetSemanticDescription(contentView, out var semanticDescription)) + return semanticDescription; + + if (Value is string valueText && !string.IsNullOrWhiteSpace(valueText)) + return valueText; + + return ContentAsString(); + } + + static bool TryGetSemanticDescription(IView view, out string semanticDescription) + { + semanticDescription = null; + + if (view == null) + return false; + + if (!string.IsNullOrWhiteSpace(view.Semantics?.Description)) + { + semanticDescription = view.Semantics.Description; + return true; + } + + if (view is IText text && !string.IsNullOrWhiteSpace(text.Text)) + { + semanticDescription = text.Text; + return true; + } + + if (view is IContentView contentView && contentView.PresentedContent is IView presentedContent && TryGetSemanticDescription(presentedContent, out semanticDescription)) + return true; + + if (view is Microsoft.Maui.ILayout layout) + { + for (int i = 0; i < layout.Count; i++) + { + var child = layout[i]; + + if (TryGetSemanticDescription(child, out semanticDescription)) + return true; + } + } + + return false; + } + class CornerRadiusToShape : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) diff --git a/src/Controls/tests/DeviceTests/Elements/RadioButton/RadioButtonTests.cs b/src/Controls/tests/DeviceTests/Elements/RadioButton/RadioButtonTests.cs index c48b77bb8c3c..48de473a4b08 100644 --- a/src/Controls/tests/DeviceTests/Elements/RadioButton/RadioButtonTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/RadioButton/RadioButtonTests.cs @@ -101,5 +101,60 @@ await InvokeOnMainThreadAsync(() => await AssertionExtensions.WaitForGC(radioButtonHandlerRef, layoutHandlerRef, layoutPlatformRef, radioButtonPlatformRef); } + + [Fact(DisplayName = "Issue 34322 - Templated RadioButton uses content label for semantics")] + public async Task Issue34322_TemplatedRadioButtonUsesContentLabelForSemantics() + { + EnsureTemplatedRadioButtonHandlersCreated(); + + var radioButton = CreateIssue34322RadioButton("Dog", isChecked: false, useSemanticDescription: false); + + await CreateHandlerAndAddToWindow(radioButton, _ => + { + Assert.Equal("Dog", (radioButton as IView).Semantics.Description); + return Task.CompletedTask; + }); + } + + void EnsureTemplatedRadioButtonHandlersCreated() + { + EnsureHandlerCreated(builder => + { + builder.ConfigureMauiHandlers(handlers => + { + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + }); + }); + } + + static RadioButton CreateIssue34322RadioButton(string label, bool isChecked, bool useSemanticDescription) + { + var radioButton = new RadioButton + { + ControlTemplate = RadioButton.DefaultTemplate, + Content = new VerticalStackLayout + { + Children = + { + new Label + { + Text = label + } + } + }, + IsChecked = isChecked, + }; + + if (useSemanticDescription) + SemanticProperties.SetDescription(radioButton, label); + + return radioButton; + } } -} \ No newline at end of file +} diff --git a/src/Core/src/Platform/Android/SemanticExtensions.cs b/src/Core/src/Platform/Android/SemanticExtensions.cs index 9245bf44bc9c..8736dc3ca941 100644 --- a/src/Core/src/Platform/Android/SemanticExtensions.cs +++ b/src/Core/src/Platform/Android/SemanticExtensions.cs @@ -90,6 +90,13 @@ public static void UpdateSemanticNodeInfo(this View platformView, IView virtualV if (!string.IsNullOrWhiteSpace(newText)) info.Text = newText; + if (virtualView is IRadioButton radioButton) + { + info.ClassName = Java.Lang.Class.FromType(typeof(global::Android.Widget.RadioButton)).Name; + info.Checkable = true; + info.Checked = radioButton.IsChecked; + } + if (!string.IsNullOrWhiteSpace(virtualView.AutomationId) && platformView?.Context != null) { From 955ca081fa851077375568c249f202eb2b86b43b Mon Sep 17 00:00:00 2001 From: Subhiksha Chandrasekaran Date: Mon, 16 Mar 2026 19:42:09 +0530 Subject: [PATCH 2/3] Updated concerns --- .../src/Core/RadioButton/RadioButton.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Controls/src/Core/RadioButton/RadioButton.cs b/src/Controls/src/Core/RadioButton/RadioButton.cs index 5a9ae994490a..87ff5b1d15ec 100644 --- a/src/Controls/src/Core/RadioButton/RadioButton.cs +++ b/src/Controls/src/Core/RadioButton/RadioButton.cs @@ -755,13 +755,19 @@ private protected override Semantics UpdateSemantics() string GetSemanticDescriptionFromContent() { if (Content is string contentText) + { return contentText; + } if (Content is IView contentView && TryGetSemanticDescription(contentView, out var semanticDescription)) + { return semanticDescription; + } if (Value is string valueText && !string.IsNullOrWhiteSpace(valueText)) + { return valueText; + } return ContentAsString(); } @@ -770,8 +776,10 @@ static bool TryGetSemanticDescription(IView view, out string semanticDescription { semanticDescription = null; - if (view == null) + if (view is null) + { return false; + } if (!string.IsNullOrWhiteSpace(view.Semantics?.Description)) { @@ -786,16 +794,20 @@ static bool TryGetSemanticDescription(IView view, out string semanticDescription } if (view is IContentView contentView && contentView.PresentedContent is IView presentedContent && TryGetSemanticDescription(presentedContent, out semanticDescription)) + { return true; + } if (view is Microsoft.Maui.ILayout layout) { - for (int i = 0; i < layout.Count; i++) + for (int index = 0; index < layout.Count; index++) { - var child = layout[i]; + var child = layout[index]; if (TryGetSemanticDescription(child, out semanticDescription)) + { return true; + } } } From f9ea7c4761d497a9702b51cafbbb5b78e750dd85 Mon Sep 17 00:00:00 2001 From: Subhiksha Chandrasekaran Date: Thu, 19 Mar 2026 19:30:16 +0530 Subject: [PATCH 3/3] updated suggestions --- .../src/Core/RadioButton/RadioButton.cs | 5 +++- .../Elements/RadioButton/RadioButtonTests.cs | 29 +++++++++++++++++++ .../Platform/Android/SemanticExtensions.cs | 4 ++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/Core/RadioButton/RadioButton.cs b/src/Controls/src/Core/RadioButton/RadioButton.cs index 87ff5b1d15ec..a071264bd8bf 100644 --- a/src/Controls/src/Core/RadioButton/RadioButton.cs +++ b/src/Controls/src/Core/RadioButton/RadioButton.cs @@ -759,8 +759,11 @@ string GetSemanticDescriptionFromContent() return contentText; } - if (Content is IView contentView && TryGetSemanticDescription(contentView, out var semanticDescription)) + if (Content is IView contentView) { + // Don't fall back to ContentAsString() for view-based content — it calls ToString() + // on the view and returns a type name rather than meaningful text. + TryGetSemanticDescription(contentView, out var semanticDescription); return semanticDescription; } diff --git a/src/Controls/tests/DeviceTests/Elements/RadioButton/RadioButtonTests.cs b/src/Controls/tests/DeviceTests/Elements/RadioButton/RadioButtonTests.cs index 48de473a4b08..ad99f8d6e5e8 100644 --- a/src/Controls/tests/DeviceTests/Elements/RadioButton/RadioButtonTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/RadioButton/RadioButtonTests.cs @@ -116,6 +116,35 @@ await CreateHandlerAndAddToWindow(radioButton, _ => }); } + [Fact(DisplayName = "Issue 34322 - Explicit SemanticDescription is not overwritten by content-derived semantics")] + public async Task Issue34322_ExplicitSemanticDescriptionNotOverwrittenByContent() + { + EnsureTemplatedRadioButtonHandlersCreated(); + + var radioButton = new RadioButton + { + ControlTemplate = RadioButton.DefaultTemplate, + Content = new VerticalStackLayout + { + Children = + { + new Label + { + Text = "Dog" + } + } + }, + IsChecked = false, + }; + SemanticProperties.SetDescription(radioButton, "Custom Description"); + + await CreateHandlerAndAddToWindow(radioButton, _ => + { + Assert.Equal("Custom Description", (radioButton as IView).Semantics.Description); + return Task.CompletedTask; + }); + } + void EnsureTemplatedRadioButtonHandlersCreated() { EnsureHandlerCreated(builder => diff --git a/src/Core/src/Platform/Android/SemanticExtensions.cs b/src/Core/src/Platform/Android/SemanticExtensions.cs index 8736dc3ca941..222b2d91dcd3 100644 --- a/src/Core/src/Platform/Android/SemanticExtensions.cs +++ b/src/Core/src/Platform/Android/SemanticExtensions.cs @@ -8,6 +8,8 @@ namespace Microsoft.Maui.Platform { public static partial class SemanticExtensions { + // Cached once to avoid repeated JNI/type lookups on every accessibility traversal. + static readonly string s_radioButtonClassName = Java.Lang.Class.FromType(typeof(global::Android.Widget.RadioButton)).Name; public static void UpdateSemanticNodeInfo(this View platformView, IView virtualView, AccessibilityNodeInfoCompat? info) { if (info == null || virtualView == null) @@ -92,7 +94,7 @@ public static void UpdateSemanticNodeInfo(this View platformView, IView virtualV if (virtualView is IRadioButton radioButton) { - info.ClassName = Java.Lang.Class.FromType(typeof(global::Android.Widget.RadioButton)).Name; + info.ClassName = s_radioButtonClassName; info.Checkable = true; info.Checked = radioButton.IsChecked; }