The NumberBox lets users enter and adjust numbers (quantity, price, tax rate, font size…). Its style and behavior reproduce the WinUI 3 (Windows App SDK) NumberBox: it is essentially a TextBox + math parsing + NaN semantics, so this component embeds FluereInput (consistent with the embedded TextBox in the WinUI template) and implements WinUI's value-handling semantics, validation modes, spin buttons and inline expressions.
Values follow WinUI's Value ↔ Text dual channel: when the value changes, the formatted text is written back to the input box; on blur or Enter, the text is parsed and the value updated. Clearing the input box yields null (corresponding to WinUI's NaN), at which point placeholder is shown.
Difference from the original plan in
docs/todo.md: reuse of reka-ui'sNumberFieldwas originally planned, but itsNumberFieldInputhas a few hard conflicts with WinUI —Home/Endare mapped to min / max (WinUI leaves them to the TextBox to move the caret), invalid input clears the value on blur (WinUI keeps the original value), arrow keys snap to the step (WinUI adds directly), the wheel direction is reversed, andLargeChangeis hardcoded to 10 steps. Here the "reuse" approach is kept but re-implemented as a self-contained implementation: theformat/parse/expression/stepfour pure-function layers plus theuse-number-box/use-spin-repeattwo hooks, each unit-testable independently.
v-model binds the numeric value, header is WinUI's title row (also the default accessible name), and description renders below the control and acts as aria-describedby.
Quantity: 12 · Unit price: 199.9 (clearing the input yields null, corresponding to WinUI's NaN)
min / max map to Minimum / Maximum (by default no bounds are set, in which case aria-valuemin / aria-valuemax are not rendered); step is SmallChange (arrow keys / wheel / spin buttons), largeStep is LargeChange (PageUp / PageDown). Consistent with WinUI: no step snapping, values are only clamped (or wrapped) when out of range.
Arrow keys / wheel move by step, PageUp / PageDown by largeStep; out-of-range values are clamped to the range, and — as with WinUI — there is no step snapping.
Pass wrap-enabled for IsWrapEnabled: after reaching an endpoint the value wraps around to the other end, and both buttons stay usable.
spinButtonPlacementMode maps to SpinButtonPlacementMode: hidden (default) / inline (up/down buttons inline to the right of the input box) / compact (a button panel floats up on focus). inline shares a TextBox border with the input box; the buttons are 32×24, margin 4, corner radius 4; holding a button steps once immediately, then repeats every 250ms (the WinUI RepeatButton Delay / Interval defaults). Buttons never enter the Tab sequence (WinUI IsTabStop=False) but can be activated with Enter / Space.
The inline buttons and the input box share one TextBox border: the buttons are 32×24 with margin 4 and corner radius 4, repeating every 250ms while held (the WinUI RepeatButton Delay / Interval defaults).
Compact mode shows up / down indicators to the right of the input box; on focus a 36×36 button panel floats out on the right (corner radius 8 + shadow depth 16) and collapses on blur.
accepts-expression maps to AcceptsExpression: on blur or Enter it evaluates expressions composed of + - * / ^ and parentheses, with precedence ^ > * / > + -. After evaluation the raw expression is not preserved (it is replaced by the result).
Computed value: 0 · Budget: 1000 · Tax rate: 0.06
validationMode maps to ValidationMode:
invalidInputOverwritten (default): on blur / Enter, invalid input is overwritten by the formatted text of the current value, and out-of-range values are clamped back into the range;disabled: keeps the user's input and out-of-range values, leaving validation to the consumer.Escape always discards the input and returns to the current value; Enter commits the input (and swallows the key to avoid triggering an implicit form submission, matching how WinUI handles Enter).
Strict mode: 20 · Lenient mode: 20
formatOptions (Intl.NumberFormatOptions) maps to NumberFormatter, and locale determines the decimal point / group separators / minus sign — display and parsing share the same set of symbols. SSR apps are recommended to pass locale explicitly: by default the runtime's default locale is used, but the server and browser defaults may differ (e.g. the de-DE decimal point), producing a hydration mismatch on the first frame.
WinUI's default DecimalFormatter is set to IntegerDigits(1) + FractionDigits(0), which on several WinAppSDK versions rounds the displayed value to an integer (microsoft/microsoft-ui-xaml#8780: entering 25.8 shows 26 after blur). On the web side decimals are not dropped by default (maximumFractionDigits: 20), otherwise a usage such as step = 0.1 becomes unusable at the display layer; pass :format-options="{ maximumFractionDigits: 0 }" to get WinUI's integer look. Before display, a WinUI-style 10-significant-digit rounding is also applied to wipe out floating-point noise such as 0.1 + 0.2 = 0.30000000000000004.
When disabled, the input box falls to the TextControl…Disabled tier and the spin buttons move to …ForegroundDisabled; the header and description both gray out.
When placed inside a <form> with a name, it adds a hidden native <input type="hidden"> submitting name=value (an empty string when there is no value).
| Prop (Props) | Type | Default | Description |
|---|---|---|---|
modelValue | number | null | — | Current value (v-model); null means no value (WinUI NaN) |
defaultValue | number | null | null | Uncontrolled initial value |
min / max | number | — (no bounds) | Range (WinUI Minimum / Maximum) |
step | number | 1 | Step (WinUI SmallChange) |
largeStep | number | 10 | Large step (WinUI LargeChange, PageUp / PageDown) |
disabled | boolean | false | Whether the control is disabled |
header | string | — | Top title (WinUI Header), also the default accessible name |
label | string | — | Accessible name (takes precedence over header) |
description | string | — | Description below the control (WinUI Description), also used as aria-describedby |
placeholder | string | — | Placeholder (WinUI PlaceholderText), shown only when there is no value |
name | string | — | Form submission name, paired with the hidden native input |
locale | string | — | BCP 47 language tag; determines the formatting and parsing symbols |
formatOptions | Intl.NumberFormatOptions | maximumFractionDigits: 20 | Number formatting (WinUI NumberFormatter) |
spinButtonPlacementMode | 'hidden' | 'compact' | 'inline' | 'hidden' | Spin button presentation (WinUI SpinButtonPlacementMode) |
validationMode | 'invalidInputOverwritten' | 'disabled' | 'invalidInputOverwritten' | Validation mode (WinUI ValidationMode) |
wrapEnabled | boolean | false | Wrap around at the endpoints (WinUI IsWrapEnabled) |
acceptsExpression | boolean | false | Whether inline expressions are parsed (WinUI AcceptsExpression) |
increaseLabel | string | 'Increase' | Accessible name of the plus button (localized from a resource in WinUI) |
decreaseLabel | string | 'Decrease' | Accessible name of the minus button |
| Event | Payload | Description |
|---|---|---|
update:modelValue | number | null | Value change (v-model) |
valueChanged | { oldValue: number | null, newValue: number | null } | Value change (corresponds to WinUI ValueChanged) |
| Slot (Slots) | Description |
|---|---|
header | Top title row (WinUI Header / HeaderTemplate) |
description | Description below the control (WinUI Description) |
Keyboard and pointer handling follow
NumberBox.cpp: arrow keys step bySmallChange,PageUp/PageDownbyLargeChange,Entercommits,Escapereverts, and gaining focus selects the whole text; the mouse wheel only responds while the input box holds focus, scrolling up increments (MouseWheelDelta > 0). WinUI leavesHome/Endto the TextBox, and this component likewise does not treat them as step keys.Accessibility: the input box carries
role="spinbutton"witharia-valuenow/aria-valuemin/aria-valuemax(the latter two are not rendered when no bounds are set, matching WinUI's behavior of composing them into the UIA name only onceMinimum/Maximumare rewritten); the spin buttons do not enter the Tab sequence but can be activated by keyboard, and each carries an accessible name.Two web-side approximations: the compact popup approximates WinUI's Acrylic surface with
colorNeutralBackground1+shadow16(the Fluent token set has no material tokens); when the inline buttons appear, the right side of the input box gives up an extra 84px to keep text from pressing onto the buttons (WinUI lets text press under the buttons).WinUI's
Textproperty is not exposed as a separate prop: on the web side the input-box text is derived fromv-modelplus formatting (Value → Text); useformatOptionsfor custom text display. The WinUI initialization order of "when bothTextandValueare set,Valuewins" therefore does not apply.