Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
951e505
apply BitChoiceGroup improvements #12740
msynk Jul 21, 2026
904f917
resolve review comments
msynk Jul 21, 2026
4dba0c3
resolve local review findings
msynk Jul 26, 2026
fa690a8
fix local review findings
msynk Jul 26, 2026
cfd9d9d
resolve review comments 2
msynk Jul 27, 2026
6ab55aa
resolve review comments 2
msynk Jul 27, 2026
8301b60
resolve review comments 3
msynk Jul 28, 2026
eddc6e1
Merge branch 'develop' of https://github.com/bitfoundation/bitplatfor…
msynk Jul 28, 2026
f5a659f
resolve review comments 4
msynk Jul 28, 2026
9c06035
resolve review comments 5
msynk Jul 28, 2026
82c8d5a
Merge branch 'develop' into 12740-blazorui-choicegroup-improvements
msynk Jul 28, 2026
91812ac
resolve review comments 6
msynk Jul 28, 2026
c209bb5
fix tests
msynk Jul 28, 2026
6f2c88a
resolve review comments 7
msynk Jul 29, 2026
df9854b
fix tests
msynk Jul 29, 2026
1383dc6
resolve review comments 8
msynk Jul 29, 2026
fe79f40
resolve review comments 9
msynk Jul 29, 2026
89e723b
resolve review comments 9
msynk Jul 29, 2026
4788e0f
resolve review comments 10
msynk Jul 29, 2026
83d68ca
resolve review comments 11
msynk Jul 29, 2026
74545ec
resolve review comments 12
msynk Jul 29, 2026
666963b
resolve review comments 13
msynk Aug 3, 2026
6f179af
Merge branch 'develop' of https://github.com/bitfoundation/bitplatfor…
msynk Aug 3, 2026
2af8c54
fix siderail
msynk Aug 3, 2026
be31a28
resolve local review findings
msynk Aug 3, 2026
70f66fa
resolve local demo review findings
msynk Aug 4, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
class="@ClassBuilder.Value"
dir="@Dir?.ToString().ToLower()"
aria-label="@AriaLabel"
aria-labelledby="@GetAriaLabelledBy()">
aria-labelledby="@GetAriaLabelledBy()"
aria-required="@(Required ? "true" : null)"
aria-readonly="@(ReadOnly ? "true" : null)"
aria-disabled="@(IsEnabled ? null : "true")"
aria-orientation="@(Horizontal ? "horizontal" : "vertical")">
Comment thread
msynk marked this conversation as resolved.

