Skip to content

Commit 2933cf7

Browse files
add numberfield input with locale support
1 parent 5446ae5 commit 2933cf7

File tree

5 files changed

+266
-144
lines changed

5 files changed

+266
-144
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/ui/NumberField/contexts/NumberFieldContext.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import React from 'react';
22

33
export type NumberFieldContextType = {
44
inputValue: number|'';
5-
handleOnChange: (input: number|'') => void;
5+
formattedValue: string;
6+
locale?: string;
7+
handleOnChange: (input: string) => void;
68
handleStep: (opts: { direction: 'increment' | 'decrement'; type: 'small' | 'large' }) => void;
79
id?: string;
810
name?: string;
@@ -14,4 +16,4 @@ export type NumberFieldContextType = {
1416

1517
const NumberFieldContext = React.createContext<NumberFieldContextType | null>(null);
1618

17-
export default NumberFieldContext;
19+
export default NumberFieldContext;

src/components/ui/NumberField/fragments/NumberFieldInput.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const NumberFieldInput = forwardRef<NumberFieldInputElement, NumberFieldInputPro
1313
}
1414
const {
1515
inputValue,
16+
formattedValue,
17+
locale,
1618
handleOnChange,
1719
handleStep,
1820
id,
@@ -44,10 +46,10 @@ const NumberFieldInput = forwardRef<NumberFieldInputElement, NumberFieldInputPro
4446
return (
4547
<input
4648
ref={ref}
47-
type="number"
49+
type="text"
4850
onKeyDown={handleKeyDown}
49-
value={inputValue === '' ? '' : inputValue}
50-
onChange={(e) => { const val = e.target.value; handleOnChange(val === '' ? '' : Number(val)); }}
51+
value={locale ? formattedValue : (inputValue === '' ? '' : inputValue)}
52+
onChange={(e) => handleOnChange(e.target.value)}
5153
id={id}
5254
name={name}
5355
disabled={disabled}
@@ -60,4 +62,4 @@ const NumberFieldInput = forwardRef<NumberFieldInputElement, NumberFieldInputPro
6062

6163
NumberFieldInput.displayName = 'NumberFieldInput';
6264

63-
export default NumberFieldInput;
65+
export default NumberFieldInput;
Lines changed: 154 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,178 @@
1-
import React, { forwardRef, ElementRef, ComponentPropsWithoutRef } from 'react';
2-
import { useControllableState } from '~/core/hooks/useControllableState';
3-
import NumberFieldContext from '../contexts/NumberFieldContext';
4-
import { customClassSwitcher } from '~/core';
5-
import clsx from 'clsx';
1+
import React, { forwardRef, ElementRef, ComponentPropsWithoutRef } from "react";
2+
import { useControllableState } from "~/core/hooks/useControllableState";
3+
import NumberFieldContext from "../contexts/NumberFieldContext";
4+
import { customClassSwitcher } from "~/core";
5+
import clsx from "clsx";
66

7-
const COMPONENT_NAME = 'NumberField';
7+
const COMPONENT_NAME = "NumberField";
88

9-
export type NumberFieldRootElement = ElementRef<'div'>;
9+
export type NumberFieldRootElement = ElementRef<"div">;
1010
export type NumberFieldRootProps = {
11-
name?: string
12-
defaultValue?: number | ''
13-
value?: number | ''
14-
onValueChange?: (value: number | '') => void
15-
step?: number
16-
largeStep?: number
17-
min?: number
18-
max?: number
19-
disabled?: boolean
20-
readOnly?: boolean
21-
required?: boolean
22-
} & ComponentPropsWithoutRef<'div'>;
23-
24-
const NumberFieldRoot = forwardRef<NumberFieldRootElement, NumberFieldRootProps>(({ children, name, defaultValue = '', value, onValueChange, largeStep, step, min, max, disabled, readOnly, required, id, className, ...props }, ref) => {
11+
name?: string;
12+
defaultValue?: number | "";
13+
value?: number | "";
14+
onValueChange?: (value: number | "") => void;
15+
step?: number;
16+
largeStep?: number;
17+
min?: number;
18+
max?: number;
19+
disabled?: boolean;
20+
readOnly?: boolean;
21+
required?: boolean;
22+
locale?: string;
23+
} & ComponentPropsWithoutRef<"div">;
24+
25+
const NumberFieldRoot = forwardRef<
26+
NumberFieldRootElement,
27+
NumberFieldRootProps
28+
>(
29+
(
30+
{
31+
children,
32+
name,
33+
defaultValue = "",
34+
value,
35+
onValueChange,
36+
largeStep,
37+
step = 1,
38+
min,
39+
max,
40+
disabled,
41+
readOnly,
42+
required,
43+
id,
44+
className,
45+
locale,
46+
...props
47+
},
48+
ref,
49+
) => {
2550
const rootClass = customClassSwitcher(className, COMPONENT_NAME);
26-
const [inputValue, setInputValue] = useControllableState<number | ''>(
27-
value,
28-
defaultValue,
29-
onValueChange);
30-
31-
const handleOnChange = (input: number| '') => {
32-
if (input === '') {
33-
setInputValue('');
34-
return;
35-
}
36-
if (max !== undefined && input > max) {
37-
setInputValue(max);
38-
return;
39-
}
51+
const [inputValue, setInputValue] = useControllableState<number | "">(
52+
value,
53+
defaultValue,
54+
onValueChange,
55+
);
4056

41-
if (min !== undefined && input < min) {
42-
setInputValue(min);
43-
return;
44-
}
57+
const getDecimalSeparator = (locale: string) => {
58+
const parts = new Intl.NumberFormat(locale).formatToParts(1234.5);
59+
return parts.find((part) => part.type === "decimal")?.value || ".";
60+
};
4561

46-
setInputValue(input);
62+
const handleOnChange = (val: string) => {
63+
if (val === "") {
64+
setInputValue("");
65+
return;
66+
}
67+
68+
const decimal = getDecimalSeparator(locale || "en-US");
69+
const regex = new RegExp(`[^0-9${decimal}]`, "g");
70+
const cleaned = val.replace(regex, "");
71+
const normalized = cleaned.replace(decimal, ".");
72+
const numericValue = parseFloat(normalized);
73+
74+
if (isNaN(numericValue)) {
75+
return;
76+
}
77+
78+
if (max !== undefined && numericValue > max) {
79+
setInputValue(max);
80+
return;
81+
}
82+
83+
if (min !== undefined && numericValue < min) {
84+
setInputValue(min);
85+
return;
86+
}
87+
88+
setInputValue(numericValue);
4789
};
4890
const applyStep = (amount: number) => {
49-
setInputValue((prev) => {
50-
let temp = prev;
51-
if (temp === '') {
52-
if (min !== undefined) {
53-
temp = min;
54-
} else {
55-
temp = -1;
56-
}
57-
}
58-
const nextValue = temp + amount;
59-
60-
if (max !== undefined && nextValue > max) {
61-
return max;
62-
}
63-
64-
if (min !== undefined && nextValue < min) {
65-
return min;
66-
}
67-
68-
return nextValue;
69-
});
70-
};
91+
setInputValue((prev) => {
92+
let temp = prev;
93+
if (temp === "") {
94+
if (min !== undefined) {
95+
temp = min;
96+
} else {
97+
temp = 0;
98+
}
99+
}
100+
const nextValue = temp + amount;
71101

72-
const handleStep = ({ type, direction } : {type: 'small'| 'large', direction: 'increment' | 'decrement' }) => {
73-
let amount = 0;
74-
75-
switch (type) {
76-
case 'small':
77-
if (!step) return;
78-
amount = step;
79-
break;
80-
case 'large':
81-
if (!largeStep) return;
82-
amount = largeStep;
83-
break;
102+
if (max !== undefined && nextValue > max) {
103+
return max;
84104
}
85105

86-
if (direction === 'decrement') {
87-
amount *= -1;
106+
if (min !== undefined && nextValue < min) {
107+
return min;
88108
}
89109

90-
applyStep(amount);
110+
return nextValue;
111+
});
91112
};
92113

114+
const handleStep = ({
115+
type,
116+
direction,
117+
}: {
118+
type: "small" | "large";
119+
direction: "increment" | "decrement";
120+
}) => {
121+
let amount = 0;
122+
123+
switch (type) {
124+
case "small":
125+
if (!step) {
126+
return;
127+
}
128+
amount = step;
129+
break;
130+
case "large":
131+
if (!largeStep) return;
132+
amount = largeStep;
133+
break;
134+
}
135+
136+
if (direction === "decrement") {
137+
amount *= -1;
138+
}
139+
140+
applyStep(amount);
141+
};
142+
143+
const formattedValue = new Intl.NumberFormat(locale ? locale : "en-US", {
144+
maximumFractionDigits: 20,
145+
}).format(inputValue === "" ? 0 : inputValue);
146+
93147
const contextValues = {
94-
inputValue,
95-
handleOnChange,
96-
handleStep,
97-
id,
98-
name,
99-
disabled,
100-
readOnly,
101-
required,
102-
rootClass
148+
inputValue,
149+
formattedValue,
150+
locale,
151+
handleOnChange,
152+
handleStep,
153+
id,
154+
name,
155+
disabled,
156+
readOnly,
157+
required,
158+
rootClass,
103159
};
104160

105161
return (
106-
<div ref={ref} className={clsx(`${rootClass}-root`, className)} {...props}>
107-
<NumberFieldContext.Provider value={contextValues}>
108-
{children}
109-
</NumberFieldContext.Provider>
110-
</div>
162+
<div
163+
ref={ref}
164+
className={clsx(`${rootClass}-root`, className)}
165+
{...props}
166+
>
167+
<NumberFieldContext.Provider value={contextValues}>
168+
{children}
169+
</NumberFieldContext.Provider>
170+
</div>
111171
);
112-
});
172+
},
173+
);
113174

114175
NumberFieldRoot.displayName = COMPONENT_NAME;
115176

116177
export default NumberFieldRoot;
178+

0 commit comments

Comments
 (0)