Skip to content

Commit 1a8475a

Browse files
Added support for precision prop for Input component (#2487)
* Added support for precision prop for Input component * Jest test * Added proptype * Added precision logic fo onchange * Added storybook description * Handled intial value formatting
1 parent 12710e5 commit 1a8475a

File tree

5 files changed

+148
-18
lines changed

5 files changed

+148
-18
lines changed

src/components/Input.jsx renamed to src/components/Input/index.jsx

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ import React, { useState, forwardRef } from "react";
22

33
import classnames from "classnames";
44
import PropTypes from "prop-types";
5-
import { replace } from "ramda";
65

76
import { useId } from "hooks";
87
import { hyphenize } from "utils";
98

10-
import Label from "./Label";
9+
import {
10+
enforceDecimalPrecision,
11+
formatWithPrecision,
12+
formatWithRejectCharsRegex,
13+
getTrimmedValue,
14+
} from "./utils";
15+
16+
import Label from "../Label";
1117

1218
const SIZES = { small: "small", medium: "medium", large: "large" };
1319

@@ -33,6 +39,7 @@ const Input = forwardRef(
3339
rejectCharsRegex,
3440
onBlur,
3541
disableTrimOnBlur = false,
42+
precision = -1,
3643
...otherProps
3744
},
3845
ref
@@ -43,7 +50,8 @@ const Input = forwardRef(
4350
const errorId = `error_${id}`;
4451
const helpTextId = `helpText_${id}`;
4552

46-
const value = otherProps.value ?? valueInternal ?? "";
53+
const value =
54+
formatWithPrecision(otherProps.value, precision) ?? valueInternal ?? "";
4755

4856
const valueLength = value?.toString().length || 0;
4957
const isCharacterLimitVisible = valueLength >= maxLength * 0.85;
@@ -58,26 +66,27 @@ const Input = forwardRef(
5866

5967
const isMaxLengthPresent = !!maxLength || maxLength === 0;
6068

61-
const handleRegexChange = e => {
62-
const globalRegex = new RegExp(rejectCharsRegex, "g");
63-
e.target.value = replace(globalRegex, "", e.target.value);
64-
onChange(e);
65-
};
66-
67-
const handleChange = rejectCharsRegex ? handleRegexChange : onChange;
68-
69-
const handleTrimmedChangeOnBlur = e => {
70-
if (disableTrimOnBlur || typeof value !== "string") return;
69+
const handleChange = e => {
70+
let formattedValue = formatWithRejectCharsRegex(
71+
e.target.value,
72+
rejectCharsRegex
73+
);
7174

72-
const trimmedValue = value.trim();
73-
if (value === trimmedValue) return;
75+
formattedValue = enforceDecimalPrecision(formattedValue, precision);
7476

75-
e.target.value = trimmedValue;
76-
handleChange(e);
77+
e.target.value = formattedValue;
78+
onChange(e);
7779
};
7880

7981
const handleOnBlur = e => {
80-
handleTrimmedChangeOnBlur(e);
82+
const trimmedValue = getTrimmedValue(value, disableTrimOnBlur);
83+
const formattedValue = formatWithPrecision(trimmedValue, precision);
84+
85+
if (formattedValue !== value) {
86+
e.target.value = formattedValue;
87+
handleChange(e);
88+
}
89+
8190
onBlur?.(e);
8291
};
8392

@@ -180,6 +189,15 @@ Input.propTypes = {
180189
* To specify the type of Input field.
181190
*/
182191
type: PropTypes.string,
192+
/**
193+
* To specify how many decimal places to show in the input.
194+
*
195+
* For example, if precision is 2:
196+
* 10 will be shown as "10.00"
197+
* 10.1 will be shown as "10.10"
198+
* 9.758 will be rounded and shown as "9.76"
199+
*/
200+
precision: PropTypes.number,
183201
/**
184202
* To specify the label props to be passed to the Label component.
185203
*/

src/components/Input/utils.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { replace } from "ramda";
2+
3+
const toFixed = (numStr, precision) => {
4+
const num = Number(numStr);
5+
if (Number.isNaN(num)) return numStr;
6+
7+
return num.toFixed(precision);
8+
};
9+
10+
const isValidNumberString = numStr => {
11+
if (typeof numStr !== "string") return false;
12+
13+
return !Number.isNaN(Number(numStr.trim()));
14+
};
15+
16+
export const formatWithPrecision = (value, precision) => {
17+
if (precision < 0 || !value) return value;
18+
19+
const str = value.toString();
20+
21+
if (isValidNumberString(str)) return toFixed(str, precision);
22+
23+
return str;
24+
};
25+
26+
export const enforceDecimalPrecision = (value, precision) => {
27+
if (precision < 0 || !value) return value;
28+
29+
const valueStr = value.toString();
30+
31+
if (precision === 0) return valueStr.split(".")[0];
32+
33+
const regex = new RegExp(`^\\d*\\.?\\d{0,${precision}}$`);
34+
if (regex.test(valueStr)) return value;
35+
36+
const parts = valueStr.split(".");
37+
if (parts.length === 1) return parts[0];
38+
39+
const [integerPart, decimalPart] = parts;
40+
41+
return `${integerPart}.${decimalPart.substring(0, precision)}`;
42+
};
43+
44+
export const formatWithRejectCharsRegex = (value, rejectCharsRegex) => {
45+
if (!rejectCharsRegex) return value;
46+
47+
const globalRegex = new RegExp(rejectCharsRegex, "g");
48+
49+
return replace(globalRegex, "", value);
50+
};
51+
52+
export const getTrimmedValue = (value, disableTrimOnBlur) => {
53+
if (disableTrimOnBlur || typeof value !== "string") return value;
54+
55+
return value.trim();
56+
};

stories/Components/Input.stories.jsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,21 @@ RejectCharsInputStory.parameters = {
198198
},
199199
};
200200

201+
const PrecisionInputStory = args => (
202+
<Input label={`Input (up to ${args.precision} decimal places)`} {...args} />
203+
);
204+
205+
PrecisionInputStory.storyName = "Precision";
206+
PrecisionInputStory.parameters = {
207+
docs: {
208+
description: {
209+
story: `The prop \`precision\` will accept a number and limit the number of decimal places to the specified value.`,
210+
},
211+
},
212+
};
213+
214+
PrecisionInputStory.args = { precision: 2, type: "number" };
215+
201216
const CSSCustomization = args => <Input {...args} />;
202217

203218
CSSCustomization.storyName = "Input CSS Customization";
@@ -227,6 +242,7 @@ export {
227242
FormikInputStory,
228243
RejectCharsInputStory,
229244
CSSCustomization,
245+
PrecisionInputStory,
230246
};
231247

232248
export default metadata;

tests/Input.test.jsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,43 @@ describe("Input", () => {
158158
await userEvent.type(getByLabelText("label"), "Test");
159159
expect(getByLabelText("label")).toHaveValue("Test");
160160
});
161+
162+
it("should format input value correctly based on precision prop", async () => {
163+
const { getByLabelText, rerender } = render(
164+
<Input label="label" precision={2} />
165+
);
166+
const input = getByLabelText("label");
167+
await userEvent.type(input, "12345.6789");
168+
expect(input).toHaveValue("12345.67");
169+
170+
rerender(<Input label="label" precision={3} />);
171+
await userEvent.clear(input);
172+
await userEvent.type(input, "9876.54321");
173+
expect(input).toHaveValue("9876.543");
174+
175+
rerender(<Input label="label" precision={2} />);
176+
await userEvent.clear(input);
177+
await userEvent.type(input, "45");
178+
await userEvent.tab();
179+
expect(input).toHaveValue("45.00");
180+
181+
rerender(<Input label="label" precision={0} />);
182+
await userEvent.clear(input);
183+
await userEvent.type(input, "45.67");
184+
expect(input).toHaveValue("4567");
185+
186+
rerender(<Input label="label" />);
187+
await userEvent.clear(input);
188+
await userEvent.type(input, "45.677");
189+
expect(input).toHaveValue("45.677");
190+
191+
rerender(<Input label="label" precision={2} value={45.677} />);
192+
expect(input).toHaveValue("45.68");
193+
194+
rerender(<Input label="label" precision={3} value={45.6} />);
195+
expect(input).toHaveValue("45.600");
196+
197+
rerender(<Input label="label" value={45.677} />);
198+
expect(input).toHaveValue("45.677");
199+
});
161200
});

types/Input.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface InputProps
1515
helpText?: string;
1616
className?: string;
1717
nakedInput?: boolean;
18+
precision?: number;
1819
contentSize?: number;
1920
required?: boolean;
2021
labelProps?: LabelProps;

0 commit comments

Comments
 (0)