diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/NumericField/NumericFieldTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/NumericField/NumericFieldTest.razor
index ca7a1ddae27d..d297728dba7d 100644
--- a/src/MudBlazor.UnitTests.Viewer/TestComponents/NumericField/NumericFieldTest.razor
+++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/NumericField/NumericFieldTest.razor
@@ -39,7 +39,7 @@
Variant="Variant.Filled"
Margin="Margin.Dense"
Max="9.9"
- Min="-5.0"
+ Min="-5.0"
Immediate="true"/>
-
+
+ Label="decimal?"
+ Variant="Variant.Outlined"
+ Margin="Margin.Dense" />
+
+
+
+
@code {
public static string __description__ = "The textfield should be changed by up/down arrows";
diff --git a/src/MudBlazor.UnitTests/Components/NumericFieldTests.cs b/src/MudBlazor.UnitTests/Components/NumericFieldTests.cs
index cb749308e4e8..2f8976976a25 100644
--- a/src/MudBlazor.UnitTests/Components/NumericFieldTests.cs
+++ b/src/MudBlazor.UnitTests/Components/NumericFieldTests.cs
@@ -25,7 +25,7 @@ namespace MudBlazor.UnitTests.Components
public class NumericFieldTests : BunitTest
{
// TestCaseSource does not know about "Nullable" so having values as Nullable does not make sense here
- static object[] TypeCases =
+ static object[] TypeCases =
{
new object[] { (byte)5 },
new object[] { (sbyte)5 },
@@ -50,7 +50,7 @@ public void NumericFieldLabelFor()
var label = comp.FindAll(".mud-input-label");
label[0].Attributes.GetNamedItem("for")?.Value.Should().Be("numericFieldLabelTest");
}
-
+
///
/// Initial Text for double should be 0, with F1 format it should be 0.0
///
@@ -93,10 +93,10 @@ public void NumericFieldTest2()
///
/// Setting the value to null should not cause a validation error
///
- [Test]
- public async Task IntNumericFieldWithNullableTypes()
+ [TestCaseSource(nameof(TypeCases))]
+ public async Task NumericField_WithNullableTypes_ShouldAllowNulls(T value) where T : struct
{
- var comp = Context.RenderComponent>(ComponentParameter.CreateParameter("Value", 17));
+ var comp = Context.RenderComponent>(ComponentParameter.CreateParameter("Value", value));
// print the generated html
//Console.WriteLine(comp.Markup);
comp.SetParametersAndRender(ComponentParameter.CreateParameter("Value", null));
@@ -106,51 +106,6 @@ public async Task IntNumericFieldWithNullableTypes()
comp.Find("input").Blur();
comp.FindAll("div.mud-input-error").Count.Should().Be(0);
}
-
- ///
- /// Setting the value to null should not cause a validation error
- ///
- [Test]
- public async Task DecimalNumericFieldWithNullableTypes()
- {
- var comp = Context.RenderComponent>(ComponentParameter.CreateParameter("Value", 17M));
- comp.SetParametersAndRender(ComponentParameter.CreateParameter("Value", null));
- comp.Find("input").Blur();
- comp.FindAll("div.mud-input-error").Count.Should().Be(0);
- comp.Find("input").Change("");
- comp.Find("input").Blur();
- comp.FindAll("div.mud-input-error").Count.Should().Be(0);
- }
-
- ///
- /// Setting the value to null should not cause a validation error
- ///
- [Test]
- public async Task Int64NumericFieldWithNullableTypes()
- {
- var comp = Context.RenderComponent>(ComponentParameter.CreateParameter("Value", 17L));
- comp.SetParametersAndRender(ComponentParameter.CreateParameter("Value", null));
- comp.Find("input").Blur();
- comp.FindAll("div.mud-input-error").Count.Should().Be(0);
- comp.Find("input").Change("");
- comp.Find("input").Blur();
- comp.FindAll("div.mud-input-error").Count.Should().Be(0);
- }
-
- ///
- /// Setting the value to null should not cause a validation error
- ///
- [Test]
- public async Task UInt64NumericFieldWithNullableTypes()
- {
- var comp = Context.RenderComponent>(ComponentParameter.CreateParameter("Value", 17UL));
- comp.SetParametersAndRender(ComponentParameter.CreateParameter("Value", null));
- comp.Find("input").Blur();
- comp.FindAll("div.mud-input-error").Count.Should().Be(0);
- comp.Find("input").Change("");
- comp.Find("input").Blur();
- comp.FindAll("div.mud-input-error").Count.Should().Be(0);
- }
//This doesn't make any sense because you cannot set anything that's not a number
/////
@@ -280,7 +235,7 @@ public async Task NumericFieldFluentValidationTest1()
//Console.WriteLine("Error message: " + numericField.ErrorText);
numericField.ErrorText.Should().BeNullOrEmpty();
}
-
+
///
/// Validate handling of decimal support & precision kept
///
@@ -431,7 +386,7 @@ public async Task NumericFieldTest_KeyboardInput()
comp.Find("input").KeyUp(new KeyboardEventArgs() { Key = "9", Type = "keyup", });
comp.WaitForAssertion(() => numericField.Value.Should().Be(1234.56));
}
-
+
///
/// Keydown disabled, should not do anything
///
@@ -450,7 +405,7 @@ public async Task NumericFieldTest_KeyboardInput_Disabled()
comp.Find("input").KeyUp(new KeyboardEventArgs() { Key = "9", Type = "keyup", });
comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(1234.56));
}
-
+
///
/// Keydown readonly, should not do anything
///
@@ -676,6 +631,46 @@ public async Task NumericField_Validation(T value)
numericField.Value.Should().Be(value);
}
+ [TestCaseSource(nameof(TypeCases))]
+ public async Task NumericFieldMinMax(T value)
+ {
+ var min = (T)Convert.ChangeType(1, typeof(T));
+ var max = (T)Convert.ChangeType(10, typeof(T));
+ var comp = Context.RenderComponent>();
+ comp.SetParam(x => x.Min, min);
+ comp.SetParam(x => x.Max, max);
+
+ comp.Find("input").Change("15");
+ comp.Find("input").Blur();
+
+ comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(max));
+
+ comp.Find("input").Change("0");
+ comp.Find("input").Blur();
+
+ comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(min));
+ }
+
+ [TestCaseSource(nameof(TypeCases))]
+ public async Task NumericFieldMinMaxNullable(T value) where T : struct
+ {
+ var min = (T)Convert.ChangeType(1, typeof(T));
+ var max = (T)Convert.ChangeType(10, typeof(T));
+ var comp = Context.RenderComponent>();
+ comp.SetParam(x => x.Min, min);
+ comp.SetParam(x => x.Max, max);
+
+ comp.Find("input").Change("15");
+ comp.Find("input").Blur();
+
+ comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(max));
+
+ comp.Find("input").Change("0");
+ comp.Find("input").Blur();
+
+ comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(min));
+ }
+
[TestCaseSource(nameof(TypeCases))]
public async Task NumericField_Increment_Decrement(T value)
{
@@ -697,6 +692,27 @@ public async Task NumericField_Increment_Decrement(T value)
comp.Instance.Value.Should().Be(value);
}
+ [TestCaseSource(nameof(TypeCases))]
+ public async Task NumericFieldNullable_Increment_Decrement(T value) where T : struct
+ {
+ var comp = Context.RenderComponent>();
+ var max = Convert.ChangeType(10, typeof(T));
+ var min = Convert.ChangeType(0, typeof(T));
+ comp.SetParam(x => x.Max, max);
+ comp.SetParam(x => x.Min, min);
+ comp.SetParam(x => x.Step, value);
+ comp.SetParam(x => x.Value, value);
+ await comp.InvokeAsync(() => comp.Instance.Increment().Wait());
+ await comp.InvokeAsync(() => comp.Instance.Decrement().Wait());
+ comp.Instance.Value.Should().Be(value);
+ // setting min and max to value will cover the boundary checking code
+ comp.SetParam(x => x.Max, value);
+ comp.SetParam(x => x.Min, value);
+ await comp.InvokeAsync(() => comp.Instance.Increment().Wait());
+ await comp.InvokeAsync(() => comp.Instance.Decrement().Wait());
+ comp.Instance.Value.Should().Be(value);
+ }
+
[TestCaseSource(nameof(TypeCases))]
public async Task NumericField_Increment_Decrement_OverflowHandled(T value)
{
@@ -714,6 +730,41 @@ public async Task NumericField_Increment_Decrement_OverflowHandled(T value)
comp.Instance.Value.Should().Be(comp.Instance.Min);
}
+ [TestCaseSource(nameof(TypeCases))]
+ public async Task NumericFieldNullable_Increment_Decrement_OverflowHandled(T value) where T : struct
+ {
+ var comp = Context.RenderComponent>();
+ comp.SetParam(x => x.Step, value);
+
+ // test max overflow
+ comp.SetParam(x => x.Value, comp.Instance.Max);
+ await comp.InvokeAsync(() => comp.Instance.Increment().Wait());
+ comp.Instance.Value.Should().Be(comp.Instance.Max);
+
+ // test min overflow
+ comp.SetParam(x => x.Value, comp.Instance.Min);
+ await comp.InvokeAsync(() => comp.Instance.Decrement().Wait());
+ comp.Instance.Value.Should().Be(comp.Instance.Min);
+ }
+
+ ///
+ /// NumericField with min/max set and nullable int can be cleared
+ ///
+ [TestCase(10, 20, 15)]
+ [TestCase(-20, -10, -15)]
+ public async Task NumericFieldCanBeCleared(int min, int max, int value)
+ {
+ var comp = Context.RenderComponent>();
+ comp.SetParam(x => x.Min, min);
+ comp.SetParam(x => x.Max, max);
+ comp.SetParam(x => x.Value, value);
+
+ comp.Find("input").Change("");
+ comp.Find("input").Blur();
+
+ comp.WaitForAssertion(() => comp.Instance.Value.Should().BeNull());
+ }
+
///
/// Special format with currency format should not result in error
///
diff --git a/src/MudBlazor/Components/NumericField/MudNumericField.razor.cs b/src/MudBlazor/Components/NumericField/MudNumericField.razor.cs
index a763c609ea82..80c4fd543da5 100644
--- a/src/MudBlazor/Components/NumericField/MudNumericField.razor.cs
+++ b/src/MudBlazor/Components/NumericField/MudNumericField.razor.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.RegularExpressions;
@@ -17,6 +18,7 @@ namespace MudBlazor
public partial class MudNumericField : MudDebouncedInput
{
private IKeyInterceptor _keyInterceptor;
+ private Comparer _comparer = new(CultureInfo.InvariantCulture);
public MudNumericField() : base()
{
@@ -180,14 +182,16 @@ private async Task Change(double factor = 1)
try
{
var nextValue = GetNextValue(factor);
- if (nextValue is IComparable comparable)
+
+ // validate that the data type is a value type before we compare them
+ if (typeof(T).IsValueType)
{
- if (factor > 0 && comparable.CompareTo(Value) < 0)
+ if (factor > 0 && _comparer.Compare(nextValue, Value) < 0)
nextValue = Max;
- else if (factor < 0 && comparable.CompareTo(Value) > 0)
+ else if (factor < 0 && (_comparer.Compare(nextValue, Value) > 0 || nextValue is null))
nextValue = Min;
}
-
+
await SetValueAsync(ConstrainBoundaries(nextValue).value);
_elementReference.SetText(Text).AndForget();
}
@@ -226,16 +230,20 @@ private T GetNextValue(double factor)
/// Returns a valid value and if it has been changed.
protected (T value, bool changed) ConstrainBoundaries(T value)
{
- // check if Max/Min has value, if not use MaxValue/MinValue for that data type
- if (value is IComparable comparable)
+ if (value == null)
+ return (default(T), false);
+
+ // validate that the data type is a value type before we compare them
+ if (typeof(T).IsValueType)
{
- if (comparable.CompareTo(Max) > 0)
+ // check if value is bigger than defined MAX, if so take the defined MAX value instead
+ if (_comparer.Compare(value, Max) > 0)
return (Max, true);
- else if (comparable.CompareTo(Min) < 0)
+
+ // check if value is lower than defined MIN, if so take the defined MIN value instead
+ if (_comparer.Compare(value, Min) < 0)
return (Min, true);
- }
- else if (value == null)
- return (default(T), true);
+ };
return (value, false);
}
@@ -419,7 +427,7 @@ private long FromInt64(T v)
=> Convert.ToInt64((long?)(object)v);
private ulong FromUInt64(T v)
=> Convert.ToUInt64((ulong?)(object)v);
-
+
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);