diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index a29a319601..d0db31d5f9 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -753,6 +753,18 @@ + + + Because of the limitation of the web component, the maximum value is set to 9999999999 for really large numbers. + + The maximum value for the underlying type + + + + Because of the limitation of the web component, the minimum value is set to -9999999999 for really large negative numbers. + + The minimum value for the underlying type + Gets or sets the content to be rendered inside the component. @@ -7670,6 +7682,12 @@ Gets or sets the content to be rendered inside the component. + + + If true, the min and max values will be automatically set based on the type of TValue, + unless an explicit value for Min or Max is provided. + + Formats the value as a string. Derived classes can override this to determine the formatting used for CurrentValueAsString. diff --git a/examples/Demo/Shared/Pages/NumberField/Examples/NumberFieldTypeConstraints.razor b/examples/Demo/Shared/Pages/NumberField/Examples/NumberFieldTypeConstraints.razor new file mode 100644 index 0000000000..b8fe38377d --- /dev/null +++ b/examples/Demo/Shared/Pages/NumberField/Examples/NumberFieldTypeConstraints.razor @@ -0,0 +1,26 @@ +

+ Unsigned short with inherent constraints from type + Example unsigned short: @exampleUshort1 +
+ Minimum value: @(ushort.MinValue); Maximum value: @(ushort.MaxValue) +

+ +

+ Unsigned short with inherent constraints from type and manual max + Example unsigned short: @exampleUshort2 +
+ Minimum value: @(ushort.MinValue); Maximum value: 10 +

+ +

+ Unsigned short with inherent constraints, but Min and Max overrides. + Example unsigned short: @exampleUshort3 +
+ Minimum value: 5; Maximum value: 10 +

+ +@code { + ushort exampleUshort1 { get; set; } + ushort exampleUshort2 { get; set; } + ushort exampleUshort3 { get; set; } +} diff --git a/examples/Demo/Shared/Pages/NumberField/Examples/NumberFieldTypes.razor b/examples/Demo/Shared/Pages/NumberField/Examples/NumberFieldTypes.razor index 9748071b1e..a438488efe 100644 --- a/examples/Demo/Shared/Pages/NumberField/Examples/NumberFieldTypes.razor +++ b/examples/Demo/Shared/Pages/NumberField/Examples/NumberFieldTypes.razor @@ -1,46 +1,77 @@ 

- Long
+ Long +
Example long: @exampleLong -
- Minimum value: -999999999999; Maximum value: 999999999999 +
+ Minimum value: @(MinValue); Maximum value: @(MaxValue)

Short -
+
Minimum value: @(short.MinValue); Maximum value: @(short.MaxValue)

- Float
+ Float +
Example float: @exampleFloat -
- Minimum value: @(float.MinValue); Maximum value: @(float.MaxValue) +
+ Minimum value: @(MinValue); Maximum value: @(MaxValue)

- Float
- Example float: @exampleFloat (step=0.25) -
- Minimum value: @(float.MinValue); Maximum value: @(float.MaxValue) + Float +
+ Example float: @exampleFloat2 (step=0.25) +
+ Minimum value: @(MinValue); Maximum value: @(MaxValue)

- Double
+ Double +
Example double: @exampleDouble -
- Minimum value: @(double.MinValue); Maximum value: @(double.MaxValue) +
+ Minimum value: @(MinValue); Maximum value: @(MaxValue)

- Decimal
+ Decimal +
Example decimal: @exampleDecimal -
- Minimum value: @(decimal.MinValue); Maximum value: @(decimal.MaxValue) +
+ Minimum value: @(MinValue); Maximum value: @(MaxValue) +

+ +

+ Unsigned short + Example unsigned short: @exampleUshort +
+ Minimum value: @(ushort.MinValue); Maximum value: @(ushort.MaxValue) +

+ +

+ Unsigned integer + Example unsigned integer: @exampleUint +
+ Minimum value: @(uint.MinValue); Maximum value: @(uint.MaxValue) +

+ +

+ Unsigned long + Example unsigned long: @exampleUlong +
+ Minimum value: @(ulong.MinValue); Maximum value: @(MaxValue)

