Skip to content

[dev-v5] Add Radio and RadioGroup #3647

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 15, 2025
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<FluentStack Orientation="Orientation.Vertical" VerticalGap="10">
<FluentRadioGroup TValue="string" Label="Default">
<FluentRadio></FluentRadio>
</FluentRadioGroup>

<FluentRadioGroup TValue="string" Label="Checked" Value="@(string.Empty)">
<FluentRadio Value="@(string.Empty)"></FluentRadio>
</FluentRadioGroup>

<FluentRadioGroup TValue="string" Label="Disabled">
<FluentRadio Disabled="true"></FluentRadio>
</FluentRadioGroup>

<FluentRadioGroup TValue="string" Label="Field">
<FluentRadio Label="Apple" Value="@("Apple")"></FluentRadio>
</FluentRadioGroup>
</FluentStack>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<FluentRadioGroup @bind-Value="@fruit" Label="Favorite fruit" Name="favorite-fruit">
<FluentRadio Value="@("apple")" Label="Apple"></FluentRadio>
<FluentRadio Value="@("banana")" Label="Banana"></FluentRadio>
<FluentRadio Value="@("orange")" Label="Orange"></FluentRadio>
<FluentRadio Value="@("grape")" Label="Grape"></FluentRadio>
<FluentRadio Value="@("kiwi")" Label="Kiwi"></FluentRadio>
</FluentRadioGroup>

<p>Favorite fruit: @fruit</p>