<label id="@_labelId"
disabled="@(IsEnabled is false)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Bit.BlazorUI;
{
private List<TItem> _items = [];
private string _name = default!;
private bool _autoFocusDone;
private string _labelId = default!;
private bool _optionsOrderDirty;
private string _optionsContainerId = default!;
Expand All @@ -26,6 +27,11 @@ namespace Bit.BlazorUI;
/// </summary>
[Parameter] public string? AriaLabelledBy { get; set; }

/// <summary>
/// Determines if the ChoiceGroup is auto focused on first render, focusing its checked item (or its first item).
/// </summary>
[Parameter] public bool AutoFocus { get; set; }

/// <summary>
/// Keeps the assigned Index of each option in sync with the markup order of the options, even when
/// an option is added, removed, or reordered conditionally after the first render (an option that
Expand All @@ -52,6 +58,12 @@ namespace Bit.BlazorUI;
[Parameter, ResetClassBuilder]
public BitColor? Color { get; set; }

/// <summary>
/// The gap between the items of the ChoiceGroup.
/// </summary>
[Parameter, ResetStyleBuilder]
public string? Gap { get; set; }

/// <summary>
/// Renders the items in the ChoiceGroup horizontally.
/// </summary>
Expand Down Expand Up @@ -313,6 +325,8 @@ protected override void RegisterCssClasses()
protected override void RegisterCssStyles()
{
StyleBuilder.Register(() => Styles?.Root);

StyleBuilder.Register(() => Gap.HasValue() ? $"--bit-chg-item-gap:{Gap}" : string.Empty);
}

protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
Expand Down Expand Up @@ -357,6 +371,30 @@ private void SetIndexItems()

internal string? GetInputId(TItem item) => GetId(item) ?? $"ChoiceGroup-{UniqueId}-Input-{GetValue(item)}";

internal string GetDescriptionId(TItem item) => $"{GetInputId(item)}-description";

// Keeps the InputElement of the base class pointing at the input of the checked item (or the first
// item when nothing is checked), so FocusAsync targets the same input the Tab key would land on.
// Called by each item after render; also performs the one-time AutoFocus on that input.
internal async Task SetInputElement(TItem item, ElementReference inputElement)
{
var isChecked = GetIsCheckedItem(item);

if (isChecked is false)
{
if (_items.Any(GetIsCheckedItem)) return;
if (ReferenceEquals(_items.FirstOrDefault(), item) is false) return;
}

InputElement = inputElement;

if (AutoFocus && _autoFocusDone is false && IsEnabled && ReadOnly is false && GetIsEnabled(item))
{
_autoFocusDone = true;
await InputElement.FocusAsync();
}
}
Comment thread
msynk marked this conversation as resolved.

Comment thread
msynk marked this conversation as resolved.
Outdated
internal async Task HandleClick(TItem item)
{
if (IsEnabled is false || ReadOnly || GetIsEnabled(item) is false) return;
Expand Down Expand Up @@ -680,6 +718,28 @@ internal BitImageSize GetImageSize(TItem item)
return item.GetValueFromProperty<RenderFragment<TItem>?>(NameSelectors.Template.Name);
}

internal string? GetDescription(TItem item)
{
if (item is BitChoiceGroupItem<TValue> choiceGroupItem)
{
return choiceGroupItem.Description;
}

if (item is BitChoiceGroupOption<TValue> choiceGroupOption)
{
return choiceGroupOption.Description;
}

if (NameSelectors is null) return null;

if (NameSelectors.Description.Selector is not null)
{
return NameSelectors.Description.Selector!(item);
}

return item.GetValueFromProperty<string?>(NameSelectors.Description.Name);
}

internal string? GetText(TItem item)
{
if (item is BitChoiceGroupItem<TValue> choiceGroupItem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,27 @@
}
}

// The input is visually hidden but stays focusable, so the browser provides the full native radio
// group keyboard interaction (a single tab stop landing on the checked item, arrow keys moving the
// selection with wrapping and RTL awareness, and Space checking the focused item).
.bit-chg-inp {
margin: 0;
padding: 0;
opacity: 0;
width: 1px;
height: 1px;
border: none;
overflow: hidden;
position: absolute;
pointer-events: none;
clip-path: inset(50%);
}

.bit-chg-inp:focus-visible + .bit-chg-itl {
outline-offset: spacing(0.25);
outline: spacing(0.25) solid var(--bit-chg-clr);
}

.bit-chg-itl {
display: flex;
cursor: pointer;
Expand All @@ -140,6 +161,17 @@
flex-direction: var(--bit-chg-flex-direction);
}

.bit-chg-txd {
display: flex;
flex-direction: column;
}

.bit-chg-dsc {
display: block;
color: $clr-fg-sec;
font-size: calc(var(--bit-chg-fontsize) - #{spacing(0.25)});
}

.bit-chg-ili {
margin: 0;
display: flex;
Expand Down Expand Up @@ -339,6 +371,10 @@
.bit-chg-ico {
color: $clr-fg-dis;
}

.bit-chg-dsc {
color: $clr-fg-dis;
}
}

.bit-chg-pri {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,9 @@ public class BitChoiceGroupClassStyles
/// Custom CSS classes/styles for the text of each item of the BitChoiceGroup.
/// </summary>
public string? ItemText { get; set; }

/// <summary>
/// Custom CSS classes/styles for the description of each item of the BitChoiceGroup.
/// </summary>
public string? ItemDescription { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ public class BitChoiceGroupItem<TValue>
/// </summary>
public string? Class { get; set; }

/// <summary>
/// The secondary text to show under the text of the BitChoiceGroup item.
/// </summary>
public string? Description { get; set; }

/// <summary>
/// Id attribute of the BitChoiceGroup item.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ public class BitChoiceGroupNameSelectors<TItem, TValue>
/// </summary>
public BitNameSelectorPair<TItem, string?> Class { get; set; } = new(nameof(BitChoiceGroupItem<TValue>.Class));

/// <summary>
/// The Description field name and selector of the custom input class.
/// </summary>
public BitNameSelectorPair<TItem, string?> Description { get; set; } = new(nameof(BitChoiceGroupItem<TValue>.Description));

/// <summary>
/// The Id field name and selector of the custom input class.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ public partial class BitChoiceGroupOption<TValue> : ComponentBase, IDisposable
/// </summary>
[Parameter] public string? Class { get; set; }

/// <summary>
/// The secondary text to show under the text of the BitChoiceGroup option.
/// </summary>
[Parameter] public string? Description { get; set; }

/// <summary>
/// Id attribute of the BitChoiceGroup option.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,27 @@
var inputId = ChoiceGroup.GetInputId(Item);
var template = ChoiceGroup.GetTemplate(Item);
var isChecked = ChoiceGroup.GetIsCheckedItem(Item);
var description = ChoiceGroup.GetDescription(Item);
var descriptionId = ChoiceGroup.GetDescriptionId(Item);
}

<div style="@ChoiceGroup.GetItemContainerCssStyles(Item)" class="@ChoiceGroup.GetItemContainerCssClasses(Item)" @attributes="ChoiceGroup.GetItemMarkerAttributes(Item)">
<input @ref="_inputElement" @attributes="ChoiceGroup.InputHtmlAttributes"
@onclick="e => ChoiceGroup.HandleClick(Item)"
@onclick:preventDefault="ChoiceGroup.ReadOnly"
@onchange="e => ChoiceGroup.HandleChange(Item)"
type="radio"
id="@inputId"
checked=@isChecked
class="bit-chg-inp"
name="@ChoiceGroup.GetName()"
required="@ChoiceGroup.Required"
tabindex="@(ChoiceGroup.ReadOnly ? "-1" : null)"
aria-label="@ChoiceGroup.GetAriaLabel(Item)"
aria-describedby="@(description.HasValue() ? descriptionId : null)"
value="@ChoiceGroup.GetValue(Item)?.ToString()"
disabled="@(ChoiceGroup.GetIsEnabled(Item) is false)" />

<label for="@inputId" style="@ChoiceGroup.Styles?.ItemLabel" class="bit-chg-itl @ChoiceGroup.GetItemLabelCssClasses(Item) @ChoiceGroup.Classes?.ItemLabel">
@if (template is not null)
{
Expand Down Expand Up @@ -71,6 +89,17 @@
{
<div style="@ChoiceGroup.Styles?.ItemTextWrapper" class="bit-chg-itw @ChoiceGroup.Classes?.ItemTextWrapper">
<span style="@ChoiceGroup.Styles?.ItemText" class="bit-chg-itx @ChoiceGroup.Classes?.ItemText">@ChoiceGroup.GetText(Item)</span>
@if (description.HasValue())
{
<span id="@descriptionId" style="@ChoiceGroup.Styles?.ItemDescription" class="bit-chg-dsc @ChoiceGroup.Classes?.ItemDescription">@description</span>
}
</div>
}
else if (description.HasValue())
{
<div class="bit-chg-txd">
<span style="@ChoiceGroup.Styles?.ItemText" class="bit-chg-itx @ChoiceGroup.Classes?.ItemText">@ChoiceGroup.GetText(Item)</span>
<span id="@descriptionId" style="@ChoiceGroup.Styles?.ItemDescription" class="bit-chg-dsc @ChoiceGroup.Classes?.ItemDescription">@description</span>
</div>
}
else
Expand All @@ -80,17 +109,4 @@
}
}
</label>

<input @attributes="ChoiceGroup.InputHtmlAttributes"
@onclick="e => ChoiceGroup.HandleClick(Item)"
@onchange="e => ChoiceGroup.HandleChange(Item)"
hidden
type="radio"
name="@ChoiceGroup.GetName()"
id="@inputId"
checked=@isChecked
required="@ChoiceGroup.Required"
aria-label="@ChoiceGroup.GetAriaLabel(Item)"
value="@ChoiceGroup.GetValue(Item)?.ToString()"
disabled="@(ChoiceGroup.GetIsEnabled(Item) is false)" />
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
namespace Bit.BlazorUI;
namespace Bit.BlazorUI;

public partial class _BitChoiceGroupItem<TItem, TValue> : ComponentBase where TItem : class, new ()
{
private ElementReference _inputElement;

[Parameter] public TItem Item { get; set; } = default!;

[Parameter] public BitChoiceGroup<TItem, TValue> ChoiceGroup { get; set; } = default!;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);

await ChoiceGroup.SetInputElement(Item, _inputElement);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

<PageOutlet Url="components/choicegroup"
Title="ChoiceGroup"
Description="ChoiceGroup lets people select a single option from two or more choices." />
Description="ChoiceGroup, also known as Radio or RadioGroup, lets people select a single value from two or more mutually exclusive choices, using fully keyboard accessible native radio inputs with support for icons, images, descriptions, and custom templates." />

<div>
<DemoPage Name="ChoiceGroup"
SecondaryNames="@(["Radio", "RadioButton", "RadioGroup", "RadioButtonGroup"])"
Description="ChoiceGroup lets people select a single option from two or more choices."
Description="ChoiceGroup, also known as Radio or RadioGroup, lets people select a single value from two or more mutually exclusive choices. It renders fully keyboard accessible native radio inputs and supports icons, images, prefixes, per-item descriptions, and custom templates through three different item APIs."
Parameters="componentParameters"
SubEnums="componentSubEnums"
SubClasses="componentSubClasses"
Expand Down
Loading
Loading