@code { - int exampleInt { get; set; } = 123; - int exampleInt2 { get; set; } = 345; short shortMin = short.MinValue; - long exampleLong { get; set; } = 999999999999; + long exampleLong { get; set; } = 9999999997; float exampleFloat { get; set; } = 123.45f; + float exampleFloat2 { get; set; } = 123.45f; double exampleDouble { get; set; } = 456.32d; decimal exampleDecimal { get; set; } = Decimal.One / 3; + ushort exampleUshort { get; set; } + uint exampleUint { get; set; } + ulong exampleUlong { get; set; } + + private const long MaxValue = 9999999999; + private const long MinValue = -9999999999; } diff --git a/examples/Demo/Shared/Pages/NumberField/NumberFieldPage.razor b/examples/Demo/Shared/Pages/NumberField/NumberFieldPage.razor index 1e756683d3..ddfae3641b 100644 --- a/examples/Demo/Shared/Pages/NumberField/NumberFieldPage.razor +++ b/examples/Demo/Shared/Pages/NumberField/NumberFieldPage.razor @@ -33,6 +33,8 @@ + + diff --git a/src/Core/Components/Base/FluentInputBaseHandlers.cs b/src/Core/Components/Base/FluentInputBaseHandlers.cs index 05c8c62ac0..3e76ac4ea8 100644 --- a/src/Core/Components/Base/FluentInputBaseHandlers.cs +++ b/src/Core/Components/Base/FluentInputBaseHandlers.cs @@ -34,6 +34,11 @@ protected virtual async Task ChangeHandlerAsync(ChangeEventArgs e) { await SetCurrentValueAsync(result ?? default); _notifyCalled = true; + + if(FieldBound && CascadedEditContext != null) + { + _parsingValidationMessages?.Clear(); // Clear any previous errors + } } else { @@ -56,7 +61,7 @@ protected virtual async Task ChangeHandlerAsync(ChangeEventArgs e) ///
/// /// - protected virtual async Task InputHandlerAsync(ChangeEventArgs e) // TODO: To update in all Input fields + protected virtual async Task InputHandlerAsync(ChangeEventArgs e) // TODO: To update in all Input fields { if (!Immediate) { diff --git a/src/Core/Components/Base/InputHelpers.cs b/src/Core/Components/Base/InputHelpers.cs index f4e7a186f9..827e8aeb38 100644 --- a/src/Core/Components/Base/InputHelpers.cs +++ b/src/Core/Components/Base/InputHelpers.cs @@ -4,19 +4,25 @@ namespace Microsoft.FluentUI.AspNetCore.Components; internal static class InputHelpers { + private const string WebComponentMaxValue = "9999999999"; + private const string WebComponentMinValue = "-9999999999"; + /// + /// Because of the limitation of the web component, the maximum value is set to 9999999999 for really large numbers. + /// + /// The maximum value for the underlying type public static string GetMaxValue() { Type? targetType = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue); TypeCode typeCode = Type.GetTypeCode(targetType); var value = typeCode switch { - TypeCode.Decimal => decimal.MaxValue.ToString(CultureInfo.InvariantCulture), - TypeCode.Double => double.MaxValue.ToString(CultureInfo.InvariantCulture), + TypeCode.Decimal => WebComponentMaxValue, + TypeCode.Double => WebComponentMaxValue, TypeCode.Int16 => short.MaxValue.ToString(), TypeCode.Int32 => int.MaxValue.ToString(), TypeCode.Int64 => "999999999999", TypeCode.SByte => sbyte.MaxValue.ToString(), - TypeCode.Single => float.MaxValue.ToString(CultureInfo.InvariantCulture), + TypeCode.Single => WebComponentMaxValue, TypeCode.UInt16 => ushort.MaxValue.ToString(CultureInfo.InvariantCulture), TypeCode.UInt32 => uint.MaxValue.ToString(), TypeCode.UInt64 => "999999999999", @@ -26,6 +32,10 @@ public static string GetMaxValue() return value; } + /// + /// Because of the limitation of the web component, the minimum value is set to -9999999999 for really large negative numbers. + /// + /// The minimum value for the underlying type public static string GetMinValue() { Type? targetType = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue); @@ -34,16 +44,16 @@ public static string GetMinValue() var value = typeCode switch { - TypeCode.Decimal => decimal.MinValue.ToString(CultureInfo.InvariantCulture), - TypeCode.Double => double.MinValue.ToString(CultureInfo.InvariantCulture), + TypeCode.Decimal => WebComponentMinValue, + TypeCode.Double => WebComponentMinValue, TypeCode.Int16 => short.MinValue.ToString(), TypeCode.Int32 => int.MinValue.ToString(), - TypeCode.Int64 => "-999999999999", + TypeCode.Int64 => WebComponentMinValue, TypeCode.SByte => sbyte.MinValue.ToString(), - TypeCode.Single => float.MinValue.ToString(CultureInfo.InvariantCulture), + TypeCode.Single => WebComponentMinValue, TypeCode.UInt16 => ushort.MinValue.ToString(CultureInfo.InvariantCulture), TypeCode.UInt32 => uint.MinValue.ToString(), - TypeCode.UInt64 => "-999999999999", + TypeCode.UInt64 => ulong.MinValue.ToString(), _ => "" }; @@ -81,16 +91,6 @@ internal static void ValidateLongInputs(string? max, string? min) { throw new ArgumentException("Long Max value is smaller then Min value."); } - - if (maxValue > 999999999999) - { - throw new ArgumentException("Long Max value can not be bigger than 999999999999."); - } - - if (minValue < -999999999999) - { - throw new ArgumentException("Long Min value can not be less than -999999999999."); - } } internal static void ValidateShortInputs(string? max, string? min) @@ -137,12 +137,65 @@ internal static void ValidateDecimalInputs(string? max, string? min) } } - internal static void ValidateInputParameters(string? max, string? min) + internal static void ValidateUShortInputs(string max, string min) + { + var maxValue = Convert.ToUInt16(max); + var minValue = Convert.ToUInt16(min); + + if (maxValue < minValue) + { + throw new ArgumentException("Unsigned Short Max value is smaller than Min value."); + } + } + + internal static void ValidateUIntegerInputs(string max, string min) + { + var maxValue = Convert.ToUInt32(max); + var minValue = Convert.ToUInt32(min); + + if (maxValue < minValue) + { + throw new ArgumentException("Unsigned integer Max value is smaller than Min value."); + } + } + + internal static void ValidateULongInputs(string max, string min) { + var maxValue = Convert.ToUInt64(max); + var minValue = Convert.ToUInt64(min); + + if (maxValue < minValue) + { + throw new ArgumentException("Unsigned Long Max value is smaller than Min value."); + } + } + internal static void ValidateInputParameters(string? max, string? min) + { if (max == null || min == null) { - return; //nothing to validate + // No need to validate if either max or min is null + return; + } + + if (typeof(TValue) == typeof(ushort)) + { + ValidateUShortInputs(max, min); + } + + if (typeof(TValue) == typeof(uint)) + { + ValidateUIntegerInputs(max, min); + } + + if (typeof(TValue) == typeof(ulong)) + { + ValidateULongInputs(max, min); + } + + if(typeof(TValue) == typeof(byte)) + { + ValidateUShortInputs(max, min); } if (typeof(TValue) == typeof(sbyte)) diff --git a/src/Core/Components/NumberField/FluentNumberField.razor b/src/Core/Components/NumberField/FluentNumberField.razor index e641871d16..9ad70719b9 100644 --- a/src/Core/Components/NumberField/FluentNumberField.razor +++ b/src/Core/Components/NumberField/FluentNumberField.razor @@ -2,7 +2,6 @@ @inherits FluentInputBase @typeparam TValue where TValue : new() @using System.Globalization; -@using System.Reflection; @{ var value = BindConverter.FormatValue(CurrentValue, CultureInfo.InvariantCulture); @@ -21,8 +20,8 @@ minlength="@MinLength" size="@Size" step=@Step - max="@(ReadOnly ? value : Max)" - min="@(ReadOnly ? value : Min)" + max="@(ReadOnly ? value : MaxValue)" + min="@(ReadOnly ? value : MinValue)" id=@Id value="@value" disabled="@Disabled" diff --git a/src/Core/Components/NumberField/FluentNumberField.razor.cs b/src/Core/Components/NumberField/FluentNumberField.razor.cs index 4b369173df..443819d8bd 100644 --- a/src/Core/Components/NumberField/FluentNumberField.razor.cs +++ b/src/Core/Components/NumberField/FluentNumberField.razor.cs @@ -86,7 +86,7 @@ public partial class FluentNumberField : FluentInputBase, IAsync /// Gets or sets the error message to show when the field can not be parsed. /// [Parameter] - public string ParsingErrorMessage { get; set; } = "The {0} field must be a number."; + public string ParsingErrorMessage { get; set; } = "The {0} field must be a (valid) number."; /// /// Gets or sets the content to be rendered inside the component. @@ -94,18 +94,32 @@ public partial class FluentNumberField : FluentInputBase, IAsync [Parameter] public RenderFragment? ChildContent { get; set; } + /// + /// If true, the min and max values will be automatically set based on the type of TValue, + /// unless an explicit value for Min or Max is provided. + /// + [Parameter] + public bool UseTypeConstraints { get; set; } + private static readonly string _stepAttributeValue = GetStepAttributeValue(); + // If type constraints is true and min is null, set min to the minimum value of TValue. + private string? MinValue => UseTypeConstraints && Min == null ? InputHelpers.GetMinValue() : Min; + + // If type constraints is true and max is null, set max to the maximum value of TValue. + private string? MaxValue => UseTypeConstraints && Max == null ? InputHelpers.GetMaxValue() : Max; + private static string GetStepAttributeValue() { - // Unwrap Nullable, because InputBase already deals with the Nullable aspect - // of it for us. We will only get asked to parse the T for nonempty inputs. var targetType = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue); if (targetType == typeof(sbyte) || targetType == typeof(byte) || targetType == typeof(int) || + targetType == typeof(uint) || targetType == typeof(long) || + targetType == typeof(ulong) || targetType == typeof(short) || + targetType == typeof(ushort) || targetType == typeof(float) || targetType == typeof(double) || targetType == typeof(decimal)) @@ -118,20 +132,6 @@ private static string GetStepAttributeValue() } } - protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) - { - if (BindConverter.TryConvertTo(value, CultureInfo.InvariantCulture, out result)) - { - validationErrorMessage = null; - return true; - } - else - { - validationErrorMessage = string.Format(CultureInfo.InvariantCulture, ParsingErrorMessage, FieldBound ? FieldIdentifier.FieldName : UnknownBoundField); - return false; - } - } - /// /// Formats the value as a string. Derived classes can override this to determine the formatting used for CurrentValueAsString. /// @@ -139,7 +139,7 @@ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(fa /// A string representation of the value. protected override string? FormatValueAsString(TValue? value) { - // Avoiding a cast to IFormattable to avoid boxing. + // Directly convert to string using InvariantCulture for all types return value switch { null => null, @@ -150,10 +150,32 @@ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(fa float @float => BindConverter.FormatValue(@float, CultureInfo.InvariantCulture), double @double => BindConverter.FormatValue(@double, CultureInfo.InvariantCulture), decimal @decimal => BindConverter.FormatValue(@decimal, CultureInfo.InvariantCulture), + uint @uint => BindConverter.FormatValue(@uint, CultureInfo.InvariantCulture).ToString(), + ushort @ushort => BindConverter.FormatValue(@ushort, CultureInfo.InvariantCulture).ToString(), + ulong @ulong => BindConverter.FormatValue(@ulong, CultureInfo.InvariantCulture).ToString(), _ => throw new InvalidOperationException($"Unsupported type {value.GetType()}"), }; } + protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) + { + try + { + if (BindConverter.TryConvertTo(value, CultureInfo.InvariantCulture, out result)) + { + validationErrorMessage = null; + return true; + } + } + catch (ArgumentException) + { + result = default!; + } + + validationErrorMessage = string.Format(CultureInfo.InvariantCulture, ParsingErrorMessage, FieldBound ? FieldIdentifier.FieldName : UnknownBoundField); + return false; + } + protected override void OnParametersSet() { InputHelpers.ValidateInputParameters(Max, Min); diff --git a/tests/Core/NumberField/FluentNumberFieldTests.FluentNumberField_SetMinMax_WhenTypeConstraintsTrue.verified.html b/tests/Core/NumberField/FluentNumberFieldTests.FluentNumberField_SetMinMax_WhenTypeConstraintsTrue.verified.html new file mode 100644 index 0000000000..9fb234fdc7 --- /dev/null +++ b/tests/Core/NumberField/FluentNumberFieldTests.FluentNumberField_SetMinMax_WhenTypeConstraintsTrue.verified.html @@ -0,0 +1,2 @@ + +100 \ No newline at end of file diff --git a/tests/Core/NumberField/FluentNumberFieldTests.FluentNumberField_SetMinMax_WhenTypeConstraintsTrueAndMaxIsNotNull.verified.html b/tests/Core/NumberField/FluentNumberFieldTests.FluentNumberField_SetMinMax_WhenTypeConstraintsTrueAndMaxIsNotNull.verified.html new file mode 100644 index 0000000000..cc460b7b18 --- /dev/null +++ b/tests/Core/NumberField/FluentNumberFieldTests.FluentNumberField_SetMinMax_WhenTypeConstraintsTrueAndMaxIsNotNull.verified.html @@ -0,0 +1,2 @@ + +100 \ No newline at end of file diff --git a/tests/Core/NumberField/FluentNumberFieldTests.FluentNumberField_SetMinMax_WhenTypeConstraintsTrueAndMinIsNotNull.verified.html b/tests/Core/NumberField/FluentNumberFieldTests.FluentNumberField_SetMinMax_WhenTypeConstraintsTrueAndMinIsNotNull.verified.html new file mode 100644 index 0000000000..a06493ec51 --- /dev/null +++ b/tests/Core/NumberField/FluentNumberFieldTests.FluentNumberField_SetMinMax_WhenTypeConstraintsTrueAndMinIsNotNull.verified.html @@ -0,0 +1,2 @@ + +100 \ No newline at end of file diff --git a/tests/Core/NumberField/FluentNumberFieldTests.cs b/tests/Core/NumberField/FluentNumberFieldTests.cs index 892ddb48f5..9ebde4ab53 100644 --- a/tests/Core/NumberField/FluentNumberFieldTests.cs +++ b/tests/Core/NumberField/FluentNumberFieldTests.cs @@ -414,6 +414,119 @@ public void FluentNumberField_Throw_WhenFloatMaxIsSmallerThanMin() action.Should().Throw(); } + [Fact] + public void FluentNumberField_Throw_WhenUShortMinIsLargerThanMax() + { + ushort currentValue = 100; + // Act + Action action = () => + { + TestContext.RenderComponent>(parameters => + { + parameters.Add(p => p.Min, "10"); + parameters.Add(p => p.Max, "5"); + parameters.Bind(p => p.Value, currentValue, newValue => currentValue = 101); + parameters.AddChildContent("101"); + }); + }; + + // Assert + action.Should().Throw(); + } + + [Fact] + public void FluentNumberField_Throw_WhenUIntMinIsLargerThanMax() + { + uint currentValue = 100; + // Act + Action action = () => + { + TestContext.RenderComponent>(parameters => + { + parameters.Add(p => p.Min, "10"); + parameters.Add(p => p.Max, "5"); + parameters.Bind(p => p.Value, currentValue, newValue => currentValue = 101); + parameters.AddChildContent("101"); + }); + }; + + // Assert + action.Should().Throw(); + } + + [Fact] + public void FluentNumberField_Throw_WhenULongMinIsLargerThanMax() + { + ulong currentValue = 100; + // Act + Action action = () => + { + TestContext.RenderComponent>(parameters => + { + parameters.Add(p => p.Min, "10"); + parameters.Add(p => p.Max, "5"); + parameters.Bind(p => p.Value, currentValue, newValue => currentValue = 101); + parameters.AddChildContent("101"); + }); + }; + + // Assert + action.Should().Throw(); + } + + [Fact] + public void FluentNumberField_SetMinMax_WhenTypeConstraintsTrue() + { + var currentValue = 100; + + // Arrange && Act + var cut = TestContext.RenderComponent>(parameters => + { + parameters.Add(p => p.UseTypeConstraints, true); + parameters.Bind(p => p.Value, currentValue, newValue => currentValue = 101); + parameters.AddChildContent("100"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNumberField_SetMinMax_WhenTypeConstraintsTrueAndMinIsNotNull() + { + var currentValue = 100; + + // Arrange && Act + var cut = TestContext.RenderComponent>(parameters => + { + parameters.Add(p => p.UseTypeConstraints, true); + parameters.Add(p => p.Min, "10"); + parameters.Bind(p => p.Value, currentValue, newValue => currentValue = 101); + parameters.AddChildContent("100"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNumberField_SetMinMax_WhenTypeConstraintsTrueAndMaxIsNotNull() + { + var currentValue = 100; + + // Arrange && Act + var cut = TestContext.RenderComponent>(parameters => + { + parameters.Add(p => p.UseTypeConstraints, true); + parameters.Add(p => p.Max, "1000"); + parameters.Bind(p => p.Value, currentValue, newValue => currentValue = 101); + parameters.AddChildContent("100"); + }); + + // Assert + cut.Verify(); + } + [Fact] public void FluentNumberField_Throw_WhenDoubleMaxIsSmallerThanMin() {