-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(components): add checkbox component (#21)
* feat(components): add the base class for inputs * feat(components): add basic checkbox implementation * feat(components): add `Radius`, `Colors` and `Size` parameters to the input base * feat(components): customize checkbox appearance * feat(components): allow passing custom check icon to the checkbox component * feat(components): add slots to the checkbox component * feat(components): add `ReadOnly` parameter to the input base component * feat(components): add keyboard navigation and focus ring * test(components): add tests for the checkbox component
- Loading branch information
1 parent
76b2eb1
commit 8cbc234
Showing
13 changed files
with
795 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
// Copyright (c) LumexUI 2024 | ||
// LumexUI licenses this file to you under the MIT license | ||
// See the license here https://github.com/LumexUI/lumexui/blob/main/LICENSE | ||
|
||
// Some of the code was taken from | ||
// https://github.com/dotnet/aspnetcore/blob/main/src/Components/Web/src/Forms/InputBase.cs | ||
|
||
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq.Expressions; | ||
|
||
using LumexUI.Common; | ||
|
||
using Microsoft.AspNetCore.Components; | ||
|
||
namespace LumexUI; | ||
|
||
public abstract class LumexInputBase<TValue> : LumexComponentBase | ||
{ | ||
/// <summary> | ||
/// Gets or sets a value indicating whether the input is disabled. | ||
/// </summary> | ||
[Parameter] public bool Disabled { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets a value indicating whether the input is read-only. | ||
/// </summary> | ||
[Parameter] public bool ReadOnly { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets a color of the input. | ||
/// </summary> | ||
/// <remarks> | ||
/// The default is <see cref="ThemeColor.Default"/> | ||
/// </remarks> | ||
[Parameter] public ThemeColor Color { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the border radius of the input. | ||
/// </summary> | ||
/// <remarks> | ||
/// The default is <see cref="Radius.Medium"/> | ||
/// </remarks> | ||
[Parameter] public Radius Radius { get; set; } = Radius.Medium; | ||
|
||
/// <summary> | ||
/// Gets or sets the size of the input. | ||
/// </summary> | ||
/// <remarks> | ||
/// Default value is <see cref="Size.Medium"/> | ||
/// </remarks> | ||
[Parameter] public Size Size { get; set; } = Size.Medium; | ||
|
||
/// <summary> | ||
/// Gets or sets the value of the input. This should be used with two-way binding. | ||
/// </summary> | ||
[Parameter] public TValue? Value { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets a callback that updates the bound value. | ||
/// </summary> | ||
[Parameter] public EventCallback<TValue> ValueChanged { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets an expression that identifies the bound value. | ||
/// </summary> | ||
[Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the associated <see cref="ElementReference"/>. | ||
/// </summary> | ||
public ElementReference Element { get; protected set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the current value of the input. | ||
/// </summary> | ||
protected TValue? CurrentValue | ||
{ | ||
get => Value; | ||
set => _ = SetCurrentValueAsync( value ); | ||
} | ||
|
||
/// <summary> | ||
/// Gets or sets the current value of the input, represented as a string. | ||
/// </summary> | ||
protected string? CurrentValueAsString | ||
{ | ||
get => _parsingFailed ? _incomingValueBeforeParsing : FormatValueAsString( CurrentValue ); | ||
set => _ = SetCurrentValueAsStringAsync( value ); | ||
} | ||
|
||
private bool _parsingFailed; | ||
private bool _hasInitializedParameters; | ||
private string? _incomingValueBeforeParsing; | ||
private Type? _nullableUnderlyingType; | ||
|
||
/// <inheritdoc /> | ||
public override Task SetParametersAsync( ParameterView parameters ) | ||
{ | ||
parameters.SetParameterProperties( this ); | ||
|
||
if( !_hasInitializedParameters ) | ||
{ | ||
// This is the first run | ||
// Could put this logic in OnInit, but its nice to avoid forcing people who override OnInit to call base.OnInit() | ||
if( ValueExpression is null ) | ||
{ | ||
throw new InvalidOperationException( | ||
$"{GetType()} requires a value for the '{nameof( ValueExpression )}' parameter. " + | ||
$"Normally this is provided automatically when using '@bind-Value'." ); | ||
} | ||
|
||
_nullableUnderlyingType = Nullable.GetUnderlyingType( typeof( TValue ) ); | ||
_hasInitializedParameters = true; | ||
} | ||
|
||
return base.SetParametersAsync( ParameterView.Empty ); | ||
} | ||
|
||
/// <summary> | ||
/// Parses a string to create an instance of <typeparamref name="TValue"/>. | ||
/// Derived classes can override this to change how <see cref="CurrentValueAsString"/> interprets incoming values. | ||
/// </summary> | ||
/// <param name="value">The string value to be parsed.</param> | ||
/// <param name="result">An instance of <typeparamref name="TValue"/>.</param> | ||
/// <returns><see langword="true"/> if the value could be parsed; otherwise <see langword="false"/>.</returns> | ||
protected abstract bool TryParseValueFromString( string? value, [MaybeNullWhen( false )] out TValue? result ); | ||
|
||
/// <summary> | ||
/// Sets the current value of the input. | ||
/// </summary> | ||
/// <param name="value">The value to set.</param> | ||
protected internal async Task SetCurrentValueAsync( TValue? value ) | ||
{ | ||
var hasChanged = !EqualityComparer<TValue>.Default.Equals( value, Value ); | ||
if( hasChanged ) | ||
{ | ||
_parsingFailed = false; | ||
|
||
Value = value; | ||
await ValueChanged.InvokeAsync( value ); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Sets the current value of the input, represented as a string. | ||
/// </summary> | ||
/// <param name="value">The value to set.</param> | ||
protected internal async Task SetCurrentValueAsStringAsync( string? value ) | ||
{ | ||
_incomingValueBeforeParsing = value; | ||
|
||
if( _nullableUnderlyingType is not null && string.IsNullOrEmpty( value ) ) | ||
{ | ||
_parsingFailed = false; | ||
CurrentValue = default!; | ||
} | ||
else if( TryParseValueFromString( value, out var parsedValue ) ) | ||
{ | ||
_parsingFailed = false; | ||
await SetCurrentValueAsync( parsedValue ); | ||
} | ||
else | ||
{ | ||
_parsingFailed = true; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Formats the input value as a string. | ||
/// Derived classes can override this to determine the formatting used for <see cref="CurrentValueAsString"/>. | ||
/// </summary> | ||
/// <param name="value">The value to format.</param> | ||
/// <returns>A string representation of the input value.</returns> | ||
protected virtual string? FormatValueAsString( TValue? value ) => value?.ToString(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
using LumexUI.Common; | ||
|
||
namespace LumexUI; | ||
|
||
[ExcludeFromCodeCoverage] | ||
public class CheckboxSlots : ISlot | ||
{ | ||
public string? Root { get; set; } | ||
public string? Wrapper { get; set; } | ||
public string? Icon { get; set; } | ||
public string? Label { get; set; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
@namespace LumexUI | ||
@inherits LumexInputBase<bool> | ||
|
||
<LumexComponent As="label" | ||
Class="@RootClass" | ||
Style="@RootStyle" | ||
data-checked="@Checked" | ||
tabindex="1" | ||
@attributes="@AdditionalAttributes"> | ||
<span class="sr-only"> | ||
<input type="checkbox" | ||
value="@true" | ||
checked="@Checked" | ||
disabled="@Disabled" | ||
@ref="@Element" | ||
@attributes="@AdditionalAttributes" | ||
@onchange="@OnChangeAsync" /> | ||
</span> | ||
|
||
<span class="@WrapperClass"> | ||
@if( !string.IsNullOrEmpty( CheckIcon ) ) | ||
{ | ||
<LumexIcon Icon="@CheckIcon" Class="@IconClass" /> | ||
} | ||
else | ||
{ | ||
@_renderCheckIcon | ||
} | ||
</span> | ||
|
||
@if( ChildContent is not null ) | ||
{ | ||
<span class="@LabelClass"> | ||
@ChildContent | ||
</span> | ||
} | ||
</LumexComponent> | ||
|
||
@code { | ||
private void RenderCheckIcon( RenderTreeBuilder __builder ) | ||
{ | ||
var style = ElementStyle.Empty() | ||
.Add( "transition", "stroke-dashoffset 0.15s linear 0.15s", when: Checked ) | ||
.ToString(); | ||
|
||
<svg class="@IconClass" viewBox="0 0 17 18"> | ||
<polyline fill="none" | ||
points="1 9 7 14 15 4" | ||
stroke="currentColor" | ||
stroke-dasharray="22" | ||
stroke-dashoffset="@(Checked ? 44 : 66)" | ||
stroke-linecap="round" | ||
stroke-linejoin="round" | ||
stroke-width="2" | ||
style="@style" /> | ||
</svg> | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// Copyright (c) LumexUI 2024 | ||
// LumexUI licenses this file to you under the MIT license | ||
// See the license here https://github.com/LumexUI/lumexui/blob/main/LICENSE | ||
|
||
using LumexUI.Common; | ||
using LumexUI.Styles; | ||
|
||
using Microsoft.AspNetCore.Components; | ||
|
||
namespace LumexUI; | ||
|
||
public partial class LumexCheckbox : LumexInputBase<bool>, ISlotComponent<CheckboxSlots> | ||
{ | ||
/// <summary> | ||
/// Gets or sets content to be rendered inside the checkbox. | ||
/// </summary> | ||
[Parameter] public RenderFragment? ChildContent { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the icon to be used for indicating a checked state of the checkbox. | ||
/// </summary> | ||
[Parameter] public string? CheckIcon { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the CSS class names for the checkbox slots. | ||
/// </summary> | ||
[Parameter] public CheckboxSlots? Classes { get; set; } | ||
|
||
private protected override string? RootClass => | ||
TwMerge.Merge( Checkbox.GetStyles( this ) ); | ||
|
||
private string WrapperClass => | ||
TwMerge.Merge( Checkbox.GetWrapperStyles( this ) ); | ||
|
||
private string IconClass => | ||
TwMerge.Merge( Checkbox.GetIconStyles( this ) ); | ||
|
||
private string LabelClass => | ||
TwMerge.Merge( Checkbox.GetLabelStyles( this ) ); | ||
|
||
private readonly RenderFragment _renderCheckIcon; | ||
|
||
private bool Checked => CurrentValue; | ||
|
||
public LumexCheckbox() | ||
{ | ||
_renderCheckIcon = RenderCheckIcon; | ||
} | ||
|
||
/// <inheritdoc /> | ||
protected override bool TryParseValueFromString( string? value, out bool result ) | ||
{ | ||
throw new NotSupportedException( | ||
$"This component does not parse string inputs. " + | ||
$"Bind to the '{nameof( CurrentValue )}' property, not '{nameof( CurrentValueAsString )}'." ); | ||
} | ||
|
||
private Task OnChangeAsync( ChangeEventArgs args ) | ||
{ | ||
if( Disabled || ReadOnly ) | ||
{ | ||
return Task.CompletedTask; | ||
} | ||
|
||
return SetCurrentValueAsync( (bool)args.Value! ); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.