@phsa.tec/design-system-react 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/dataInput/Input/components/NumberInput/__tests__/number-input.test.tsx +175 -0
- package/src/components/dataInput/Input/components/NumberInput/number-input.stories.tsx +1 -1
- package/src/components/dataInput/Input/components/NumberInput/number-input.tsx +61 -59
package/package.json
CHANGED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { NumberInput } from "../number-input";
|
|
5
|
+
|
|
6
|
+
describe("NumberInput", () => {
|
|
7
|
+
it("should render label", () => {
|
|
8
|
+
render(<NumberInput label="test" data-testid="number-input" />);
|
|
9
|
+
const label = screen.getByTestId("number-input-label");
|
|
10
|
+
expect(label).toBeInTheDocument();
|
|
11
|
+
expect(label).toHaveTextContent("test");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should render error", () => {
|
|
15
|
+
render(<NumberInput error="test" data-testid="number-input" />);
|
|
16
|
+
const error = screen.getByTestId("number-input-error-label");
|
|
17
|
+
expect(error).toBeInTheDocument();
|
|
18
|
+
expect(error).toHaveTextContent("test");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should be disabled", () => {
|
|
22
|
+
render(<NumberInput disabled data-testid="number-input" />);
|
|
23
|
+
const input = screen.getByTestId("number-input");
|
|
24
|
+
expect(input).toBeDisabled();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should be required", () => {
|
|
28
|
+
render(<NumberInput required data-testid="number-input" label="test" />);
|
|
29
|
+
const label = screen.getByTestId("number-input-label");
|
|
30
|
+
expect(label).toHaveTextContent("test *");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should handle placeholder", () => {
|
|
34
|
+
render(
|
|
35
|
+
<NumberInput data-testid="number-input" placeholder="Digite um número" />
|
|
36
|
+
);
|
|
37
|
+
const input = screen.getByTestId("number-input");
|
|
38
|
+
|
|
39
|
+
expect(input).toHaveAttribute("placeholder", "Digite um número");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should handle name attribute", () => {
|
|
43
|
+
render(<NumberInput data-testid="number-input" name="amount" />);
|
|
44
|
+
const input = screen.getByTestId("number-input");
|
|
45
|
+
|
|
46
|
+
expect(input).toHaveAttribute("name", "amount");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should format numbers with thousands separator", async () => {
|
|
50
|
+
const user = userEvent.setup();
|
|
51
|
+
render(<NumberInput data-testid="number-input" thousandSeparator={true} />);
|
|
52
|
+
const input = screen.getByTestId("number-input");
|
|
53
|
+
|
|
54
|
+
await user.type(input, "1234567");
|
|
55
|
+
|
|
56
|
+
expect(input).toHaveValue("1,234,567");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should format currency values", async () => {
|
|
60
|
+
const user = userEvent.setup();
|
|
61
|
+
render(
|
|
62
|
+
<NumberInput
|
|
63
|
+
data-testid="number-input"
|
|
64
|
+
thousandSeparator={true}
|
|
65
|
+
prefix="R$ "
|
|
66
|
+
decimalScale={2}
|
|
67
|
+
fixedDecimalScale={true}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
const input = screen.getByTestId("number-input");
|
|
71
|
+
|
|
72
|
+
await user.type(input, "1234.56");
|
|
73
|
+
|
|
74
|
+
expect(input).toHaveValue("R$ 1,234.56");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should limit decimal places", async () => {
|
|
78
|
+
const user = userEvent.setup();
|
|
79
|
+
render(<NumberInput data-testid="number-input" decimalScale={2} />);
|
|
80
|
+
const input = screen.getByTestId("number-input");
|
|
81
|
+
|
|
82
|
+
await user.type(input, "123.456789");
|
|
83
|
+
|
|
84
|
+
expect(input).toHaveValue("123.45");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should handle percentage format", async () => {
|
|
88
|
+
const user = userEvent.setup();
|
|
89
|
+
render(
|
|
90
|
+
<NumberInput data-testid="number-input" suffix="%" decimalScale={2} />
|
|
91
|
+
);
|
|
92
|
+
const input = screen.getByTestId("number-input");
|
|
93
|
+
|
|
94
|
+
await user.type(input, "15.75");
|
|
95
|
+
|
|
96
|
+
expect(input).toHaveValue("15.75%");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should call onChange with synthetic event", async () => {
|
|
100
|
+
const user = userEvent.setup();
|
|
101
|
+
const mockOnChange = jest.fn();
|
|
102
|
+
render(
|
|
103
|
+
<NumberInput
|
|
104
|
+
data-testid="number-input"
|
|
105
|
+
onChange={mockOnChange}
|
|
106
|
+
name="testInput"
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
const input = screen.getByTestId("number-input");
|
|
110
|
+
|
|
111
|
+
await user.type(input, "123");
|
|
112
|
+
|
|
113
|
+
expect(mockOnChange).toHaveBeenCalled();
|
|
114
|
+
const lastCall =
|
|
115
|
+
mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
|
|
116
|
+
expect(lastCall.target.name).toBe("testInput");
|
|
117
|
+
expect(lastCall.target.value).toBe("123");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should call onValueChange with formatted data", async () => {
|
|
121
|
+
const user = userEvent.setup();
|
|
122
|
+
const mockOnValueChange = jest.fn();
|
|
123
|
+
render(
|
|
124
|
+
<NumberInput
|
|
125
|
+
data-testid="number-input"
|
|
126
|
+
onValueChange={mockOnValueChange}
|
|
127
|
+
thousandSeparator={true}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
const input = screen.getByTestId("number-input");
|
|
131
|
+
|
|
132
|
+
await user.type(input, "1234");
|
|
133
|
+
|
|
134
|
+
expect(mockOnValueChange).toHaveBeenCalled();
|
|
135
|
+
const lastCall =
|
|
136
|
+
mockOnValueChange.mock.calls[mockOnValueChange.mock.calls.length - 1][0];
|
|
137
|
+
expect(lastCall.value).toBe("1234");
|
|
138
|
+
expect(lastCall.floatValue).toBe(1234);
|
|
139
|
+
expect(lastCall.formattedValue).toBe("1,234");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should handle negative numbers", async () => {
|
|
143
|
+
const user = userEvent.setup();
|
|
144
|
+
render(<NumberInput data-testid="number-input" allowNegative={true} />);
|
|
145
|
+
const input = screen.getByTestId("number-input");
|
|
146
|
+
|
|
147
|
+
await user.type(input, "-123");
|
|
148
|
+
|
|
149
|
+
expect(input).toHaveValue("-123");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should prevent negative numbers when allowNegative is false", async () => {
|
|
153
|
+
const user = userEvent.setup();
|
|
154
|
+
render(<NumberInput data-testid="number-input" allowNegative={false} />);
|
|
155
|
+
const input = screen.getByTestId("number-input");
|
|
156
|
+
|
|
157
|
+
await user.type(input, "-123");
|
|
158
|
+
|
|
159
|
+
expect(input).toHaveValue("123");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should handle controlled value", () => {
|
|
163
|
+
render(<NumberInput data-testid="number-input" value="1000" />);
|
|
164
|
+
const input = screen.getByTestId("number-input");
|
|
165
|
+
|
|
166
|
+
expect(input).toHaveValue("1000");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should handle empty value", () => {
|
|
170
|
+
render(<NumberInput data-testid="number-input" value="" />);
|
|
171
|
+
const input = screen.getByTestId("number-input");
|
|
172
|
+
|
|
173
|
+
expect(input).toHaveValue("");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -1,68 +1,70 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
NumericFormat,
|
|
6
|
+
NumericFormatProps,
|
|
7
|
+
SourceInfo,
|
|
8
|
+
} from "react-number-format";
|
|
9
|
+
import { InputBase } from "../InputBase";
|
|
6
10
|
import { Input } from "../../../../ui/input";
|
|
11
|
+
import { InputProps } from "../Input/types";
|
|
12
|
+
import { useCallback, useMemo } from "react";
|
|
13
|
+
import { useConditionalController } from "@/hooks/use-conditional-controller";
|
|
7
14
|
|
|
8
|
-
export type NumberInputProps = Omit<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
};
|
|
15
|
+
export type NumberInputProps = Omit<NumericFormatProps, "onChange"> &
|
|
16
|
+
InputProps;
|
|
17
|
+
|
|
18
|
+
export const NumberInput = (props: NumberInputProps) => {
|
|
19
|
+
const formData = useConditionalController({
|
|
20
|
+
name: props.name || "",
|
|
21
|
+
withoutForm: props.withoutForm,
|
|
22
|
+
});
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
className,
|
|
25
|
-
withoutForm,
|
|
26
|
-
onChange,
|
|
27
|
-
"data-testid": testId,
|
|
28
|
-
component,
|
|
29
|
-
...inputProps
|
|
30
|
-
} = props;
|
|
24
|
+
const inputProps = useMemo(() => {
|
|
25
|
+
return {
|
|
26
|
+
...formData,
|
|
27
|
+
...props,
|
|
28
|
+
};
|
|
29
|
+
}, [formData, props]);
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
const onValueChange = useCallback(
|
|
32
|
+
(
|
|
33
|
+
data: {
|
|
34
|
+
value: string;
|
|
35
|
+
floatValue: number | undefined;
|
|
36
|
+
formattedValue: string;
|
|
37
|
+
},
|
|
38
|
+
sourceInfo: SourceInfo
|
|
39
|
+
) => {
|
|
40
|
+
const syntheticEvent = {
|
|
41
|
+
target: {
|
|
42
|
+
value: data.formattedValue,
|
|
43
|
+
name: props.name,
|
|
44
|
+
},
|
|
45
|
+
currentTarget: {
|
|
46
|
+
value: data.formattedValue,
|
|
47
|
+
name: props.name,
|
|
48
|
+
},
|
|
49
|
+
} as React.ChangeEvent<HTMLInputElement>;
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
>
|
|
43
|
-
{({ onChange: onBaseChange, value }) => (
|
|
44
|
-
<div
|
|
45
|
-
className="flex w-full gap-3"
|
|
46
|
-
data-testid={`number-input-wrapper-${baseTestId}`}
|
|
47
|
-
>
|
|
48
|
-
<NumericFormat
|
|
49
|
-
value={value as number}
|
|
50
|
-
customInput={Input}
|
|
51
|
-
getInputRef={ref}
|
|
52
|
-
onValueChange={({ floatValue }) => {
|
|
53
|
-
const numberValue = floatValue;
|
|
54
|
-
onBaseChange?.(numberValue);
|
|
55
|
-
onChange?.(numberValue as number);
|
|
56
|
-
}}
|
|
57
|
-
data-testid={`${baseTestId}-number-input`}
|
|
58
|
-
{...inputProps}
|
|
59
|
-
/>
|
|
60
|
-
{component}
|
|
61
|
-
</div>
|
|
62
|
-
)}
|
|
63
|
-
</InputBase>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
);
|
|
51
|
+
props.onChange?.(syntheticEvent);
|
|
52
|
+
props.onValueChange?.(data, sourceInfo);
|
|
53
|
+
if (formData.onChange) {
|
|
54
|
+
formData.onChange(data.formattedValue);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[props, formData]
|
|
58
|
+
);
|
|
67
59
|
|
|
68
|
-
|
|
60
|
+
return (
|
|
61
|
+
<InputBase {...props}>
|
|
62
|
+
<NumericFormat
|
|
63
|
+
{...inputProps}
|
|
64
|
+
customInput={Input}
|
|
65
|
+
value={props.value}
|
|
66
|
+
onValueChange={onValueChange}
|
|
67
|
+
/>
|
|
68
|
+
</InputBase>
|
|
69
|
+
);
|
|
70
|
+
};
|