Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion src/Controls/src/Core/RadioButton/RadioButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand All @@ -752,6 +752,71 @@ private protected override Semantics UpdateSemantics()
return semantics;
}

string GetSemanticDescriptionFromContent()
{
if (Content is string contentText)
{
return contentText;
}

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;
}

if (Value is string valueText && !string.IsNullOrWhiteSpace(valueText))
{
return valueText;
}

return ContentAsString();
Comment thread
SubhikshaSf4851 marked this conversation as resolved.
}

static bool TryGetSemanticDescription(IView view, out string semanticDescription)
{
semanticDescription = null;

if (view is 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 index = 0; index < layout.Count; index++)
{
var child = layout[index];

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,89 @@ 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<RadioButtonHandler>(radioButton, _ =>
{
Assert.Equal("Dog", (radioButton as IView).Semantics.Description);
return Task.CompletedTask;
});
}

[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<RadioButtonHandler>(radioButton, _ =>
{
Assert.Equal("Custom Description", (radioButton as IView).Semantics.Description);
return Task.CompletedTask;
});
}

void EnsureTemplatedRadioButtonHandlersCreated()
{
EnsureHandlerCreated(builder =>
{
builder.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<Border, BorderHandler>();
handlers.AddHandler<Shape, ShapeViewHandler>();
handlers.AddHandler<ContentPresenter, ContentViewHandler>();
handlers.AddHandler<RadioButton, RadioButtonHandler>();
handlers.AddHandler<Label, LabelHandler>();
handlers.AddHandler<Grid, LayoutHandler>();
handlers.AddHandler<VerticalStackLayout, LayoutHandler>();
});
});
}

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;
}
Comment thread
SubhikshaSf4851 marked this conversation as resolved.
}
}
}
9 changes: 9 additions & 0 deletions src/Core/src/Platform/Android/SemanticExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -90,6 +92,13 @@ public static void UpdateSemanticNodeInfo(this View platformView, IView virtualV
if (!string.IsNullOrWhiteSpace(newText))
info.Text = newText;

if (virtualView is IRadioButton radioButton)
{
info.ClassName = s_radioButtonClassName;
info.Checkable = true;
info.Checked = radioButton.IsChecked;
}

if (!string.IsNullOrWhiteSpace(virtualView.AutomationId) &&
platformView?.Context != null)
{
Expand Down
Loading