@code {
string? fruit = "banana";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<FluentRadioGroup @bind-Value="@fruit" Disabled="true" Label="Favorite fruit">
<FluentRadio Value="@("apple")" Label="Apple"></FluentRadio>
<FluentRadio Value="@("banana")" Label="Banana"></FluentRadio>
<FluentRadio Value="@("orange")" Label="Orange"></FluentRadio>
<FluentRadio Value="@("grape")" Label="Grape"></FluentRadio>
<FluentRadio Value="@("kiwi")" Label="Kiwi"></FluentRadio>
</FluentRadioGroup>


@code {
private string? fruit;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<FluentRadioGroup @bind-Value="@fruit" Label="Favorite fruit">
<FluentRadio Value="@("apple")" Label="Apple"></FluentRadio>
<FluentRadio Value="@("banana")" Label="Banana"></FluentRadio>
<FluentRadio Value="@("orange")" Label="Orange" Disabled="true"></FluentRadio>
<FluentRadio Value="@("grape")" Label="Grape"></FluentRadio>
<FluentRadio Value="@("kiwi")" Label="Kiwi" Disabled="true"></FluentRadio>
</FluentRadioGroup>


@code {
private string fruit = "banana";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<FluentRadioGroup Name="numbers" @bind-Value="@numberValue" Label="Numbers">
<FluentRadio Value="1">
<LabelTemplate><strong>One</strong></LabelTemplate>
</FluentRadio>
<FluentRadio Value="2">
<LabelTemplate><em>Two</em></LabelTemplate>
</FluentRadio>
</FluentRadioGroup>
<p>
Selected: @(numberValue is null ? "-" : $"{numberValue} (Type: {numberValue?.GetType()})" )
</p>

<FluentRadioGroup Name="strings" @bind-Value="@stringValue" Label="Strings">
<FluentRadio Value="@("one")"><LabelTemplate><em>One</em></LabelTemplate></FluentRadio>
<FluentRadio Value="@("two")"><LabelTemplate><strong>Two</strong></LabelTemplate></FluentRadio>
</FluentRadioGroup>
<p>
Selected: @(stringValue is null ? "-" : $"{stringValue} (Type: {stringValue?.GetType()})")
</p>

@code {
int? numberValue;
string? stringValue;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<form>
<FluentRadioGroup Name="options" Id="radio-group-required" Label="Choose an option" Required="true" @bind-Value="@value" Message="Please select a fruit." >
<FluentRadio Id="option1" Name="options" Value="@("Option 1")" Label="Option 1"></FluentRadio>
<FluentRadio Id="option2" Name="options" Value="@("Option 2")" Label="Option 2"></FluentRadio>
<FluentRadio Id="option3" Name="options" Value="@("Option 3")" Label="Option 3"></FluentRadio>
</FluentRadioGroup>
<br />
<div>
<FluentButton Type="ButtonType.Submit" Appearance="ButtonAppearance.Primary">Submit</FluentButton>
<FluentButton Id="reset-button" Type="ButtonType.Reset">Reset</FluentButton>
</div>
<span id="success-message" hidden>Form submitted successfully!</span>
</form>


@code {
private string? value;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@page "/radiovertical"
<FluentRadioGroup @bind-Value="@fruit" Label="Favorite fruit" Orientation="@Orientation.Vertical">
<FluentRadio Label="Apple"></FluentRadio>
<FluentRadio Label="Banana"></FluentRadio>
<FluentRadio Label="Orange"></FluentRadio>
<FluentRadio Label="Grape"></FluentRadio>
<FluentRadio Label="Kiwi"></FluentRadio>
</FluentRadioGroup>

<p>Favorite fruit: @fruit</p>
@code {
private string fruit = "banana";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
title: RadioGroup and Radio
route: /Radio
---

# RadioGroup and Radio

Radio groups let people select a single item from a short list. Use them in layouts that offer enough space to list up to five options or if it's important to view all options at once.

If there isn't enough space, try a dropdown instead. If you need to let people select more than one option, use checkboxes. To let them immediately turn a setting on or off, try a switch.

## Behavior

Although it is technically possible to show a single Radio button (as can be seen below), it *must* be placed inside a RadioGroup to have any function.

### Default selection
Present a selected option in radio groups by default. The default selection should be placed first and should be the most logical response. Remaining options should be listed in a logical order. For more information, see the Content section.

## Layout
Radio groups can be aligned vertically or horizontally (default). When horizontally aligned, the label can appear next to or under the radio input.

## Accessibility
Include intuitive labels with radio groups.

When tabbing, focus will fall on the first option if no options are selected. If there is a selection, focus will fall on that option first.

## Content
Keep labels short and clear
Keep individual radio labels as concise and descriptive as possible. Use fragments instead of full sentences. If long labels can’t be avoided, text will wrap onto the next line. Never truncate radio text with an ellipsis. Use sentence case with no end punctuation.

Skip the period in radio labels. For the label that introduces the radio group, don’t end with a colon. For more info, go to Periods in the Microsoft Writing Style Guide.

Use sentence style capitalization—only capitalize the first word. For more info, go to Capitalization in the Microsoft Writing Style Guide.

## Examples

### Radio button appearances

A radio button is either unchecked or checked. Usually, once an item in a group has been checked, the result of the group as a whole cannot be unchecked again.
An item can also be disabled and can show a label to indicate the value.

{{ RadioDefault }}

## RadioGroup

Radios are placed and used inside a radio group. Only one of the items in a group can have a checked state.
You can bind to the `Value` of the group to the get the value of the checked item.

{{ RadioGroupDefault }}

## Strongly typed items and using Label template
Radio items allow for strongly binding to types. Because of this, string values need to be defined in the following way:
`Value="@("one")"`

As an alternative to of using the `Label` parameter (string value only),
it is possible to use the `LabelTemplate` parameter to specify a template for the label.
In case both are specified, the `Label` parameter is used.

{{ RadioGroupLabelTemplate }}

## RadioGroup with vertical orientation
When the radio group has a vertical orientation, the items are stacked on top of each other.

{{ RadioGroupVertical }}

## Disabled RadioGroup
A radio group can be disabled as a whole. This means that the user cannot select any of the items in the group.
{{ RadioGroupDisabledGroup }}

## RadioGroup with disabled items
Besides disabling the whole group, it is also possible to disable specific items in a group.
{{ RadioGroupDisabledItems }}

## RadioGroup required
{{ RadioGroupRequired }}

## API FluentRadioGroup
{{ API Type=FluentRadioGroup }}

## API FluentRadio
{{ API Type=FluentRadio }}

## Migrating to v5

{{ INCLUDE File=MigrationFluentRadio }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: Migration FluentRadio and FluentRadioGroup
route: /Migration/Radio
hidden: true
---

### FluentRadio specific changes
- The `FluentRadio` component now inherits from `FluentInputBase` (instead of `FluentComponentBase` before). This means it supports and uses all parameters from `FluentInputBase`, such as `Disabled`, `Required`, `Value`, `Label`, etc.
- Using the `ChildContent` parameter to specify the contents/label of a Radio item is no longer supported. Use the `Label` or `LabelTemplate` parameters instead
- The `ReadOnly` parameter is not supported. Use the `Disabled` parameter instead.



### Removed properties💥
- `ChildContent`, use `Label` or `LabelTemplate`
21 changes: 21 additions & 0 deletions spelling.dic
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,24 @@ tabindex
tablist
tabpanel
textarea
sourcecode
summarydata
currentcolor
menuitem
menuitems
rightclick
menuitemcheckbox
menuitem
menuitemradio
menuitemcheckboxobsolete
menuitemradioobsolete
onmenuitemchange
ontabchange
ondropdownchange
ondialogtoggle
ondialogbeforetoggle
myid
menuclicked
menuchecked
rendertree
testid
28 changes: 28 additions & 0 deletions src/Core/Components/Radio/FluentRadio.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@namespace Microsoft.FluentUI.AspNetCore.Components
@using Microsoft.FluentUI.AspNetCore.Components.Extensions
@using System.Diagnostics
@inherits FluentComponentBase
@typeparam TValue
@{
Debug.Assert(Context != null);
}
<FluentField ForId="@Id"
Class="@ClassValue"
Style="@StyleValue"
Label="@Label"
LabelPosition="@Components.LabelPosition.After"
LabelWidth="@LabelWidth"
IncludeInputSlot="false">
<LabelTemplate>@LabelTemplate</LabelTemplate>
<ChildContent>
<fluent-radio id="@Id"
checked="@(Context.CurrentValue?.Equals(Value) == true ? GetToggledTrueValue() : null)"
disabled="@Disabled"
slot="@FluentSlot.FieldInput"
value="@(Value?.ToString() ?? Label ?? null)"
name="@Context.GroupName"
@onchange="@Context.ChangeEventCallback"
@attributes="@AdditionalAttributes">
</fluent-radio>
</ChildContent>
</FluentField>
106 changes: 106 additions & 0 deletions src/Core/Components/Radio/FluentRadio.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// ------------------------------------------------------------------------
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------------------

using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components.Utilities;

namespace Microsoft.FluentUI.AspNetCore.Components;

/// <summary>
/// A Fluent Radio button component.
/// </summary>
/// <typeparam name="TValue">The type for the value of the radio button</typeparam>
public partial class FluentRadio<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : FluentComponentBase
{
bool _trueValueToggle;

internal FluentRadioContext? Context { get; private set; }

/// <summary />
public FluentRadio()
{
Id = Identifier.NewId();
}

/// <summary>
/// Gets the optional CSS class. If given, this will be included in the class attribute of the component.
/// </summary>
protected virtual string? ClassValue => DefaultClassBuilder.Build();

/// <summary>
/// Gets the optional in-line styles. If given, these will be included in the style attribute of the component.
/// </summary>
protected virtual string? StyleValue => DefaultStyleBuilder
.Build();

/// <summary>
/// Gets or sets the name of the element.
/// Allows access by name from the associated form.
/// ⚠️ This value needs to be set manually for SSR scenarios to work correctly.
/// </summary>
[Parameter]
public virtual string? Name { get; set; }

/// <inheritdoc cref="IFluentField.Disabled" />
[Parameter]
public virtual bool? Disabled { get; set; }

/// <inheritdoc cref="IFluentField.Label" />
[Parameter]
public virtual string? Label { get; set; }

/// <inheritdoc cref="IFluentField.LabelTemplate" />
[Parameter]
public virtual RenderFragment? LabelTemplate { get; set; }

/// <inheritdoc cref="IFluentField.LabelWidth" />
[Parameter]
public virtual string? LabelWidth { get; set; }

/// <summary>
/// Gets or sets the value of the element.
/// </summary>
[Parameter]
public TValue? Value { get; set; }

[CascadingParameter]
private FluentRadioContext? CascadedContext { get; set; }

/// <inheritdoc />
protected override void OnParametersSet()
{
Context = string.IsNullOrEmpty(Name) ? CascadedContext : CascadedContext?.FindContextInAncestors(Name);

if (Context == null)
{
throw new InvalidOperationException($"{GetType()} must have an ancestor {typeof(FluentRadioGroup<TValue>)} " +
$"with a matching 'Name' property, if specified.");
}
}

// This is an unfortunate hack, but is needed for the scenario described by test InputRadioGroupWorksWithMutatingSetter.
// Radio groups are special in that modifying one <input type=radio> instantly and implicitly also modifies the previously
// selected one in the same group. As such, our SetUpdatesAttributeName mechanism isn't sufficient to stay in sync with the
// DOM, because the 'change' event will fire on the new <input type=radio> you just selected, not the previously-selected
// one, and so the previously-selected one doesn't get notified to update its state in the old rendertree. So, if the setter
// reverts the incoming value, the previously-selected one would produce an empty diff (because its .NET value hasn't changed)
// and hence it would be left unselected in the DOM. If you don't understand why this is a problem, try commenting out the
// line that toggles _trueValueToggle and see the E2E test fail.
//
// This hack works around that by causing InputRadio *always* to force its own 'checked' state to be true in the DOM if it's
// true in .NET, whether or not it was true before, by continually changing the value that represents 'true'. This doesn't
// really cause any significant increase in traffic because if we're rendering this InputRadio at all, sending one more small
// attribute value is inconsequential.
//
// Ultimately, a better solution would be to make SetUpdatesAttributeName smarter still so that it knows about the special
// semantics of radio buttons so that, when one <input type="radio"> changes, it treats any previously-selected sibling
// as needing DOM sync as well. That's a more sophisticated change and might not even be useful if the radio buttons
// aren't truly siblings and are in different DOM subtrees (and especially if they were rendered by different components!)
private string GetToggledTrueValue()
{
_trueValueToggle = !_trueValueToggle;
return _trueValueToggle ? "a" : "b";
}
}
Loading