diff --git a/src/tedi/components/form/number-field/number-field.spec.tsx b/src/tedi/components/form/number-field/number-field.spec.tsx
index b2350576a..f28fd5d94 100644
--- a/src/tedi/components/form/number-field/number-field.spec.tsx
+++ b/src/tedi/components/form/number-field/number-field.spec.tsx
@@ -63,7 +63,7 @@ describe('NumberField component', () => {
fireEvent.click(decrementButton);
const input = screen.getByRole('spinbutton');
- expect(input).toHaveValue(0);
+ expect(input).toHaveValue('0');
});
it('does not increment above the maximum value', () => {
@@ -72,7 +72,31 @@ describe('NumberField component', () => {
fireEvent.click(incrementButton);
const input = screen.getByRole('spinbutton');
- expect(input).toHaveValue(10);
+ expect(input).toHaveValue('10');
+ });
+
+ it('parses a comma as a decimal separator', () => {
+ const handleChange = jest.fn();
+ render();
+ const input = screen.getByRole('spinbutton');
+ fireEvent.change(input, { target: { value: '1,5' } });
+ expect(handleChange).toHaveBeenCalledWith(1.5);
+ });
+
+ it('does not fire onChange for partial entries like a lone minus sign', () => {
+ const handleChange = jest.fn();
+ render();
+ const input = screen.getByRole('spinbutton');
+ fireEvent.change(input, { target: { value: '-' } });
+ expect(handleChange).not.toHaveBeenCalled();
+ });
+
+ it('fires onChange with 0 when the input is cleared', () => {
+ const handleChange = jest.fn();
+ render();
+ const input = screen.getByRole('spinbutton');
+ fireEvent.change(input, { target: { value: '' } });
+ expect(handleChange).toHaveBeenCalledWith(0);
});
it('renders helper text when provided', () => {
@@ -118,4 +142,24 @@ describe('NumberField component', () => {
const container = document.querySelector('.tedi-number-field');
expect(container).toHaveClass('tedi-number-field--small');
});
+
+ it('formats the displayed value with the configured decimal separator', () => {
+ render();
+ const input = screen.getByRole('spinbutton');
+ expect(input).toHaveValue('1,5');
+ });
+
+ it('reformats the display to the configured decimal separator on blur', () => {
+ render();
+ const input = screen.getByRole('spinbutton');
+ fireEvent.change(input, { target: { value: '2.5' } });
+ fireEvent.blur(input);
+ expect(input).toHaveValue('2,5');
+ });
+
+ it('uses inputMode="decimal" when decimalSeparator is a comma', () => {
+ render();
+ const input = screen.getByRole('spinbutton');
+ expect(input).toHaveAttribute('inputmode', 'decimal');
+ });
});
diff --git a/src/tedi/components/form/number-field/number-field.tsx b/src/tedi/components/form/number-field/number-field.tsx
index a9778b38c..21e0e4316 100644
--- a/src/tedi/components/form/number-field/number-field.tsx
+++ b/src/tedi/components/form/number-field/number-field.tsx
@@ -1,5 +1,5 @@
import cn from 'classnames';
-import React, { ChangeEvent, useCallback, useMemo, useRef, useState } from 'react';
+import React, { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { BreakpointSupport, useBreakpointProps } from '../../../helpers';
import { useLabels } from '../../../providers/label-provider';
@@ -51,13 +51,20 @@ export interface NumberFieldProps extends BreakpointSupport void;
/**
* Specifies the input mode for the field (e.g., numeric or decimal).
- * @default numeric
+ * Defaults to `'decimal'` when `decimalPlaces > 0` or `decimalSeparator === ','`,
+ * otherwise numeric.
*/
inputMode?: 'numeric' | 'decimal';
/**
* Number of decimal places for rounding calculations.
*/
decimalPlaces?: number;
+ /**
+ * Character used as the decimal separator when displaying the value.
+ * Both `.` and `,` are always accepted as input regardless of this setting.
+ * @default .
+ */
+ decimalSeparator?: '.' | ',';
/**
* Minimum allowed value. Disables decrementing below this value and restricts manual input.
*/
@@ -85,8 +92,9 @@ export const NumberField = (props: NumberFieldProps) => {
required,
className,
size,
- inputMode = 'numeric',
+ inputMode,
decimalPlaces,
+ decimalSeparator = '.',
min,
max,
step = 1,
@@ -101,18 +109,36 @@ export const NumberField = (props: NumberFieldProps) => {
input,
} = getCurrentBreakpointProps(props);
+ const resolvedInputMode =
+ inputMode ?? ((decimalPlaces && decimalPlaces > 0) || decimalSeparator === ',' ? 'decimal' : 'numeric');
+
+ const formatNumber = useCallback(
+ (num: number): string => (decimalSeparator === ',' ? String(num).replace('.', ',') : String(num)),
+ [decimalSeparator]
+ );
+
const { getLabel } = useLabels();
const inputRef = useRef(null);
+ const isFocusedRef = useRef(false);
const [inputUpdated, setInputUpdated] = useState('');
const [inputInnerValue, setInputInnerValue] = useState(defaultValue ?? 0);
+ const [displayValue, setDisplayValue] = useState(() =>
+ formatNumber(value !== undefined ? value : defaultValue ?? 0)
+ );
const getCurrentValue = useMemo(
(): number => (onChange && typeof value !== 'undefined' ? value : inputInnerValue),
[onChange, value, inputInnerValue]
);
+ useEffect(() => {
+ if (!isFocusedRef.current && typeof value !== 'undefined') {
+ setDisplayValue(formatNumber(value));
+ }
+ }, [value, formatNumber]);
+
const helperId = helper ? `${id}-helper` : undefined;
const isInvalid = useCallback(
@@ -157,11 +183,30 @@ export const NumberField = (props: NumberFieldProps) => {
updateValueUpdatedLabel(returnValue);
onChange?.(returnValue);
setInputInnerValue(returnValue);
+ setDisplayValue(formatNumber(returnValue));
+ };
+
+ const handleInputChange = ({ currentTarget: { value: rawValue } }: ChangeEvent) => {
+ setDisplayValue(rawValue);
+
+ const normalized = rawValue.replace(',', '.');
+ const parsed = rawValue === '' ? 0 : parseFloat(normalized);
+
+ if (!Number.isNaN(parsed)) {
+ const clamped = forceToLimits(parsed);
+ const rounded = roundValue(clamped);
+ onChange?.(rounded);
+ setInputInnerValue(rounded);
+ }
+ };
+
+ const handleFocus = () => {
+ isFocusedRef.current = true;
};
- const handleInputChange = ({ currentTarget: { value } }: ChangeEvent) => {
- onChange?.(forceToLimits(+value));
- setInputInnerValue(forceToLimits(+value));
+ const handleBlur = () => {
+ isFocusedRef.current = false;
+ setDisplayValue(formatNumber(getCurrentValue));
};
const renderButton = (direction: TDirection) => {
@@ -205,17 +250,20 @@ export const NumberField = (props: NumberFieldProps) => {