feat: Add FluentNumberInput<TValue> component with demo and tests#4668
feat: Add FluentNumberInput<TValue> component with demo and tests#4668AClerbois wants to merge 5 commits intomicrosoft:dev-v5from
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new FluentNumberInput<TValue> component (with JS interop support and tests) and introduces a demo documentation page for it, including an auto-generated API section.
Changes:
- Added
FluentNumberInput<TValue>(Razor/C#) with invariant-culture formatting/parsing and JS interop to applystep/min/maxto the inner<input>. - Added unit tests for rendering, binding/parsing, and culture-related parsing/formatting behavior.
- Added demo docs + examples for the new component, including an
## API FluentNumberInputsection with{{ API Type=FluentNumberInput<int> }}.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Core/Components/NumberInput/FluentNumberInputTests.razor | New unit tests covering rendering, binding, and culture parsing. |
| src/Core/Components/NumberInput/FluentNumberInput.razor.css | Adds width/max-width styling consistency with other inputs. |
| src/Core/Components/NumberInput/FluentNumberInput.razor.cs | Implements the new component logic and JS interop calls. |
| src/Core/Components/NumberInput/FluentNumberInput.razor | Adds the Razor markup wrapping <fluent-text-input type="number"> in FluentField. |
| src/Core.Scripts/src/ExportedMethods.ts | Exposes the new NumberInput JS namespace on window.Microsoft.FluentUI.... |
| src/Core.Scripts/src/Components/NumberInput/NumberInput.ts | Adds helper to set step/min/max on the inner control. |
| examples/Demo/FluentUI.Demo.Client/Documentation/Components/NumberInput/FluentNumberInput.md | New documentation page including API generation directive. |
| examples/Demo/FluentUI.Demo.Client/Documentation/Components/NumberInput/Examples/NumberInput*.razor | New demo examples for common scenarios (types, culture, min/max/step, etc.). |
| AGENTS.md | Updates the repo structure description (minor wording change). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
examples/Demo/FluentUI.Demo.Client/Documentation/Components/NumberInput/FluentNumberInput.md
Show resolved
Hide resolved
...uentUI.Demo.Client/Documentation/Components/NumberInput/Examples/NumberInputAppearance.razor
Outdated
Show resolved
Hide resolved
.../FluentUI.Demo.Client/Documentation/Components/NumberInput/Examples/NumberInputCulture.razor
Outdated
Show resolved
Hide resolved
.../FluentUI.Demo.Client/Documentation/Components/NumberInput/Examples/NumberInputCulture.razor
Outdated
Show resolved
Hide resolved
.../FluentUI.Demo.Client/Documentation/Components/NumberInput/Examples/NumberInputDefault.razor
Outdated
Show resolved
Hide resolved
...luentUI.Demo.Client/Documentation/Components/NumberInput/Examples/NumberInputImmediate.razor
Outdated
Show resolved
Hide resolved
...uentUI.Demo.Client/Documentation/Components/NumberInput/Examples/NumberInputMinMaxStep.razor
Outdated
Show resolved
Hide resolved
- Revert AGENTS.md change (unrelated to this PR) - Fix Min/Max/Step to reapply when parameters change (not only on firstRender) - Add Wrap=true to Appearance example for responsive layout - Simplify Culture example, move docs to .md file - Delete redundant examples (Default, Immediate, MinMaxStep) - Update FluentNumberInput.md to remove deleted example references - Add Verify snapshot test for default int rendering
| } | ||
|
|
||
| result = default; | ||
| validationErrorMessage = $"The '{DisplayName ?? "Unknown Bound Field"}' field is not valid."; |
There was a problem hiding this comment.
this needs to be in the localization files
There was a problem hiding this comment.
Done. Added NumberInput_InvalidValue key to LanguageResource.resx with value "The {0} field is not a valid number." and the error message now uses Localizer[Localization.LanguageResource.NumberInput_InvalidValue].
| /// </summary> | ||
| protected override string? FormatValueAsString(TValue value) | ||
| { | ||
| return Convert.ToString(value, CultureInfo.InvariantCulture); |
There was a problem hiding this comment.
we should use BindConverter here to avoid boxing.
protected override string? FormatValueAsString(TValue? value)
{
// Avoiding a cast to IFormattable to avoid boxing.
switch (value)
{
case null:
return null;
case int @int:
return BindConverter.FormatValue(@int, CultureInfo.InvariantCulture);
case long @long:
return BindConverter.FormatValue(@long, CultureInfo.InvariantCulture);
case short @short:
return BindConverter.FormatValue(@short, CultureInfo.InvariantCulture);
case float @float:
return BindConverter.FormatValue(@float, CultureInfo.InvariantCulture);
case double @double:
return BindConverter.FormatValue(@double, CultureInfo.InvariantCulture);
case decimal @decimal:
return BindConverter.FormatValue(@decimal, CultureInfo.InvariantCulture);
default:
throw new InvalidOperationException($"Unsupported type {value.GetType()}");
}There was a problem hiding this comment.
Addressed differently: replaced the IComparable<TValue> constraint with INumber<TValue> (from System.Numerics, available since .NET 7). Since INumber<T> inherits IFormattable, we now call value.ToString(null, Culture) directly — no boxing occurs because TValue is constrained to struct. This avoids the large switch on concrete types while covering all numeric types.
| { | ||
| // Normalize comma to dot to handle edge cases where a browser | ||
| // might send the locale-formatted decimal separator. | ||
| var normalized = value?.Replace(',', '.'); |
There was a problem hiding this comment.
I don't think that this is necessary. The native InputNumber component from Blazor just does it like this:
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
if (BindConverter.TryConvertTo<TValue>(value, CultureInfo.InvariantCulture, out result))
{
validationErrorMessage = null;
return true;
}
else
{
validationErrorMessage = string.Format(CultureInfo.InvariantCulture, ParsingErrorMessage, DisplayName ?? FieldIdentifier.FieldName);
return false;
}
}There was a problem hiding this comment.
Agreed. Replaced BindConverter.TryConvertTo with TValue.TryParse(normalized, Culture, out var parsedValue) — a static abstract method from IParsable<TSelf> (inherited by INumber<TValue>). Simpler and no trimming annotation needed.
| /// <exception cref="InvalidOperationException">Thrown when TValue is not a supported numeric type.</exception> | ||
| public FluentNumberInput(LibraryConfiguration configuration) : base(configuration) | ||
| { | ||
| if (typeof(TValue) != typeof(byte) && |
There was a problem hiding this comment.
we should probably check for nullable types as well.
var targetType = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue);There was a problem hiding this comment.
Resolved by switching the constraint to where TValue : struct, INumber<TValue>. This provides compile-time safety — only numeric types are accepted. The 11-type runtime check and Nullable.GetUnderlyingType are no longer needed since the compiler enforces the constraint.
| @inherits FluentUITestContext | ||
| @code | ||
| { | ||
| public FluentNumberInputTests() |
There was a problem hiding this comment.
can you add test cases for null as well? In v4 this component was quite buggy with null.
There was a problem hiding this comment.
good remark, I will do
There was a problem hiding this comment.
Added tests for invalid input ("abc") and empty input (""), which verify the value remains unchanged when parsing fails. With the struct constraint, TValue itself cannot be null (e.g. int defaults to 0). Total: 24 tests, 95.5% coverage.
| @using System.Globalization | ||
|
|
||
| <FluentStack Orientation="Orientation.Vertical" VerticalGap="10"> | ||
| <FluentNumberInput TValue="double" |
There was a problem hiding this comment.
You must add a Culture (type CultureInfo) parameter to allow a specific format when the browser or server uses a different culture. E.g. https://localhost:7197/DateTime/Calendar#culture
There was a problem hiding this comment.
Done. Added [Parameter] public CultureInfo Culture { get; set; } = CultureInfo.InvariantCulture; — used in FormatValueAsString, TryParseValueFromString, and FormatNullableValue.
| Label="@($"Culture: {CultureInfo.CurrentCulture.Name}")" | ||
| Step="0.01" | ||
| Min="0" | ||
| Max="9999.99" |
There was a problem hiding this comment.
If the minimum and maximum values are not met, the bound value must be set to the minimum or maximum value, not to the specified value.
See an example here: https://www.mudblazor.com/components/numericfield
There was a problem hiding this comment.
Done. Values are now clamped to Min/Max bounds in TryParseValueFromString. Added tests: Clamp_AboveMax, Clamp_BelowMin, Clamp_WithinRange, Clamp_Double_AboveMax, and Clamp_Double_BelowMin.
| Min="0" | ||
| Max="9999.99" | ||
| Placeholder="0.00" | ||
| @bind-Value="@price" /> |
There was a problem hiding this comment.
@dvoituron I can't even write , or . into this fields. How did you put the , there?
There was a problem hiding this comment.
@AClerbois it looks like the underlying web-component is not yet supporting the number type. The only way for us to get this to work is switching the input type to text.
There was a problem hiding this comment.
This is a limitation of the <fluent-text-input type="number"> web component — it doesn't properly support decimal character input like a native <input type="number"> does. As @MarvinKlein1508 mentioned, switching to type="text" may be the only workaround. This would require changes to the Razor template and JS interop. Should we address this in a follow-up PR?
There was a problem hiding this comment.
@AClerbois I'm not sure about a follow-up PR. Right now this component is pretty much unusable for me when I need to enter and decimal numbers. It only works for full numbers for me.
I can't enter 1.5 or 1,5. My locale is de-de
- Replace struct+IComparable constraint with INumber<TValue> for compile-time numeric type safety - Use TValue.TryParse (via IParsable) instead of BindConverter.TryConvertTo - Use IFormattable.ToString instead of Convert.ToString to avoid boxing - Add Culture parameter (defaults to InvariantCulture) for formatting/parsing - Clamp parsed values to Min/Max bounds - Localize parsing error message via LanguageResource.NumberInput_InvalidValue - Add tests: invalid input, empty input, double clamping (24 total, 95.5% coverage)
|
I’m working on it
From: Marvin Klein ***@***.***>
Sent: Friday, 3 April 2026 19:53
To: microsoft/fluentui-blazor ***@***.***>
Cc: Adrien Clerbois ***@***.***>; Mention ***@***.***>
Subject: Re: [microsoft/fluentui-blazor] feat: Add FluentNumberInput<TValue> component with demo and tests (PR #4668)
@MarvinKlein1508 commented on this pull request.
________________________________
In examples/Demo/FluentUI.Demo.Client/Documentation/Components/NumberInput/Examples/NumberInputCulture.razor<#4668?email_source=notifications&email_token=AMC45VPRYAXVL7EHJR4MOHT4T73AHA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTIMBVGY3DKMBTGM3KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJL3QOJPXEZLWNFSXOX3DNRUWG2Y#discussion_r3033769604>:
@@ -0,0 +1,18 @@
***@***.*** System.Globalization
+
+<FluentStack Orientation="Orientation.Vertical" VerticalGap="10">
+ <FluentNumberInput TValue="double"
+ Label="@($"Culture: {CultureInfo.CurrentCulture.Name}")"
+ Step="0.01"
+ Min="0"
+ Max="9999.99"
+ Placeholder="0.00"
+ @***@***.***" />
@AClerbois<https://github.com/AClerbois> I'm not sure about a follow-up PR. Right now this component is pretty much unusable for me when I need to enter and decimal numbers. It only works for full numbers for me.
I can't enter 1.5 or 1,5. My locale is de-de
—
Reply to this email directly, view it on GitHub<#4668?email_source=notifications&email_token=AMC45VIKWSYFAOVC6KT3IV34T73AHA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTIMBVGY3DKMBTGM3KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJPLQOJPXEZLWNFSXOX3ON52GSZTJMNQXI2LPNZZV6Y3MNFRWW#discussion_r3033769604>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AMC45VMWEGJCEOCET6GJ2WT4T73AHAVCNFSM6AAAAACXJNTSCWVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHM2DANJWGY2TAMZTGY>.
You are receiving this because you were mentioned.Message ID: ***@***.******@***.***>>
|
…port The HTML input type=number blocks locale-specific decimal separators (e.g. comma in fr-FR). Switching to type=text with inputmode=decimal preserves the mobile numeric keyboard while allowing all decimal characters to be entered.



Summary
Adds a new
FluentNumberInput<TValue>component with full JS interop support, demo documentation, and unit tests.Changes
Component (
src/Core/Components/NumberInput/)FluentNumberInput<TValue>— numeric input wrapping<fluent-text-input type="number">insideFluentFieldbyte,sbyte,short,ushort,int,uint,long,ulong,float,double,decimalstep/min/maxto the inner shadow-DOM<input>, with re-application on parameter changesJS Interop (
src/Core.Scripts/)NumberInput.ts— helper to setstep/min/maxon the inner controlExportedMethods.tsDemo documentation (
examples/Demo/.../NumberInput/){{ API Type=FluentNumberInput<int> }}Tests (
tests/Core/Components/NumberInput/)Closes #4667