Skip to content

Commit

Permalink
[dev-v5] Add checkbox component (#3223)
Browse files Browse the repository at this point in the history
  • Loading branch information
AClerbois authored Feb 11, 2025
1 parent 48f5434 commit 417dd11
Show file tree
Hide file tree
Showing 22 changed files with 636 additions and 2 deletions.
6 changes: 6 additions & 0 deletions Microsoft.FluentUI-v5.lutconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<LUTConfig Version="1.0">
<Repository />
<ParallelBuilds>true</ParallelBuilds>
<ParallelTestRuns>true</ParallelTestRuns>
<TestCaseTimeout>180000</TestCaseTimeout>
</LUTConfig>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<FluentStack Orientation="Orientation.Vertical">
<FluentCheckbox Shape="@CheckboxShape.Square" Label="Square checked" Checked="true" />
<FluentCheckbox Shape="@CheckboxShape.Circular" Label="Circular checked" Checked="true" />
</FluentStack>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<FluentStack Orientation="Orientation.Vertical">
<FluentCheckbox Margin="0" @bind-Value="@value1" Label="Apples" />
<FluentCheckbox Margin="0" @bind-Value="@value2" Disabled="true" Label="Bananas (disabled)" />
<FluentCheckbox Margin="0" @bind-Value="@value3" Label="Oranges" />
</FluentStack>

@code {
bool value1 = true;
bool value2 = true;
bool value3;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<FluentStack Orientation="Orientation.Vertical">
<FluentCheckbox Shape="@CheckboxShape.Square" Label="Square disabled checked" Disabled="true" Checked="true" />
<FluentCheckbox Shape="@CheckboxShape.Circular" Label="Circular disabled checked" Disabled="true" Checked="true" />

<FluentCheckbox Shape="@CheckboxShape.Square" Label="Square disabled unchecked" Disabled="true" Checked="false" />
<FluentCheckbox Shape="@CheckboxShape.Circular" Label="Circular disabled unchecked" Disabled="true" Checked="false" />
</FluentStack>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<FluentStack Orientation="Orientation.Vertical">
<FluentCheckbox @bind-Value="@value" @bind-CheckState="checkState" ShowIndeterminate="false" Label="Indeterminate with label" />
<FluentLabel>Value is @(value ? "checked" : "unchecked")</FluentLabel>
<FluentLabel>CheckState is @(checkState is null ? "(null indeterminate)" : checkState.Value ? "checked" : "unchecked")</FluentLabel>
</FluentStack>

@code {
private bool value;
private bool? checkState;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<FluentGrid>
<!-- Three state = true -->
<FluentGridItem Xs="4">
<FluentCheckbox @bind-CheckState="state1"
@bind-Value="value1"
ThreeState="true"
Label="Three state = true" />
</FluentGridItem>
<FluentGridItem Xs="8">
Value = @value1 - CheckState is @(state1 is null ? "(null indeterminate)" : state1.Value.ToString())
</FluentGridItem>

<!-- Three state = false -->
<FluentGridItem Xs="4">
<FluentCheckbox @bind-Value="value2"
ThreeState="false"
Label="Three state = false" />
</FluentGridItem>
<FluentGridItem Xs="8">
Value = @value2
</FluentGridItem>

<!-- ShowIndeterminate = false -->
<FluentGridItem Xs="4">
<FluentCheckbox @bind-CheckState="state3"
@bind-Value="value3"
ThreeState="true"
ShowIndeterminate="false"
Label="ShowIndeterminate = false" />
</FluentGridItem>
<FluentGridItem Xs="8">
Value = @value3 - CheckState is @(state3 is null ? "(null indeterminate)" : state3.Value.ToString())
</FluentGridItem>

</FluentGrid>

@code {
bool value1, value2, value3;
bool? state1 = false, state3 = null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: Checkbox
route: /Checkbox
---

# Checkbox

A **FluentCheckbox** component enables a user to select or deselect an option.
It's typically used to capture a boolean value.

{{ CheckboxDefault }}

## Appearance

The apparent style of a checkbox can be changed by setting the `Shape` property, but also by setting the `Size` property.

You can also add a label to the checkbox by setting the `Label` property.
The label will be automatically positioned next to the checkbox.

We recommend using a spacing of 24px between checkboxes and other components.

{{ CheckboxAppearances }}


### Indeterminate

To define the indeterminate state, you need to use the CheckState bindable property,
which has three possible values: null, true and false.

For the majority of uses, a checkbox with two values (checked/unchecked) is probably sufficient.
In this case, the value bindable property is used.
Value has only two possible values: true and false.

A ShowIndeterminate=‘true’ attribute allows you to indicate that the user cannot display this "Indeterminate"
state himself. This allows you to place the box in the indeterminate state when the page is first displayed, but
without being able to return to it afterwards (except by code).

{{ CheckboxIndeterminate }}

## Three-State Checkbox

The `FluentCheckbox` component supports a three-state mode, which allows the checkbox to have an additional indeterminate state. This can be useful for scenarios where a checkbox represents a mixed or partial selection.

To enable the three-state mode, set the `ThreeState` property to `true`. You can also control the order of the states using the `ThreeStateOrderUncheckToIntermediate` property.

- `ThreeState`: Enables the three-state mode.
- `ThreeStateOrderUncheckToIntermediate`: Controls the order of the states. If set to `true`, the order will be Unchecked -> Intermediate -> Checked. If set to `false` (default), the order will be Unchecked -> Checked -> Intermediate.

{{ CheckboxThreeStates }}

## API FluentCheckbox

{{ API Type=FluentCheckbox }}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
Expand Down
22 changes: 22 additions & 0 deletions src/Core/Components/Checkbox/FluentCheckbox.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@namespace Microsoft.FluentUI.AspNetCore.Components
@using Microsoft.FluentUI.AspNetCore.Components.Extensions
@inherits FluentInputBase<bool>

<FluentField InputComponent="@this" ForId="@Id" Class="@ClassValue" Style="@StyleValue">
<fluent-checkbox @ref="@Element"
aria-label="@AriaLabel"
autofocus="@Autofocus"
checked="@(_checked ? "true" : "false")"
disabled="@Disabled"
indeterminate="@(_indeterminate ? "true" : "false")"
id="@Id"
name="@Name"
readonly="@ReadOnly"
required="@Required"
shape="@Shape.ToAttributeValue()"
size="@Size.ToAttributeValue()"
@onfocusout="@FocusOutHandlerAsync"
@attributes="AdditionalAttributes"
@onchange="@OnCheckChangedHandlerAsync"
slot="input" />
</FluentField>
224 changes: 224 additions & 0 deletions src/Core/Components/Checkbox/FluentCheckbox.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// ------------------------------------------------------------------------
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------------------

using System.Diagnostics.CodeAnalysis;
using System.Reflection.Metadata;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;

namespace Microsoft.FluentUI.AspNetCore.Components;

/// <summary>
/// The FluentCheckbox component is used to render a checkbox input
/// </summary>
public partial class FluentCheckbox : FluentInputBase<bool>, IFluentComponentElementBase
{
/// <summary>
///
/// </summary>
public FluentCheckbox()
{
LabelPosition = Components.LabelPosition.After;
}

/// <inheritdoc cref="IFluentComponentElementBase.Element" />
[Parameter]
public ElementReference Element { get; set; }

/// <summary>
/// Gets or sets the state of the CheckBox: true, false or null.
/// Useful when the mode ThreeState is enable
/// </summary>
[Parameter]
public bool? CheckState { get; set; }

/// <summary>
/// Gets or sets the shape of the checkbox
/// </summary>
[Parameter]
public CheckboxShape Shape { get; set; } = CheckboxShape.Square;

/// <summary>
/// Gets or sets the size of the checkbox. See <see cref="CheckboxSize"/>
/// </summary>
[Parameter]
public CheckboxSize Size { get; set; } = CheckboxSize.Medium;

/// <summary>
/// Gets or sets a value indicating whether the user can display the indeterminate state by clicking the CheckBox.
/// </summary>
/// <remarks>If this is not the case, the checkbox can be started in the indeterminate state, but the user cannot activate it with the mouse.</remarks>
/// <value>true</value>
[Parameter]
public bool ShowIndeterminate { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether the CheckBox will allow three check states rather than two.
/// </summary>
[Parameter]
public bool ThreeState { get; set; }

/// <summary>
/// Gets or sets a value indicating the order of the three states of the CheckBox.
/// False(by default), the order is Unchecked -> Checked -> Intermediate.
/// True: the order is Unchecked -> Intermediate -> Checked.
/// </summary>
[Parameter]
public bool ThreeStateOrderUncheckToIntermediate { get; set; }

/// <summary>
/// Action to be called when the CheckBox state changes.
/// </summary>
[Parameter]
public EventCallback<bool?> CheckStateChanged { get; set; }

/// <summary>
/// Handler for the OnFocus event.
/// </summary>
/// <param name="e"></param>
/// <returns></returns>
protected virtual Task FocusOutHandlerAsync(FocusEventArgs e)
{
FocusLost = true;
return Task.CompletedTask;
}

/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();

if (ThreeState && CheckState.HasValue)
{
await SetValueChangedAsync(CheckState.Value);
}
}

/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Utilities.Attributes.observeAttributeChange", Element, "checked", "boolean");
await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Utilities.Attributes.observeAttributeChange", Element, "indeterminate", "boolean", "", true);
}
}

private bool _checked => CheckState ?? Value;

private bool _indeterminate => ThreeState
? !CheckState.HasValue
: !ShowIndeterminate && !CheckState.HasValue;

private async Task SetValueChangedAsync(bool newValue)
{
if (Value == newValue)
{
return;
}

Value = newValue;

if (ValueChanged.HasDelegate)
{
await ValueChanged.InvokeAsync(newValue);
}
}

private async Task SetCheckStateChangedAsync(bool? newValue)
{
CheckState = newValue;

await SetValueChangedAsync(newValue ?? false);

if (CheckStateChanged.HasDelegate)
{
await CheckStateChanged.InvokeAsync(newValue);
}
}

private async Task OnCheckChangedHandlerAsync(ChangeEventArgs e)
{
ArgumentNullException.ThrowIfNull(e);

if (ThreeState)
{
if (_checked)
{
// Current Check
if (ThreeStateOrderUncheckToIntermediate)
{
await SetToUncheckedAsync();
}
else
{
await SetToIndeterminateAsync();
}
}
else if (_indeterminate)
{
// Current _indeterminate
if (ThreeStateOrderUncheckToIntermediate)
{
await SetToCheckedAsync();
}
else
{
await SetToUncheckedAsync();
}
}
else
{
// Current Uncheck
if (ThreeStateOrderUncheckToIntermediate && ShowIndeterminate)
{
await SetToIndeterminateAsync();
}
else
{
await SetToCheckedAsync();
}
}
}
else
{
await SetCheckStateChangedAsync(!_checked);
}
}

private async Task SetToIndeterminateAsync()
{
await SetCheckStateChangedAsync(ShowIndeterminate ? null : false);
}

private async Task SetToCheckedAsync()
{
await SetCheckStateChangedAsync(true);
}

private async Task SetToUncheckedAsync()
{
await SetCheckStateChangedAsync(newValue: false);
}

/// <summary>
/// Parses a string to create the <see cref="Microsoft.AspNetCore.Components.Forms.InputBase{TValue}.Value"/>.
/// </summary>
/// <param name="value">The string value to be parsed.</param>
/// <param name="result">The result to inject into the Value.</param>
/// <param name="validationErrorMessage">If the value could not be parsed, provides a validation error message.</param>
/// <returns>True if the value could be parsed; otherwise false.</returns>
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out bool result, [NotNullWhen(false)] out string? validationErrorMessage)
{
// Overriding mandatory because the parent method is abstract and called via the OnChanged.
// However, this method is not used in this component because we need to manage the CheckState.
throw new NotSupportedException();
}

internal bool InternalTryParseValueFromString(string? value, [MaybeNullWhen(false)] out bool result, [NotNullWhen(false)] out string? validationErrorMessage)
{
return TryParseValueFromString(value, out result, out validationErrorMessage);
}
}
Loading

0 comments on commit 417dd11

Please sign in to comment.