@phsa.tec/design-system-react 0.1.4 → 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/.github/workflows/deploy-storybook.yml +21 -0
- package/package.json +4 -2
- package/src/components/dataInput/Input/components/Input/index.tsx +14 -9
- package/src/components/dataInput/Input/components/Input/types.ts +2 -0
- package/src/components/dataInput/Input/components/MaskInput/__tests__/mask-input.test.tsx +11 -10
- package/src/components/dataInput/Input/components/MaskInput/mask-input.stories.tsx +25 -9
- package/src/components/dataInput/Input/components/MaskInput/mask-input.tsx +47 -28
- package/src/components/dataInput/Input/components/MultipleInput/MultipleInput.tsx +91 -22
- package/src/components/dataInput/Input/components/MultipleInput/__tests__/multiple-input.test.tsx +152 -0
- package/src/components/dataInput/Input/components/MultipleInput/multiple-input.stories.tsx +38 -13
- 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/src/hooks/use-mask.tsx +116 -0
- package/src/introduction.mdx +132 -0
|
@@ -16,8 +16,28 @@ concurrency:
|
|
|
16
16
|
cancel-in-progress: false
|
|
17
17
|
|
|
18
18
|
jobs:
|
|
19
|
+
test:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Setup Node
|
|
26
|
+
uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: "20"
|
|
29
|
+
cache: "yarn"
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: yarn install --frozen-lockfile
|
|
33
|
+
|
|
34
|
+
- name: Run tests
|
|
35
|
+
run: yarn test
|
|
36
|
+
|
|
19
37
|
build:
|
|
20
38
|
runs-on: ubuntu-latest
|
|
39
|
+
needs: test
|
|
40
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
|
21
41
|
steps:
|
|
22
42
|
- name: Checkout
|
|
23
43
|
uses: actions/checkout@v4
|
|
@@ -48,6 +68,7 @@ jobs:
|
|
|
48
68
|
url: ${{ steps.deployment.outputs.page_url }}
|
|
49
69
|
runs-on: ubuntu-latest
|
|
50
70
|
needs: build
|
|
71
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
|
51
72
|
steps:
|
|
52
73
|
- name: Deploy to GitHub Pages
|
|
53
74
|
id: deployment
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phsa.tec/design-system-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"storybook": "storybook dev -p 6006",
|
|
12
12
|
"build-storybook": "storybook build",
|
|
13
13
|
"deploy-storybook": "storybook build && touch storybook-static/.nojekyll",
|
|
14
|
-
"test": "jest"
|
|
14
|
+
"test": "jest",
|
|
15
|
+
"prepare": "husky install"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
17
18
|
"@hookform/resolvers": "^3.9.1",
|
|
@@ -75,6 +76,7 @@
|
|
|
75
76
|
"eslint": "^8",
|
|
76
77
|
"eslint-config-next": "15.0.3",
|
|
77
78
|
"eslint-plugin-storybook": "^9.0.15",
|
|
79
|
+
"husky": "^9.1.7",
|
|
78
80
|
"jest": "^30.0.4",
|
|
79
81
|
"jest-environment-jsdom": "^29.7.0",
|
|
80
82
|
"postcss": "^8",
|
|
@@ -8,6 +8,8 @@ import { cn } from "@/lib/utils";
|
|
|
8
8
|
export const Input = ({
|
|
9
9
|
"data-testid": dataTestId,
|
|
10
10
|
withoutForm = false,
|
|
11
|
+
extraElement,
|
|
12
|
+
containerClassName,
|
|
11
13
|
...props
|
|
12
14
|
}: InputProps) => {
|
|
13
15
|
const formData = useConditionalController({
|
|
@@ -29,15 +31,18 @@ export const Input = ({
|
|
|
29
31
|
required={props.required}
|
|
30
32
|
data-testid={dataTestId}
|
|
31
33
|
>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
<div className={cn("flex items-center gap-2", containerClassName)}>
|
|
35
|
+
<InputUI
|
|
36
|
+
{...inputProps}
|
|
37
|
+
data-testid={dataTestId}
|
|
38
|
+
className={cn(
|
|
39
|
+
props.className,
|
|
40
|
+
props.error &&
|
|
41
|
+
"border-destructive focus:border-destructive focus-visible:ring-0"
|
|
42
|
+
)}
|
|
43
|
+
/>
|
|
44
|
+
{extraElement}
|
|
45
|
+
</div>
|
|
41
46
|
</InputBase>
|
|
42
47
|
);
|
|
43
48
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import "@testing-library/jest-dom";
|
|
2
|
-
import {
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
3
4
|
import { MaskInput } from "../mask-input";
|
|
4
5
|
|
|
5
6
|
describe("MaskInput", () => {
|
|
6
7
|
it("should render label", () => {
|
|
7
8
|
render(
|
|
8
|
-
<MaskInput label="test" data-testid="mask-input" mask="
|
|
9
|
+
<MaskInput label="test" data-testid="mask-input" mask="999.999.999-99" />
|
|
9
10
|
);
|
|
10
11
|
const label = screen.getByTestId("mask-input-label");
|
|
11
12
|
expect(label).toBeInTheDocument();
|
|
@@ -14,7 +15,7 @@ describe("MaskInput", () => {
|
|
|
14
15
|
|
|
15
16
|
it("should render error", () => {
|
|
16
17
|
render(
|
|
17
|
-
<MaskInput error="test" data-testid="mask-input" mask="
|
|
18
|
+
<MaskInput error="test" data-testid="mask-input" mask="999.999.999-99" />
|
|
18
19
|
);
|
|
19
20
|
const error = screen.getByTestId("mask-input-error-label");
|
|
20
21
|
expect(error).toBeInTheDocument();
|
|
@@ -23,7 +24,7 @@ describe("MaskInput", () => {
|
|
|
23
24
|
|
|
24
25
|
it("should be disabled", () => {
|
|
25
26
|
render(
|
|
26
|
-
<MaskInput disabled data-testid="mask-input" mask="
|
|
27
|
+
<MaskInput disabled data-testid="mask-input" mask="999.999.999-99" />
|
|
27
28
|
);
|
|
28
29
|
const input = screen.getByTestId("mask-input");
|
|
29
30
|
expect(input).toBeDisabled();
|
|
@@ -35,7 +36,7 @@ describe("MaskInput", () => {
|
|
|
35
36
|
required
|
|
36
37
|
data-testid="mask-input"
|
|
37
38
|
label="test"
|
|
38
|
-
mask="
|
|
39
|
+
mask="999.999.999-99"
|
|
39
40
|
/>
|
|
40
41
|
);
|
|
41
42
|
const label = screen.getByTestId("mask-input-label");
|
|
@@ -43,11 +44,11 @@ describe("MaskInput", () => {
|
|
|
43
44
|
});
|
|
44
45
|
|
|
45
46
|
it("should apply mask to input value", async () => {
|
|
46
|
-
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
render(<MaskInput data-testid="mask-input" mask="999.999.999-99" />);
|
|
47
49
|
const input = screen.getByTestId("mask-input");
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
fireEvent.input(input, { target: { value: "12345678901" } });
|
|
51
|
+
await user.type(input, "12345678901");
|
|
51
52
|
|
|
52
53
|
expect(input).toHaveValue("123.456.789-01");
|
|
53
54
|
});
|
|
@@ -56,7 +57,7 @@ describe("MaskInput", () => {
|
|
|
56
57
|
render(
|
|
57
58
|
<MaskInput
|
|
58
59
|
data-testid="mask-input"
|
|
59
|
-
mask="
|
|
60
|
+
mask="999.999.999-99"
|
|
60
61
|
placeholder="Digite seu CPF"
|
|
61
62
|
/>
|
|
62
63
|
);
|
|
@@ -67,7 +68,7 @@ describe("MaskInput", () => {
|
|
|
67
68
|
|
|
68
69
|
it("should handle name attribute", () => {
|
|
69
70
|
render(
|
|
70
|
-
<MaskInput data-testid="mask-input" mask="
|
|
71
|
+
<MaskInput data-testid="mask-input" mask="999.999.999-99" name="cpf" />
|
|
71
72
|
);
|
|
72
73
|
const input = screen.getByTestId("mask-input");
|
|
73
74
|
|
|
@@ -19,31 +19,47 @@ export const Default: Story = {
|
|
|
19
19
|
args: {
|
|
20
20
|
placeholder: "99999-999",
|
|
21
21
|
label: "CEP",
|
|
22
|
-
mask: "
|
|
22
|
+
mask: "99999-999",
|
|
23
23
|
},
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export const CPF: Story = {
|
|
27
27
|
args: {
|
|
28
|
-
placeholder: "
|
|
28
|
+
placeholder: "999.999.999-99",
|
|
29
29
|
label: "CPF",
|
|
30
|
-
mask: "
|
|
30
|
+
mask: "999.999.999-99",
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
export const Phone: Story = {
|
|
35
35
|
args: {
|
|
36
|
-
placeholder: "(
|
|
36
|
+
placeholder: "(99) 99999-9999",
|
|
37
37
|
label: "Telefone",
|
|
38
|
-
mask: "(
|
|
38
|
+
mask: "(99) 99999-9999",
|
|
39
39
|
},
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
export const Date: Story = {
|
|
43
43
|
args: {
|
|
44
|
-
placeholder: "
|
|
44
|
+
placeholder: "99/99/9999",
|
|
45
45
|
label: "Data",
|
|
46
|
-
mask: "
|
|
46
|
+
mask: "99/99/9999",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const LettersExample: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
placeholder: "AAA-9999",
|
|
53
|
+
label: "Código com Letras",
|
|
54
|
+
mask: "AAA-9999",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const MixedExample: Story = {
|
|
59
|
+
args: {
|
|
60
|
+
placeholder: "A99-AAA-999",
|
|
61
|
+
label: "Código Misto",
|
|
62
|
+
mask: "A99-AAA-999",
|
|
47
63
|
},
|
|
48
64
|
};
|
|
49
65
|
|
|
@@ -60,8 +76,8 @@ export const WithForm = () => {
|
|
|
60
76
|
<MaskInput
|
|
61
77
|
name="cpf"
|
|
62
78
|
label="CPF"
|
|
63
|
-
placeholder="
|
|
64
|
-
mask="
|
|
79
|
+
placeholder="999.999.999-99"
|
|
80
|
+
mask="999.999.999-99"
|
|
65
81
|
/>
|
|
66
82
|
</form>
|
|
67
83
|
</Form>
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { useIMask, ReactMaskOpts } from "react-imask";
|
|
4
3
|
import { Input } from "../Input";
|
|
5
4
|
import { useConditionalController } from "@/hooks/use-conditional-controller";
|
|
6
5
|
import { InputProps } from "../Input/types";
|
|
6
|
+
import { useMask } from "@/hooks/use-mask";
|
|
7
|
+
import { useRef } from "react";
|
|
7
8
|
|
|
8
|
-
export type MaskInputProps =
|
|
9
|
+
export type MaskInputProps = {
|
|
10
|
+
mask?: string;
|
|
11
|
+
} & InputProps;
|
|
9
12
|
|
|
10
13
|
export const MaskInput = ({
|
|
11
14
|
"data-testid": dataTestId,
|
|
12
15
|
withoutForm = false,
|
|
16
|
+
extraElement,
|
|
17
|
+
mask = "",
|
|
13
18
|
...props
|
|
14
19
|
}: MaskInputProps) => {
|
|
15
20
|
const formData = useConditionalController({
|
|
@@ -17,38 +22,52 @@ export const MaskInput = ({
|
|
|
17
22
|
withoutForm,
|
|
18
23
|
});
|
|
19
24
|
|
|
20
|
-
const {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} = props;
|
|
29
|
-
|
|
30
|
-
const { ref, value } = useIMask({
|
|
31
|
-
...imaskProps,
|
|
32
|
-
});
|
|
25
|
+
const inputProps = React.useMemo(() => {
|
|
26
|
+
return {
|
|
27
|
+
...formData,
|
|
28
|
+
...props,
|
|
29
|
+
};
|
|
30
|
+
}, [formData, props]);
|
|
31
|
+
|
|
32
|
+
const { applyMask } = useMask({ mask });
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
35
|
+
|
|
36
|
+
const applyMaskToInput = React.useCallback(
|
|
37
|
+
(value: string) => {
|
|
38
|
+
if (inputRef.current && mask) {
|
|
39
|
+
const maskedValue = applyMask(value);
|
|
40
|
+
inputRef.current.value = maskedValue;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
[applyMask, mask]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const handleChange = React.useCallback(
|
|
47
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
48
|
+
const maskedValue = mask ? applyMask(e.target.value) : e.target.value;
|
|
49
|
+
|
|
50
|
+
const response = {
|
|
51
|
+
...e,
|
|
52
|
+
target: {
|
|
53
|
+
...e.target,
|
|
54
|
+
value: maskedValue,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
applyMaskToInput(maskedValue);
|
|
58
|
+
return inputProps?.onChange?.(response);
|
|
59
|
+
},
|
|
60
|
+
[applyMask, inputProps, mask, applyMaskToInput]
|
|
61
|
+
);
|
|
39
62
|
|
|
40
63
|
return (
|
|
41
64
|
<Input
|
|
42
|
-
{...
|
|
43
|
-
ref={
|
|
65
|
+
{...inputProps}
|
|
66
|
+
ref={inputRef}
|
|
67
|
+
onChange={handleChange}
|
|
68
|
+
extraElement={extraElement}
|
|
44
69
|
data-testid={dataTestId}
|
|
45
70
|
withoutForm={true}
|
|
46
|
-
label={label}
|
|
47
|
-
error={error}
|
|
48
|
-
required={required}
|
|
49
|
-
className={className}
|
|
50
|
-
placeholder={placeholder}
|
|
51
|
-
name={name}
|
|
52
71
|
/>
|
|
53
72
|
);
|
|
54
73
|
};
|
|
@@ -1,36 +1,105 @@
|
|
|
1
1
|
import { Button } from "../../../../actions/Button";
|
|
2
|
-
import { Input, InputProps } from "../Input";
|
|
3
|
-
import { MultipleInputBase } from "./MultipleInputBase";
|
|
4
2
|
import { Icon } from "../../../../dataDisplay/Icon";
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
|
+
import { useConditionalController } from "@/hooks/use-conditional-controller";
|
|
5
|
+
import { MaskInput, MaskInputProps } from "../MaskInput";
|
|
5
6
|
|
|
6
|
-
export type MultipleInputProps =
|
|
7
|
+
export type MultipleInputProps = MaskInputProps & {
|
|
7
8
|
data?: string[];
|
|
8
|
-
|
|
9
|
+
onAdd?: (data: string) => void;
|
|
10
|
+
onRemove?: (position: number) => void;
|
|
9
11
|
name: string;
|
|
12
|
+
defaultValue?: string;
|
|
13
|
+
"data-testid"?: string;
|
|
10
14
|
};
|
|
11
15
|
|
|
12
16
|
export const MultipleInput = ({
|
|
13
17
|
data = [],
|
|
14
|
-
|
|
18
|
+
onAdd = () => {},
|
|
19
|
+
defaultValue = "",
|
|
20
|
+
onRemove = () => {},
|
|
21
|
+
withoutForm = false,
|
|
15
22
|
...props
|
|
16
23
|
}: MultipleInputProps) => {
|
|
24
|
+
const [inputValue, setInputValue] = useState(defaultValue);
|
|
25
|
+
|
|
26
|
+
const formData = useConditionalController({
|
|
27
|
+
name: props.name || "",
|
|
28
|
+
withoutForm,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const inputItems = useMemo(() => {
|
|
32
|
+
if (formData?.value) {
|
|
33
|
+
return formData.value;
|
|
34
|
+
}
|
|
35
|
+
return data;
|
|
36
|
+
}, [data, formData]);
|
|
37
|
+
|
|
38
|
+
const onAddData = useCallback(() => {
|
|
39
|
+
const trimmedValue = inputValue.trim();
|
|
40
|
+
if (trimmedValue) {
|
|
41
|
+
if (formData?.onChange) {
|
|
42
|
+
formData.onChange([...inputItems, trimmedValue]);
|
|
43
|
+
} else {
|
|
44
|
+
onAdd(trimmedValue);
|
|
45
|
+
}
|
|
46
|
+
setInputValue("");
|
|
47
|
+
}
|
|
48
|
+
}, [inputValue, formData, onAdd, inputItems]);
|
|
49
|
+
|
|
50
|
+
const onRemoveData = useCallback(
|
|
51
|
+
(index: number) => {
|
|
52
|
+
if (formData?.onChange) {
|
|
53
|
+
const newData = inputItems.filter(
|
|
54
|
+
(_: string, i: number) => i !== index
|
|
55
|
+
);
|
|
56
|
+
formData.onChange(newData);
|
|
57
|
+
} else {
|
|
58
|
+
onRemove(index);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[formData, inputItems, onRemove]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const renderItens = useCallback(() => {
|
|
65
|
+
return inputItems?.map((item: string, index: number) => {
|
|
66
|
+
return (
|
|
67
|
+
<div key={item} className="flex justify-between">
|
|
68
|
+
{item}
|
|
69
|
+
<Button
|
|
70
|
+
variant="ghost"
|
|
71
|
+
className="text-destructive"
|
|
72
|
+
size={"icon"}
|
|
73
|
+
onClick={() => onRemoveData(index)}
|
|
74
|
+
>
|
|
75
|
+
<Icon name="MdDelete" />
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
}, [inputItems, onRemoveData]);
|
|
81
|
+
|
|
82
|
+
const extraElement = useMemo(
|
|
83
|
+
() => (
|
|
84
|
+
<Button type="button" onClick={onAddData} disabled={!inputValue}>
|
|
85
|
+
<Icon name="MdAdd" />
|
|
86
|
+
</Button>
|
|
87
|
+
),
|
|
88
|
+
[onAddData, inputValue]
|
|
89
|
+
);
|
|
90
|
+
|
|
17
91
|
return (
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
</Button>
|
|
31
|
-
}
|
|
32
|
-
/>
|
|
33
|
-
)}
|
|
34
|
-
</MultipleInputBase>
|
|
92
|
+
<div className="flex flex-col gap-2">
|
|
93
|
+
<MaskInput
|
|
94
|
+
{...props}
|
|
95
|
+
withoutForm
|
|
96
|
+
extraElement={extraElement}
|
|
97
|
+
onChange={(e) => {
|
|
98
|
+
setInputValue(e.target.value);
|
|
99
|
+
}}
|
|
100
|
+
value={inputValue}
|
|
101
|
+
/>
|
|
102
|
+
{renderItens()}
|
|
103
|
+
</div>
|
|
35
104
|
);
|
|
36
105
|
};
|
package/src/components/dataInput/Input/components/MultipleInput/__tests__/multiple-input.test.tsx
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import { MultipleInput } from "../MultipleInput";
|
|
4
|
+
|
|
5
|
+
describe("MultipleInput", () => {
|
|
6
|
+
it("should render label", () => {
|
|
7
|
+
render(
|
|
8
|
+
<MultipleInput
|
|
9
|
+
label="test"
|
|
10
|
+
name="test-input"
|
|
11
|
+
data-testid="multiple-input"
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
const label = screen.getByTestId("multiple-input-label");
|
|
15
|
+
expect(label).toBeInTheDocument();
|
|
16
|
+
expect(label).toHaveTextContent("test");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should render error", () => {
|
|
20
|
+
render(
|
|
21
|
+
<MultipleInput
|
|
22
|
+
error="test"
|
|
23
|
+
name="test-input"
|
|
24
|
+
data-testid="multiple-input"
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
const error = screen.getByTestId("multiple-input-error-label");
|
|
28
|
+
expect(error).toBeInTheDocument();
|
|
29
|
+
expect(error).toHaveTextContent("test");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should be disabled", () => {
|
|
33
|
+
render(
|
|
34
|
+
<MultipleInput disabled name="test-input" data-testid="multiple-input" />
|
|
35
|
+
);
|
|
36
|
+
const input = screen.getByTestId("multiple-input");
|
|
37
|
+
expect(input).toBeDisabled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should be required", () => {
|
|
41
|
+
render(
|
|
42
|
+
<MultipleInput
|
|
43
|
+
required
|
|
44
|
+
name="test-input"
|
|
45
|
+
label="test"
|
|
46
|
+
data-testid="multiple-input"
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
const label = screen.getByTestId("multiple-input-label");
|
|
50
|
+
expect(label).toHaveTextContent("test *");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should change value", () => {
|
|
54
|
+
render(<MultipleInput name="test-input" data-testid="multiple-input" />);
|
|
55
|
+
const input = screen.getByTestId("multiple-input");
|
|
56
|
+
fireEvent.change(input, { target: { value: "test" } });
|
|
57
|
+
expect(input).toHaveValue("test");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should render add button", () => {
|
|
61
|
+
render(<MultipleInput name="test-input" data-testid="multiple-input" />);
|
|
62
|
+
const addButton = screen.getByRole("button");
|
|
63
|
+
expect(addButton).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should add item when add button is clicked", () => {
|
|
67
|
+
const mockOnAdd = jest.fn();
|
|
68
|
+
render(
|
|
69
|
+
<MultipleInput
|
|
70
|
+
name="test-input"
|
|
71
|
+
data-testid="multiple-input"
|
|
72
|
+
onAdd={mockOnAdd}
|
|
73
|
+
withoutForm
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const input = screen.getByTestId("multiple-input");
|
|
78
|
+
const addButton = screen.getByRole("button");
|
|
79
|
+
|
|
80
|
+
fireEvent.change(input, { target: { value: "test item" } });
|
|
81
|
+
fireEvent.click(addButton);
|
|
82
|
+
|
|
83
|
+
expect(mockOnAdd).toHaveBeenCalledWith("test item");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should render with initial data", () => {
|
|
87
|
+
const initialData = ["item 1", "item 2"];
|
|
88
|
+
|
|
89
|
+
render(
|
|
90
|
+
<MultipleInput
|
|
91
|
+
name="test-input"
|
|
92
|
+
data={initialData}
|
|
93
|
+
data-testid="multiple-input"
|
|
94
|
+
withoutForm
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(screen.getByText("item 1")).toBeInTheDocument();
|
|
99
|
+
expect(screen.getByText("item 2")).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should handle mask prop", () => {
|
|
103
|
+
render(
|
|
104
|
+
<MultipleInput
|
|
105
|
+
name="test-input"
|
|
106
|
+
mask="(99) 99999-9999"
|
|
107
|
+
data-testid="multiple-input"
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const input = screen.getByTestId("multiple-input");
|
|
112
|
+
expect(input).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should remove item when remove button is clicked", () => {
|
|
116
|
+
const mockOnRemove = jest.fn();
|
|
117
|
+
const initialData = ["item 1", "item 2"];
|
|
118
|
+
|
|
119
|
+
render(
|
|
120
|
+
<MultipleInput
|
|
121
|
+
name="test-input"
|
|
122
|
+
data={initialData}
|
|
123
|
+
onRemove={mockOnRemove}
|
|
124
|
+
data-testid="multiple-input"
|
|
125
|
+
withoutForm
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const removeButtons = screen.getAllByRole("button");
|
|
130
|
+
const removeButton = removeButtons.find((button) =>
|
|
131
|
+
button.querySelector('[data-icon="MdDelete"]')
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (removeButton) {
|
|
135
|
+
fireEvent.click(removeButton);
|
|
136
|
+
expect(mockOnRemove).toHaveBeenCalledWith(0);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should disable add button when input is empty", () => {
|
|
141
|
+
render(
|
|
142
|
+
<MultipleInput
|
|
143
|
+
name="test-input"
|
|
144
|
+
data-testid="multiple-input"
|
|
145
|
+
withoutForm
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const addButton = screen.getByRole("button");
|
|
150
|
+
expect(addButton).toBeDisabled();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -21,8 +21,12 @@ export const Default: Story = {
|
|
|
21
21
|
name: "multipleInput",
|
|
22
22
|
data: ["Item 1", "Item 2", "Item 3"],
|
|
23
23
|
placeholder: "Multiple Input",
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
withoutForm: true,
|
|
25
|
+
onAdd: (data: string) => {
|
|
26
|
+
console.log("Added:", data);
|
|
27
|
+
},
|
|
28
|
+
onRemove: (index: number) => {
|
|
29
|
+
console.log("Removed index:", index);
|
|
26
30
|
},
|
|
27
31
|
},
|
|
28
32
|
};
|
|
@@ -34,30 +38,51 @@ export const WithError: Story = {
|
|
|
34
38
|
error: "This is an error",
|
|
35
39
|
data: ["Item 1", "Item 2", "Item 3"],
|
|
36
40
|
placeholder: "Multiple Input",
|
|
41
|
+
withoutForm: true,
|
|
42
|
+
onAdd: (data: string) => {
|
|
43
|
+
console.log("Added:", data);
|
|
44
|
+
},
|
|
45
|
+
onRemove: (index: number) => {
|
|
46
|
+
console.log("Removed index:", index);
|
|
47
|
+
},
|
|
37
48
|
},
|
|
38
49
|
};
|
|
39
50
|
|
|
40
51
|
export const WithForm: Story = {
|
|
41
52
|
args: {
|
|
42
53
|
label: "Multiple Input",
|
|
43
|
-
name: "client
|
|
54
|
+
name: "client",
|
|
44
55
|
placeholder: "Multiple Input",
|
|
45
|
-
mask: "999.999.999-99",
|
|
46
56
|
},
|
|
47
57
|
decorators: [
|
|
48
58
|
(Story: React.ComponentType) => {
|
|
49
59
|
const form = useForm({
|
|
50
60
|
defaultValues: {
|
|
51
|
-
client:
|
|
52
|
-
data: ["Item 1", "Item 2", "Item 3"],
|
|
53
|
-
},
|
|
61
|
+
client: ["Item 1", "Item 2", "Item 3"],
|
|
54
62
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Form {...form}>
|
|
67
|
+
<Story />
|
|
68
|
+
</Form>
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const WithMaskAndForm: Story = {
|
|
75
|
+
args: {
|
|
76
|
+
label: "Phone Numbers",
|
|
77
|
+
name: "phoneNumbers",
|
|
78
|
+
placeholder: "Enter phone number",
|
|
79
|
+
mask: "(99) 99999-9999",
|
|
80
|
+
},
|
|
81
|
+
decorators: [
|
|
82
|
+
(Story: React.ComponentType) => {
|
|
83
|
+
const form = useForm({
|
|
84
|
+
defaultValues: {
|
|
85
|
+
phoneNumbers: ["(11) 99999-9999", "(21) 88888-8888"],
|
|
61
86
|
},
|
|
62
87
|
});
|
|
63
88
|
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
export type UseMaskProps = {
|
|
4
|
+
mask: string;
|
|
5
|
+
placeholder?: string; // Placeholder que mostra a máscara
|
|
6
|
+
showMask?: boolean; // Se deve mostrar a máscara mesmo sem valor
|
|
7
|
+
allowEmpty?: boolean; // Se permite valor vazio
|
|
8
|
+
transform?: "uppercase" | "lowercase" | "capitalize"; // Transformação do texto
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const useMask = ({ mask, ...options }: UseMaskProps) => {
|
|
12
|
+
const [value, setValue] = useState("");
|
|
13
|
+
|
|
14
|
+
const applyMask = useCallback(
|
|
15
|
+
(inputValue: string) => {
|
|
16
|
+
if (!inputValue || !mask) return "";
|
|
17
|
+
|
|
18
|
+
// Remove todos os caracteres que não são letras ou números
|
|
19
|
+
const cleanValue = inputValue.replace(/[^a-zA-Z0-9]/g, "");
|
|
20
|
+
|
|
21
|
+
let maskedValue = "";
|
|
22
|
+
let cleanIndex = 0;
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < mask.length && cleanIndex < cleanValue.length; i++) {
|
|
25
|
+
const maskChar = mask[i];
|
|
26
|
+
const inputChar = cleanValue[cleanIndex];
|
|
27
|
+
|
|
28
|
+
if (maskChar === "9") {
|
|
29
|
+
// Só aceita números
|
|
30
|
+
if (/\d/.test(inputChar)) {
|
|
31
|
+
maskedValue += inputChar;
|
|
32
|
+
cleanIndex++;
|
|
33
|
+
} else {
|
|
34
|
+
// Se não for número, pula esse caractere do input
|
|
35
|
+
cleanIndex++;
|
|
36
|
+
i--; // Volta um passo na máscara para tentar novamente
|
|
37
|
+
}
|
|
38
|
+
} else if (maskChar === "A") {
|
|
39
|
+
// Só aceita letras
|
|
40
|
+
if (/[a-zA-Z]/.test(inputChar)) {
|
|
41
|
+
let char = inputChar;
|
|
42
|
+
// Aplica transformação se especificada
|
|
43
|
+
if (options.transform === "uppercase") char = char.toUpperCase();
|
|
44
|
+
else if (options.transform === "lowercase")
|
|
45
|
+
char = char.toLowerCase();
|
|
46
|
+
maskedValue += char;
|
|
47
|
+
cleanIndex++;
|
|
48
|
+
} else {
|
|
49
|
+
// Se não for letra, pula esse caractere do input
|
|
50
|
+
cleanIndex++;
|
|
51
|
+
i--; // Volta um passo na máscara para tentar novamente
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
// Caractere literal da máscara (parênteses, hífen, etc.)
|
|
55
|
+
maskedValue += maskChar;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return maskedValue;
|
|
60
|
+
},
|
|
61
|
+
[mask, options.transform]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const handleSetValue = useCallback(
|
|
65
|
+
(newValue: string) => {
|
|
66
|
+
const maskedValue = applyMask(newValue);
|
|
67
|
+
setValue(maskedValue);
|
|
68
|
+
},
|
|
69
|
+
[applyMask]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Função para obter valor sem máscara (raw)
|
|
73
|
+
const getRawValue = useCallback(() => {
|
|
74
|
+
return value.replace(/[^a-zA-Z0-9]/g, "");
|
|
75
|
+
}, [value]);
|
|
76
|
+
|
|
77
|
+
// Função para verificar se está completo
|
|
78
|
+
const isComplete = useCallback(() => {
|
|
79
|
+
return value.length === mask.length;
|
|
80
|
+
}, [value, mask]);
|
|
81
|
+
|
|
82
|
+
// Função para verificar se é válido
|
|
83
|
+
const isValid = useCallback(() => {
|
|
84
|
+
// Implementar validação específica
|
|
85
|
+
return isComplete() && value.length > 0;
|
|
86
|
+
}, [isComplete, value]);
|
|
87
|
+
|
|
88
|
+
// Função para limpar o valor
|
|
89
|
+
const clear = useCallback(() => {
|
|
90
|
+
setValue("");
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
// Compatibilidade com react-hook-form
|
|
94
|
+
const getFormProps = useCallback(
|
|
95
|
+
() => ({
|
|
96
|
+
value,
|
|
97
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
98
|
+
handleSetValue(e.target.value);
|
|
99
|
+
},
|
|
100
|
+
placeholder: options.placeholder,
|
|
101
|
+
}),
|
|
102
|
+
[value, handleSetValue, options.placeholder]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
value,
|
|
107
|
+
setValue: handleSetValue,
|
|
108
|
+
rawValue: getRawValue(),
|
|
109
|
+
isComplete: isComplete(),
|
|
110
|
+
isValid: isValid(),
|
|
111
|
+
applyMask,
|
|
112
|
+
clear,
|
|
113
|
+
placeholder: options.placeholder || mask.replace(/[9A]/g, "_"),
|
|
114
|
+
formProps: getFormProps(),
|
|
115
|
+
};
|
|
116
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# PHSA Design System
|
|
2
|
+
|
|
3
|
+
Welcome to the **PHSA Design System** - a modern and accessible React component library built with TypeScript, Tailwind CSS, and Radix UI.
|
|
4
|
+
|
|
5
|
+
## 🎯 About the Project
|
|
6
|
+
|
|
7
|
+
The PHSA Design System is a collection of reusable components that follow best practices in design and accessibility. Our mission is to provide a solid foundation for building consistent and professional user interfaces.
|
|
8
|
+
|
|
9
|
+
## 🚀 Technologies
|
|
10
|
+
|
|
11
|
+
- **React 19** - JavaScript library for user interfaces
|
|
12
|
+
- **TypeScript** - Static typing for JavaScript
|
|
13
|
+
- **Tailwind CSS** - Utility-first CSS framework
|
|
14
|
+
- **Radix UI** - Accessible primitive components
|
|
15
|
+
- **Lucide React** - Modern and consistent icons
|
|
16
|
+
- **React Hook Form** - Form management
|
|
17
|
+
- **Zod** - Schema validation
|
|
18
|
+
|
|
19
|
+
## 📦 Available Components
|
|
20
|
+
|
|
21
|
+
### Forms
|
|
22
|
+
|
|
23
|
+
- Input, Textarea, Select
|
|
24
|
+
- Checkbox, Switch, Radio
|
|
25
|
+
- DatePicker, TimePicker
|
|
26
|
+
- Form validation with Zod
|
|
27
|
+
|
|
28
|
+
### Navigation
|
|
29
|
+
|
|
30
|
+
- Button, Link
|
|
31
|
+
- Breadcrumb
|
|
32
|
+
- Pagination
|
|
33
|
+
- Tabs
|
|
34
|
+
|
|
35
|
+
### Feedback
|
|
36
|
+
|
|
37
|
+
- Alert, Toast
|
|
38
|
+
- Modal, Dialog
|
|
39
|
+
- Tooltip, Popover
|
|
40
|
+
- Loading states
|
|
41
|
+
|
|
42
|
+
### Layout
|
|
43
|
+
|
|
44
|
+
- Card, Container
|
|
45
|
+
- Grid, Flex
|
|
46
|
+
- Divider, Spacer
|
|
47
|
+
|
|
48
|
+
### Data
|
|
49
|
+
|
|
50
|
+
- Table
|
|
51
|
+
- DataTable
|
|
52
|
+
- Charts (in development)
|
|
53
|
+
|
|
54
|
+
## 🎨 Theme and Customization
|
|
55
|
+
|
|
56
|
+
The design system supports light and dark themes with full customization through Tailwind CSS. All components follow a consistent design system with:
|
|
57
|
+
|
|
58
|
+
- **Colors**: Semantic and accessible color palette
|
|
59
|
+
- **Typography**: Clear text hierarchy
|
|
60
|
+
- **Spacing**: Consistent spacing system
|
|
61
|
+
- **Borders**: Standardized border radius
|
|
62
|
+
- **Shadows**: Elevation system
|
|
63
|
+
|
|
64
|
+
## ♿ Accessibility
|
|
65
|
+
|
|
66
|
+
All components are built with accessibility in mind:
|
|
67
|
+
|
|
68
|
+
- Full keyboard navigation support
|
|
69
|
+
- Screen reader compatibility
|
|
70
|
+
- Adequate color contrast
|
|
71
|
+
- Visible focus states
|
|
72
|
+
- Appropriate ARIA labels
|
|
73
|
+
|
|
74
|
+
## 🛠️ How to Use
|
|
75
|
+
|
|
76
|
+
### Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install @phsa.tec/design-system-react
|
|
80
|
+
# or
|
|
81
|
+
yarn add @phsa.tec/design-system-react
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Basic Setup
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { Button, Input, Card } from "@phsa.tec/design-system-react";
|
|
88
|
+
|
|
89
|
+
function App() {
|
|
90
|
+
return (
|
|
91
|
+
<div>
|
|
92
|
+
<Card>
|
|
93
|
+
<Input placeholder="Type something..." />
|
|
94
|
+
<Button>Click here</Button>
|
|
95
|
+
</Card>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Theme Configuration
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { ThemeProvider } from "@phsa.tec/design-system-react";
|
|
105
|
+
|
|
106
|
+
function App() {
|
|
107
|
+
return (
|
|
108
|
+
<ThemeProvider defaultTheme="light" storageKey="phsa-theme">
|
|
109
|
+
{/* Your components here */}
|
|
110
|
+
</ThemeProvider>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 📚 Documentation
|
|
116
|
+
|
|
117
|
+
- **Components**: Explore all available components
|
|
118
|
+
- **Examples**: See practical usage examples
|
|
119
|
+
- **API**: Complete documentation of props and methods
|
|
120
|
+
- **Style Guide**: Design patterns and best practices
|
|
121
|
+
|
|
122
|
+
## 🤝 Contributing
|
|
123
|
+
|
|
124
|
+
Contributions are welcome! Please read our contribution guide before submitting pull requests.
|
|
125
|
+
|
|
126
|
+
## 📄 License
|
|
127
|
+
|
|
128
|
+
This project is licensed under the MIT License.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
**Need help?** Open an issue on GitHub or contact our team.
|