diff --git a/Microsoft.FluentUI-v5.lutconfig b/Microsoft.FluentUI-v5.lutconfig new file mode 100644 index 000000000..596a86030 --- /dev/null +++ b/Microsoft.FluentUI-v5.lutconfig @@ -0,0 +1,6 @@ + + + true + true + 180000 + \ No newline at end of file diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxAppearances.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxAppearances.razor new file mode 100644 index 000000000..f9eba35eb --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxAppearances.razor @@ -0,0 +1,4 @@ + + + + diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxDefault.razor new file mode 100644 index 000000000..35e5ee612 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxDefault.razor @@ -0,0 +1,11 @@ + + + + + + +@code { + bool value1 = true; + bool value2 = true; + bool value3; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxDisabled.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxDisabled.razor new file mode 100644 index 000000000..c6b39f2c2 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxDisabled.razor @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxIndeterminate.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxIndeterminate.razor new file mode 100644 index 000000000..e4f4294a9 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxIndeterminate.razor @@ -0,0 +1,10 @@ + + + Value is @(value ? "checked" : "unchecked") + CheckState is @(checkState is null ? "(null indeterminate)" : checkState.Value ? "checked" : "unchecked") + + +@code { + private bool value; + private bool? checkState; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxThreeStates.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxThreeStates.razor new file mode 100644 index 000000000..352c9296f --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/Examples/CheckboxThreeStates.razor @@ -0,0 +1,40 @@ + + + + + + + Value = @value1 - CheckState is @(state1 is null ? "(null indeterminate)" : state1.Value.ToString()) + + + + + + + + Value = @value2 + + + + + + + + Value = @value3 - CheckState is @(state3 is null ? "(null indeterminate)" : state3.Value.ToString()) + + + + +@code { + bool value1, value2, value3; + bool? state1 = false, state3 = null; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/FluentCheckbox.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/FluentCheckbox.md new file mode 100644 index 000000000..b6f834558 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Checkbox/FluentCheckbox.md @@ -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 }} diff --git a/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj b/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj index aaacc6153..e5ffad94f 100644 --- a/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj +++ b/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj @@ -1,4 +1,4 @@ - + net9.0 diff --git a/src/Core/Components/Checkbox/FluentCheckbox.razor b/src/Core/Components/Checkbox/FluentCheckbox.razor new file mode 100644 index 000000000..e8f2dc6b1 --- /dev/null +++ b/src/Core/Components/Checkbox/FluentCheckbox.razor @@ -0,0 +1,22 @@ +@namespace Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.FluentUI.AspNetCore.Components.Extensions +@inherits FluentInputBase + + + + diff --git a/src/Core/Components/Checkbox/FluentCheckbox.razor.cs b/src/Core/Components/Checkbox/FluentCheckbox.razor.cs new file mode 100644 index 000000000..4056fb103 --- /dev/null +++ b/src/Core/Components/Checkbox/FluentCheckbox.razor.cs @@ -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; + +/// +/// The FluentCheckbox component is used to render a checkbox input +/// +public partial class FluentCheckbox : FluentInputBase, IFluentComponentElementBase +{ + /// + /// + /// + public FluentCheckbox() + { + LabelPosition = Components.LabelPosition.After; + } + + /// + [Parameter] + public ElementReference Element { get; set; } + + /// + /// Gets or sets the state of the CheckBox: true, false or null. + /// Useful when the mode ThreeState is enable + /// + [Parameter] + public bool? CheckState { get; set; } + + /// + /// Gets or sets the shape of the checkbox + /// + [Parameter] + public CheckboxShape Shape { get; set; } = CheckboxShape.Square; + + /// + /// Gets or sets the size of the checkbox. See + /// + [Parameter] + public CheckboxSize Size { get; set; } = CheckboxSize.Medium; + + /// + /// Gets or sets a value indicating whether the user can display the indeterminate state by clicking the CheckBox. + /// + /// If this is not the case, the checkbox can be started in the indeterminate state, but the user cannot activate it with the mouse. + /// true + [Parameter] + public bool ShowIndeterminate { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the CheckBox will allow three check states rather than two. + /// + [Parameter] + public bool ThreeState { get; set; } + + /// + /// 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. + /// + [Parameter] + public bool ThreeStateOrderUncheckToIntermediate { get; set; } + + /// + /// Action to be called when the CheckBox state changes. + /// + [Parameter] + public EventCallback CheckStateChanged { get; set; } + + /// + /// Handler for the OnFocus event. + /// + /// + /// + protected virtual Task FocusOutHandlerAsync(FocusEventArgs e) + { + FocusLost = true; + return Task.CompletedTask; + } + + /// + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + if (ThreeState && CheckState.HasValue) + { + await SetValueChangedAsync(CheckState.Value); + } + } + + /// + 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); + } + + /// + /// Parses a string to create the . + /// + /// The string value to be parsed. + /// The result to inject into the Value. + /// If the value could not be parsed, provides a validation error message. + /// True if the value could be parsed; otherwise false. + 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); + } +} diff --git a/src/Core/Enums/CheckboxShape.cs b/src/Core/Enums/CheckboxShape.cs new file mode 100644 index 000000000..aed04084f --- /dev/null +++ b/src/Core/Enums/CheckboxShape.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using System.ComponentModel; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// The visual appearance of the . +/// +public enum CheckboxShape +{ + /// + /// The default appearance. The border is square. + /// + [Description("square")] + Square, + + /// + /// The appearance where the border is circular. + /// + [Description("circular")] + Circular, +} diff --git a/src/Core/Enums/CheckboxSize.cs b/src/Core/Enums/CheckboxSize.cs new file mode 100644 index 000000000..71af64b5e --- /dev/null +++ b/src/Core/Enums/CheckboxSize.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using System.ComponentModel; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Indicates the size of the . +/// +public enum CheckboxSize +{ + /// + /// Medium size. + /// + [Description("medium")] + Medium, + + /// + /// Large size. + /// + [Description("large")] + Large, +} diff --git a/tests/Core/Components.Tests.csproj b/tests/Core/Components.Tests.csproj index 0f0354a9b..a75115bb4 100644 --- a/tests/Core/Components.Tests.csproj +++ b/tests/Core/Components.Tests.csproj @@ -1,4 +1,4 @@ - + net9.0 diff --git a/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Default.verified.razor.html b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Default.verified.razor.html new file mode 100644 index 000000000..f6f8a4f21 --- /dev/null +++ b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Default.verified.razor.html @@ -0,0 +1,4 @@ + + + + diff --git a/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_EnableThreeState.verified.razor.html b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_EnableThreeState.verified.razor.html new file mode 100644 index 000000000..856f7036b --- /dev/null +++ b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_EnableThreeState.verified.razor.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_LabelTemplate.verified.razor.html b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_LabelTemplate.verified.razor.html new file mode 100644 index 000000000..0df3aabd0 --- /dev/null +++ b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_LabelTemplate.verified.razor.html @@ -0,0 +1,7 @@ + + + + + diff --git a/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Shape-circular.verified.razor.html b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Shape-circular.verified.razor.html new file mode 100644 index 000000000..f6330a921 --- /dev/null +++ b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Shape-circular.verified.razor.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Shape-square.verified.razor.html b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Shape-square.verified.razor.html new file mode 100644 index 000000000..567c2e958 --- /dev/null +++ b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Shape-square.verified.razor.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_ShowIndeterminate.verified.razor.html b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_ShowIndeterminate.verified.razor.html new file mode 100644 index 000000000..856f7036b --- /dev/null +++ b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_ShowIndeterminate.verified.razor.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Size-large.verified.razor.html b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Size-large.verified.razor.html new file mode 100644 index 000000000..054d85bc4 --- /dev/null +++ b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Size-large.verified.razor.html @@ -0,0 +1,4 @@ + + + + diff --git a/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Size-medium.verified.razor.html b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Size-medium.verified.razor.html new file mode 100644 index 000000000..136af48fc --- /dev/null +++ b/tests/Core/Components/Checkbox/FluentCheckboxTests.FluentCheckbox_Size-medium.verified.razor.html @@ -0,0 +1,4 @@ + + + + diff --git a/tests/Core/Components/Checkbox/FluentCheckboxTests.razor b/tests/Core/Components/Checkbox/FluentCheckboxTests.razor new file mode 100644 index 000000000..d1f5b495e --- /dev/null +++ b/tests/Core/Components/Checkbox/FluentCheckboxTests.razor @@ -0,0 +1,168 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Extensions +@using Microsoft.FluentUI.AspNetCore.Components.Tests.Extensions +@using Microsoft.FluentUI.AspNetCore.Components.Utilities +@using System.ComponentModel.DataAnnotations +@using Xunit; +@inherits TestContext + +@code +{ + public FluentCheckboxTests() + { + JSInterop.Mode = JSRuntimeMode.Loose; + Services.AddFluentUIComponents(); + } + + [Fact] + public void FluentCheckbox_Default() + { + // Arrange && Act + var cut = Render(@ + ); + // Assert + cut.Verify(); + } + + [Theory] + [InlineData(CheckboxShape.Circular, "circular")] + [InlineData(CheckboxShape.Square, "square")] + public void FluentCheckbox_Shape(CheckboxShape checkboxShape, string expectedAttribute) + { + // Arrange && Act + var cut = Render(@); + + // Assert + cut.Verify(suffix: expectedAttribute); + } + + [Theory] + [InlineData(CheckboxSize.Medium, "medium")] + [InlineData(CheckboxSize.Large, "large")] + public void FluentCheckbox_Size(CheckboxSize checkboxSize, string expectedAttribute) + { + // Arrange && Act + var cut = Render(@); + + // Assert + cut.Verify(suffix: expectedAttribute); + } + + [Fact] + public void FluentCheckbox_EnableThreeState() + { + // Arrange && Act + var cut = Render(@); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentCheckbox_TryParseValueFromString() + { + // Arrange + var fluentCheckbox = new FluentCheckbox(); + + // Act & Assert + Assert.Throws(() => fluentCheckbox.InternalTryParseValueFromString(string.Empty, out var parsedValue, out var validationErrorMessage)); + } + + [Fact] + public void FluentCheckbox_ShowIndeterminate() + { + // Arrange && Act + var cut = Render(@); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentCheckbox_LabelTemplate() + { + // Arrange && Act + var cut = Render( + @ + ); + + // Assert + cut.Verify(); + } + + [Theory] + [InlineData(true, true)] + [InlineData(false, false)] + public void FluentCheckbox_InitialValue_WithCheckState(bool? initialValue, bool expectedValue) + { + // Arrange && Act + bool value = default; + bool? checkState = initialValue; + var cut = Render( + @ + ); + + // Assert + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(false, true)] + [InlineData(true, false)] + public void FluentCheckbox_OnClick(bool initialValue, bool expectedValue) + { + // Arrange + var value = initialValue; + var cut = Render(@); + + // Act + cut.Find("fluent-checkbox").Change(""); + + // Assert + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(false, false, true)] + [InlineData(false, true, null)] + [InlineData(false, null, false)] + [InlineData(true, false, null)] + [InlineData(true, null, true)] + [InlineData(true, true, false)] + public void FluentCheckbox_ThreeStateOnClick(bool threeStateOrderUncheckToIntermediate, bool? initialValue, bool? expectedValue) + { + // Arrange + var value = initialValue; + var cut = Render(@); + var findCut = cut.Find("fluent-checkbox"); + + // Act + findCut.Change(""); + + // Assert + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, null, false)] + [InlineData(true, false, true)] + [InlineData(true, null, true)] + [InlineData(true, true, false)] + public void FluentCheckbox_ThreeStateWithoutIndeterminateStateOnClick(bool threeStateOrderUncheckToIntermediate, bool? initialValue, bool? expectedValue) + { + // Arrange + var value = initialValue; + var cut = Render(@); + var findCut = cut.Find("fluent-checkbox"); + + // Act + findCut.Change(""); + + // Assert + Assert.Equal(expectedValue, value); + } +}