@phsa.tec/design-system-react 0.1.1 → 0.1.3

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.
@@ -0,0 +1,54 @@
1
+ name: Deploy Storybook to GitHub Pages
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ permissions:
10
+ contents: read
11
+ pages: write
12
+ id-token: write
13
+
14
+ concurrency:
15
+ group: "pages"
16
+ cancel-in-progress: false
17
+
18
+ jobs:
19
+ build:
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: Build Storybook
35
+ run: yarn build-storybook
36
+
37
+ - name: Setup Pages
38
+ uses: actions/configure-pages@v4
39
+
40
+ - name: Upload artifact
41
+ uses: actions/upload-pages-artifact@v3
42
+ with:
43
+ path: "./storybook-static"
44
+
45
+ deploy:
46
+ environment:
47
+ name: github-pages
48
+ url: ${{ steps.deployment.outputs.page_url }}
49
+ runs-on: ubuntu-latest
50
+ needs: build
51
+ steps:
52
+ - name: Deploy to GitHub Pages
53
+ id: deployment
54
+ uses: actions/deploy-pages@v4
package/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  A comprehensive React design system built with modern tools and best practices, featuring reusable components, consistent styling, and powerful layout solutions.
4
4
 
5
+ ## 📚 Live Documentation
6
+
7
+ 🌐 **[View Live Documentation](https://henriques4nti4go.github.io/phsa-design-system/)**
8
+
9
+ Explore all components interactively in our Storybook documentation, deployed automatically via GitHub Pages.
10
+
5
11
  ## ✨ Features
6
12
 
7
13
  - 🧩 **Modular Components** - Reusable React components built with TypeScript
@@ -162,7 +168,13 @@ The README is written in English as requested and provides everything a develope
162
168
 
163
169
  ## 📚 Documentation
164
170
 
165
- Visit our Storybook documentation to explore all available components:
171
+ ### Live Documentation
172
+
173
+ Visit our live Storybook documentation: **[https://henriques4nti4go.github.io/phsa-design-system/](https://henriques4nti4go.github.io/phsa-design-system/)**
174
+
175
+ ### Local Development
176
+
177
+ Run Storybook locally to explore all available components:
166
178
 
167
179
  ```bash
168
180
  npm run storybook
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phsa.tec/design-system-react",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "scripts": {
@@ -10,6 +10,7 @@
10
10
  "lint": "next lint",
11
11
  "storybook": "storybook dev -p 6006",
12
12
  "build-storybook": "storybook build",
13
+ "deploy-storybook": "storybook build && touch storybook-static/.nojekyll",
13
14
  "test": "jest"
14
15
  },
15
16
  "dependencies": {
@@ -62,7 +63,8 @@
62
63
  "@storybook/addon-docs": "^9.0.15",
63
64
  "@storybook/addon-onboarding": "^9.0.15",
64
65
  "@storybook/nextjs": "^9.0.15",
65
- "@testing-library/react": "^16.2.0",
66
+ "@testing-library/react": "^14.3.1",
67
+ "@testing-library/user-event": "^14.5.2",
66
68
  "@types/jest": "^29.5.14",
67
69
  "@types/lodash": "^4.17.15",
68
70
  "@types/node": "^20",
@@ -73,7 +75,7 @@
73
75
  "eslint": "^8",
74
76
  "eslint-config-next": "15.0.3",
75
77
  "eslint-plugin-storybook": "^9.0.15",
76
- "jest": "^29.7.0",
78
+ "jest": "^30.0.4",
77
79
  "jest-environment-jsdom": "^29.7.0",
78
80
  "postcss": "^8",
79
81
  "react-test-renderer": "^19.0.0",
@@ -84,5 +86,13 @@
84
86
  },
85
87
  "publishConfig": {
86
88
  "access": "public"
89
+ },
90
+ "repository": {
91
+ "type": "git",
92
+ "url": "https://github.com/henriques4nti4go/phsa-design-system"
93
+ },
94
+ "homepage": "https://github.com/henriques4nti4go/phsa-design-system#readme",
95
+ "bugs": {
96
+ "url": "https://github.com/henriques4nti4go/phsa-design-system/issues"
87
97
  }
88
98
  }
@@ -0,0 +1,38 @@
1
+ import "@testing-library/jest-dom";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import { Input } from "../index";
4
+
5
+ describe("Input", () => {
6
+ it("should render label", () => {
7
+ render(<Input label="test" data-testid="input" />);
8
+ const label = screen.getByTestId("input-label");
9
+ expect(label).toBeInTheDocument();
10
+ expect(label).toHaveTextContent("test");
11
+ });
12
+
13
+ it("should render error", () => {
14
+ render(<Input error="test" data-testid="input" />);
15
+ const error = screen.getByTestId("input-error-label");
16
+ expect(error).toBeInTheDocument();
17
+ expect(error).toHaveTextContent("test");
18
+ });
19
+
20
+ it("should is disabled", () => {
21
+ render(<Input disabled data-testid="input" />);
22
+ const input = screen.getByTestId("input");
23
+ expect(input).toBeDisabled();
24
+ });
25
+
26
+ it("should is required", () => {
27
+ render(<Input required data-testid="input" label="test" />);
28
+ const label = screen.getByTestId("input-label");
29
+ expect(label).toHaveTextContent("test *");
30
+ });
31
+
32
+ it("should change value", () => {
33
+ render(<Input data-testid="input" />);
34
+ const input = screen.getByTestId("input");
35
+ fireEvent.change(input, { target: { value: "test" } });
36
+ expect(input).toHaveValue("test");
37
+ });
38
+ });
@@ -1,257 +1,43 @@
1
- import React from "react";
2
- import { Label } from "../../../../ui/label";
3
- import { useFormContext } from "react-hook-form";
4
- import {
5
- FormControl,
6
- FormField,
7
- FormItem,
8
- FormLabel,
9
- FormMessage,
10
- } from "../../../../../components/ui/form";
11
- import { InputBase, InputBaseProps } from "./InputBase";
12
- import { ErrorMessage } from "../../../../../components/dataDisplay/ErrorMessage";
1
+ import { Input as InputUI } from "@/components/ui/input";
2
+ import { InputProps } from "./types";
3
+ import { useMemo } from "react";
4
+ import { InputBase } from "../InputBase";
5
+ import { useConditionalController } from "@/hooks/use-conditional-controller";
13
6
  import { cn } from "@/lib/utils";
14
7
 
15
- export type InputProps = Omit<InputBaseProps, "children"> & {
16
- component?: React.ReactNode;
17
- error?: string;
18
- withoutForm?: boolean;
19
- label?: string;
20
- leftIcon?: React.ReactNode;
21
- rightIcon?: React.ReactNode;
22
- helperText?: string;
23
- floatingLabel?: boolean;
24
- };
25
-
26
8
  export const Input = ({
27
- label,
28
- withoutForm,
29
- component,
30
- name,
31
- error,
32
- leftIcon,
33
- rightIcon,
34
- helperText,
35
- floatingLabel = false,
36
- "data-testid": testId,
37
- className,
38
- required,
39
- placeholder,
9
+ "data-testid": dataTestId,
10
+ withoutForm = false,
40
11
  ...props
41
12
  }: InputProps) => {
42
- const form = useFormContext();
43
- const hasForm = !withoutForm && !!form && !!name;
44
- const [isFocused, setIsFocused] = React.useState(false);
45
- const [hasValue, setHasValue] = React.useState(false);
46
-
47
- const containerClasses = cn("relative w-full", className);
48
-
49
- if (!hasForm)
50
- return (
51
- <div className={containerClasses}>
52
- <div className="relative">
53
- {/* Floating Label ou Label Normal */}
54
- {label && !floatingLabel && (
55
- <Label
56
- htmlFor={name || "input"}
57
- className={cn(
58
- "block text-sm font-medium mb-1.5 transition-colors duration-200",
59
- error ? "text-destructive" : "text-foreground"
60
- )}
61
- >
62
- {label}
63
- {required && <span className="text-destructive ml-1">*</span>}
64
- </Label>
65
- )}
66
-
67
- {/* Input Container */}
68
- <div className="relative">
69
- {leftIcon && (
70
- <div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 z-10">
71
- {leftIcon}
72
- </div>
73
- )}
74
-
75
- <InputBase
76
- {...props}
77
- placeholder={floatingLabel ? " " : placeholder}
78
- className={cn(
79
- "w-full px-4 py-3 border border-border rounded-xl",
80
- "bg-background transition-all duration-200",
81
- "focus:border-primary focus:ring-4 focus:ring-primary/20",
82
- "placeholder:text-muted-foreground",
83
- "disabled:bg-muted disabled:text-muted-foreground",
84
- leftIcon && "pl-10",
85
- rightIcon && "pr-10",
86
- error &&
87
- "border-destructive focus:border-destructive focus:ring-destructive/20",
88
- floatingLabel && "pt-6 pb-2"
89
- )}
90
- onFocus={(e) => {
91
- setIsFocused(true);
92
- props.onFocus?.(e);
93
- }}
94
- onBlur={(e) => {
95
- setIsFocused(false);
96
- setHasValue(!!e.target.value);
97
- props.onBlur?.(e);
98
- }}
99
- />
100
-
101
- {/* Floating Label */}
102
- {label && floatingLabel && (
103
- <Label
104
- htmlFor={name || "input"}
105
- className={cn(
106
- "absolute left-4 transition-all duration-200 pointer-events-none",
107
- "text-muted-foreground",
108
- isFocused || hasValue
109
- ? "top-2 text-xs font-medium"
110
- : "top-1/2 transform -translate-y-1/2 text-base",
111
- isFocused && !error && "text-primary",
112
- error && "text-destructive"
113
- )}
114
- >
115
- {label}
116
- {required && <span className="text-destructive ml-1">*</span>}
117
- </Label>
118
- )}
119
-
120
- {rightIcon && (
121
- <div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 z-10">
122
- {rightIcon}
123
- </div>
124
- )}
125
-
126
- {component && (
127
- <div className="absolute right-3 top-1/2 transform -translate-y-1/2 z-10">
128
- {component}
129
- </div>
130
- )}
131
- </div>
132
-
133
- {/* Helper Text */}
134
- {helperText && !error && (
135
- <p className="mt-2 text-xs text-muted-foreground">{helperText}</p>
136
- )}
137
-
138
- {/* Error Message */}
139
- {error && (
140
- <div className="mt-2">
141
- <ErrorMessage>{error}</ErrorMessage>
142
- </div>
143
- )}
144
- </div>
145
- </div>
146
- );
13
+ const formData = useConditionalController({
14
+ name: props.name || "",
15
+ withoutForm,
16
+ });
17
+
18
+ const inputProps = useMemo(() => {
19
+ return {
20
+ ...formData,
21
+ ...props,
22
+ };
23
+ }, [formData, props]);
147
24
 
148
25
  return (
149
- <FormField
150
- control={form.control}
151
- name={name}
152
- render={({ field: { onChange, value, ...rest } }) => (
153
- <FormItem
154
- className={containerClasses}
155
- data-testid={testId ? `form-item-${testId}` : undefined}
156
- >
157
- <div className="relative">
158
- {/* Label Normal */}
159
- {label && !floatingLabel && (
160
- <FormLabel
161
- htmlFor={name}
162
- className={cn(
163
- "block text-sm font-medium mb-1.5 transition-colors duration-200",
164
- "text-foreground data-[invalid]:text-destructive"
165
- )}
166
- data-testid={testId ? `form-label-${testId}` : undefined}
167
- >
168
- {label}
169
- {required && <span className="text-destructive ml-1">*</span>}
170
- </FormLabel>
171
- )}
172
-
173
- <FormControl>
174
- <div className="relative">
175
- {leftIcon && (
176
- <div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 z-10">
177
- {leftIcon}
178
- </div>
179
- )}
180
-
181
- <InputBase
182
- {...props}
183
- {...rest}
184
- value={value}
185
- onChangeText={onChange}
186
- placeholder={floatingLabel ? " " : placeholder}
187
- className={cn(
188
- "w-full px-4 py-3 border border-border rounded-xl",
189
- "bg-background transition-all duration-200",
190
- "focus:border-primary focus:ring-4 focus:ring-primary/20",
191
- "placeholder:text-muted-foreground",
192
- "disabled:bg-muted disabled:text-muted-foreground",
193
- leftIcon && "pl-10",
194
- rightIcon && "pr-10",
195
- floatingLabel && "pt-6 pb-2"
196
- )}
197
- onFocus={(e) => {
198
- setIsFocused(true);
199
- props.onFocus?.(e);
200
- }}
201
- onBlur={(e) => {
202
- setIsFocused(false);
203
- setHasValue(!!value);
204
- props.onBlur?.(e);
205
- }}
206
- />
207
-
208
- {/* Floating Label */}
209
- {label && floatingLabel && (
210
- <FormLabel
211
- htmlFor={name}
212
- className={cn(
213
- "absolute left-4 transition-all duration-200 pointer-events-none",
214
- "text-muted-foreground",
215
- isFocused || hasValue || value
216
- ? "top-2 text-xs font-medium"
217
- : "top-1/2 transform -translate-y-1/2 text-base",
218
- isFocused && "text-primary"
219
- )}
220
- >
221
- {label}
222
- {required && (
223
- <span className="text-destructive ml-1">*</span>
224
- )}
225
- </FormLabel>
226
- )}
227
-
228
- {rightIcon && (
229
- <div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 z-10">
230
- {rightIcon}
231
- </div>
232
- )}
233
-
234
- {component && (
235
- <div className="absolute right-3 top-1/2 transform -translate-y-1/2 z-10">
236
- {component}
237
- </div>
238
- )}
239
- </div>
240
- </FormControl>
241
-
242
- {/* Helper Text */}
243
- {helperText && (
244
- <p className="mt-2 text-xs text-muted-foreground">{helperText}</p>
245
- )}
246
-
247
- <FormMessage
248
- role="alert"
249
- className="mt-2"
250
- data-testid={testId ? `form-message-${testId}` : undefined}
251
- />
252
- </div>
253
- </FormItem>
254
- )}
255
- />
26
+ <InputBase
27
+ label={props.label}
28
+ error={props.error}
29
+ required={props.required}
30
+ data-testid={dataTestId}
31
+ >
32
+ <InputUI
33
+ {...inputProps}
34
+ data-testid={dataTestId}
35
+ className={cn(
36
+ props.className,
37
+ props.error &&
38
+ "border-destructive focus:border-destructive focus-visible:ring-0"
39
+ )}
40
+ />
41
+ </InputBase>
256
42
  );
257
43
  };
@@ -0,0 +1,13 @@
1
+ import { InputBaseProps } from "./InputBase";
2
+
3
+ export type InputProps = Omit<InputBaseProps, "children"> & {
4
+ component?: React.ReactNode;
5
+ error?: string;
6
+ withoutForm?: boolean;
7
+ label?: string;
8
+ leftIcon?: React.ReactNode;
9
+ rightIcon?: React.ReactNode;
10
+ helperText?: string;
11
+ floatingLabel?: boolean;
12
+ "data-testid"?: string;
13
+ };
@@ -1,89 +1,31 @@
1
- import { useFormContext } from "react-hook-form";
1
+ import { ErrorLabel } from "@/components/feedback/ErrorLabel";
2
2
  import { InputProps as InputPropsUI } from "../../../../ui/input";
3
3
  import { Label } from "../../../../ui/label";
4
- import {
5
- FormControl,
6
- FormField,
7
- FormItem,
8
- FormLabel,
9
- FormMessage,
10
- } from "../../../../ui/form";
11
4
 
12
5
  export type InputBaseProps = Omit<InputPropsUI, "children"> & {
13
6
  label?: string;
14
- withoutForm?: boolean;
15
- "data-testid"?: string;
16
7
  error?: string;
17
- children?: (
18
- props: React.DetailedHTMLProps<
19
- React.InputHTMLAttributes<HTMLInputElement>,
20
- HTMLInputElement
21
- >
22
- ) => JSX.Element;
8
+ required?: boolean;
9
+ children: React.ReactNode;
10
+ "data-testid"?: string;
23
11
  };
24
12
 
25
13
  export const InputBase = ({
26
14
  label,
27
- withoutForm,
28
- className,
29
- name,
30
- required,
31
- "data-testid": testId,
32
15
  error,
33
16
  children,
34
- ...props
17
+ required,
18
+ "data-testid": testId,
35
19
  }: InputBaseProps) => {
36
- const form = useFormContext();
37
- const hasForm = !withoutForm && !!form && !!name;
38
-
39
- if (!hasForm)
40
- return (
41
- <div className="grid w-full items-center gap-3">
42
- <Label htmlFor="email">
43
- {label}
44
- {required && <span>*</span>}
45
- </Label>
46
- {children?.({
47
- ...props,
48
- className,
49
- name,
50
- required,
51
- })}
52
- {error && <p className="text-red-500">{error}</p>}
53
- </div>
54
- );
55
-
56
20
  return (
57
- <FormField
58
- control={form.control}
59
- name={name}
60
- render={({ field }) => (
61
- <FormItem
62
- className={className}
63
- data-testid={testId ? `form-item-${testId}` : undefined}
64
- >
65
- {label && (
66
- <FormLabel
67
- htmlFor={name}
68
- data-testid={testId ? `form-label-${testId}` : undefined}
69
- >
70
- {`${label}${required ? " *" : ""}`}
71
- </FormLabel>
72
- )}
73
- <FormControl>
74
- {children?.({
75
- className,
76
- required,
77
- ...props,
78
- ...field,
79
- })}
80
- </FormControl>
81
- <FormMessage
82
- role="alert"
83
- data-testid={testId ? `form-message-${testId}` : undefined}
84
- />
85
- </FormItem>
21
+ <div>
22
+ {label && (
23
+ <Label data-testid={`${testId}-label`}>
24
+ {`${label} ${required ? "*" : ""}`}{" "}
25
+ </Label>
86
26
  )}
87
- />
27
+ {children}
28
+ {error && <ErrorLabel data-testid={testId}>{error}</ErrorLabel>}
29
+ </div>
88
30
  );
89
31
  };
@@ -1,67 +1,76 @@
1
1
  import "@testing-library/jest-dom";
2
- import { render, screen } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
4
3
  import { MaskInput } from "../mask-input";
5
4
 
6
5
  describe("MaskInput", () => {
7
- const defaultProps = {
8
- options: {
9
- mask: "000.000.000-00",
10
- },
11
- name: "cpf",
12
- label: "CPF",
13
- };
14
-
15
- it("should render correctly", () => {
16
- render(<MaskInput {...defaultProps} />);
17
-
18
- expect(screen.getByTestId("input-base-cpf")).toBeInTheDocument();
19
- expect(screen.getByTestId("input-wrapper-cpf")).toBeInTheDocument();
20
- expect(screen.getByTestId("input-cpf")).toBeInTheDocument();
21
- expect(screen.getByText("CPF")).toBeInTheDocument();
6
+ it("should render label", () => {
7
+ render(
8
+ <MaskInput label="test" data-testid="mask-input" mask="000.000.000-00" />
9
+ );
10
+ const label = screen.getByTestId("mask-input-label");
11
+ expect(label).toBeInTheDocument();
12
+ expect(label).toHaveTextContent("test");
22
13
  });
23
14
 
24
- it("should apply mask when typing", async () => {
25
- const user = userEvent.setup();
26
- render(<MaskInput {...defaultProps} />);
27
-
28
- const input = screen.getByTestId("input-cpf");
29
- await user.type(input, "12345678900");
30
-
31
- expect(input).toHaveValue("123.456.789-00");
15
+ it("should render error", () => {
16
+ render(
17
+ <MaskInput error="test" data-testid="mask-input" mask="000.000.000-00" />
18
+ );
19
+ const error = screen.getByTestId("mask-input-error-label");
20
+ expect(error).toBeInTheDocument();
21
+ expect(error).toHaveTextContent("test");
32
22
  });
33
23
 
34
- it("should call onChange with masked value", async () => {
35
- const onChange = jest.fn();
36
- const user = userEvent.setup();
37
- render(<MaskInput {...defaultProps} onChange={onChange} />);
38
-
39
- const input = screen.getByTestId("input-cpf");
40
- await user.type(input, "12345678900");
41
-
42
- expect(onChange).toHaveBeenCalled();
24
+ it("should be disabled", () => {
25
+ render(
26
+ <MaskInput disabled data-testid="mask-input" mask="000.000.000-00" />
27
+ );
28
+ const input = screen.getByTestId("mask-input");
29
+ expect(input).toBeDisabled();
43
30
  });
44
31
 
45
- it("should render with error state", () => {
46
- render(<MaskInput {...defaultProps} error="Invalid CPF" />);
47
-
48
- expect(screen.getByText("Invalid CPF")).toBeInTheDocument();
32
+ it("should be required", () => {
33
+ render(
34
+ <MaskInput
35
+ required
36
+ data-testid="mask-input"
37
+ label="test"
38
+ mask="000.000.000-00"
39
+ />
40
+ );
41
+ const label = screen.getByTestId("mask-input-label");
42
+ expect(label).toHaveTextContent("test *");
49
43
  });
50
44
 
51
- it("should render with description", () => {
52
- render(<MaskInput {...defaultProps} description="Enter your CPF number" />);
45
+ it("should apply mask to input value", async () => {
46
+ render(<MaskInput data-testid="mask-input" mask="000.000.000-00" />);
47
+ const input = screen.getByTestId("mask-input");
48
+
49
+ fireEvent.focus(input);
50
+ fireEvent.input(input, { target: { value: "12345678901" } });
53
51
 
54
- expect(screen.getByText("Enter your CPF number")).toBeInTheDocument();
52
+ expect(input).toHaveValue("123.456.789-01");
55
53
  });
56
54
 
57
- it("should render with custom component", () => {
55
+ it("should handle placeholder", () => {
58
56
  render(
59
57
  <MaskInput
60
- {...defaultProps}
61
- component={<span data-testid="custom-component">Custom</span>}
58
+ data-testid="mask-input"
59
+ mask="000.000.000-00"
60
+ placeholder="Digite seu CPF"
62
61
  />
63
62
  );
63
+ const input = screen.getByTestId("mask-input");
64
+
65
+ expect(input).toHaveAttribute("placeholder", "Digite seu CPF");
66
+ });
67
+
68
+ it("should handle name attribute", () => {
69
+ render(
70
+ <MaskInput data-testid="mask-input" mask="000.000.000-00" name="cpf" />
71
+ );
72
+ const input = screen.getByTestId("mask-input");
64
73
 
65
- expect(screen.getByTestId("custom-component")).toBeInTheDocument();
74
+ expect(input).toHaveAttribute("name", "cpf");
66
75
  });
67
76
  });
@@ -16,31 +16,41 @@ export default meta;
16
16
  type Story = StoryObj<typeof meta>;
17
17
 
18
18
  export const Default: Story = {
19
+ args: {
20
+ placeholder: "99999-999",
21
+ label: "CEP",
22
+ mask: "00000-000",
23
+ },
24
+ };
25
+
26
+ export const CPF: Story = {
19
27
  args: {
20
28
  placeholder: "000.000.000-00",
21
29
  label: "CPF",
22
- mask: "999.999.999-99",
30
+ mask: "000.000.000-00",
23
31
  },
24
32
  };
25
33
 
26
34
  export const Phone: Story = {
27
35
  args: {
28
- placeholder: "(99) 99999-9999",
36
+ placeholder: "(00) 00000-0000",
29
37
  label: "Telefone",
38
+ mask: "(00) 00000-0000",
30
39
  },
31
40
  };
32
41
 
33
42
  export const Date: Story = {
34
43
  args: {
35
- placeholder: "99/99/9999",
44
+ placeholder: "00/00/0000",
36
45
  label: "Data",
46
+ mask: "00/00/0000",
37
47
  },
38
48
  };
39
49
 
40
50
  export const WithForm = () => {
41
51
  const form = useForm({
42
52
  defaultValues: {
43
- cpf: "999.999.999-99",
53
+ cpf: "",
44
54
  },
45
55
  });
46
56
 
@@ -51,7 +61,7 @@ export const WithForm = () => {
51
61
  name="cpf"
52
62
  label="CPF"
53
63
  placeholder="000.000.000-00"
54
- mask="999.999.999-99"
64
+ mask="000.000.000-00"
55
65
  />
56
66
  </form>
57
67
  </Form>
@@ -1,43 +1,54 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
- import { Input } from "../../../../ui/input";
4
- import { InputBase, InputBaseProps } from "../InputBase";
3
+ import { useIMask, ReactMaskOpts } from "react-imask";
4
+ import { Input } from "../Input";
5
+ import { useConditionalController } from "@/hooks/use-conditional-controller";
6
+ import { InputProps } from "../Input/types";
5
7
 
6
- export type MaskInputProps = Omit<InputBaseProps, "children"> & {
7
- placeholder?: string;
8
- className?: string;
9
- withoutForm?: boolean;
10
- component?: React.ReactNode;
11
- "data-testid"?: string;
12
- onChange?: (value: string) => void;
13
- };
8
+ export type MaskInputProps = ReactMaskOpts & InputProps;
14
9
 
15
10
  export const MaskInput = ({
16
- component,
17
- "data-testid": testId,
18
- label,
19
- name,
20
- withoutForm,
11
+ "data-testid": dataTestId,
12
+ withoutForm = false,
21
13
  ...props
22
14
  }: MaskInputProps) => {
15
+ const formData = useConditionalController({
16
+ name: props.name || "",
17
+ withoutForm,
18
+ });
19
+
20
+ const {
21
+ label,
22
+ error,
23
+ required,
24
+ name,
25
+ className,
26
+ placeholder,
27
+ ...imaskProps
28
+ } = props;
29
+
30
+ const { ref, value } = useIMask({
31
+ ...imaskProps,
32
+ });
33
+
34
+ React.useEffect(() => {
35
+ if (formData.value !== undefined && formData.value !== value) {
36
+ formData.onChange(value);
37
+ }
38
+ }, [formData, value]);
39
+
23
40
  return (
24
- <InputBase
41
+ <Input
42
+ {...props}
43
+ ref={ref as React.RefObject<HTMLInputElement>}
44
+ data-testid={dataTestId}
45
+ withoutForm={true}
25
46
  label={label}
26
- data-testid={testId}
27
- withoutForm={withoutForm}
47
+ error={error}
48
+ required={required}
49
+ className={className}
50
+ placeholder={placeholder}
28
51
  name={name}
29
- >
30
- {({ ...rest }) => {
31
- return (
32
- <div
33
- className="flex w-full gap-3"
34
- data-testid={`input-wrapper-${testId}`}
35
- >
36
- <Input {...props} {...rest} />
37
- {component}
38
- </div>
39
- );
40
- }}
41
- </InputBase>
52
+ />
42
53
  );
43
54
  };
@@ -0,0 +1,24 @@
1
+ import React from "react";
2
+ import { Icon } from "@/components/dataDisplay";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ export type ErrorLabelProps = React.PropsWithChildren<{
6
+ className?: string;
7
+ "data-testid"?: string;
8
+ }>;
9
+
10
+ export const ErrorLabel = ({
11
+ children,
12
+ className,
13
+ "data-testid": dataTestId,
14
+ }: ErrorLabelProps) => {
15
+ return (
16
+ <div
17
+ className={cn("flex items-center gap-2 my-2", className)}
18
+ data-testid={`${dataTestId}-error-label`}
19
+ >
20
+ <Icon name="MdErrorOutline" size={18} className="text-destructive" />
21
+ <span className="text-destructive text-sm">{children}</span>
22
+ </div>
23
+ );
24
+ };
@@ -0,0 +1,35 @@
1
+ import { useMemo } from "react";
2
+ import {
3
+ useController,
4
+ useForm,
5
+ useFormContext,
6
+ ControllerRenderProps,
7
+ FieldValues,
8
+ } from "react-hook-form";
9
+
10
+ export const useConditionalController = ({
11
+ name,
12
+ withoutForm,
13
+ }: {
14
+ name: string;
15
+ withoutForm?: boolean;
16
+ }): ControllerRenderProps<FieldValues, string> | Record<string, never> => {
17
+ const form = useFormContext();
18
+
19
+ const hasForm = useMemo(() => {
20
+ return !withoutForm && !!form?.control;
21
+ }, [withoutForm, form]);
22
+
23
+ const tempForm = useForm();
24
+
25
+ const controlToUse = useMemo(() => {
26
+ return hasForm ? form.control : tempForm.control;
27
+ }, [tempForm, form, hasForm]);
28
+
29
+ const controller = useController({
30
+ control: controlToUse,
31
+ name: name || "temp",
32
+ });
33
+
34
+ return hasForm ? controller.field : {};
35
+ };
@@ -1,100 +0,0 @@
1
- import React from "react";
2
- import { render, screen } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { Input } from "../index";
5
- import { FormProvider, useForm } from "react-hook-form";
6
-
7
- // Componente wrapper para testar o Input dentro de um formulário
8
- const FormWrapper = ({ children }: { children: React.ReactNode }) => {
9
- const methods = useForm();
10
- return <FormProvider {...methods}>{children}</FormProvider>;
11
- };
12
-
13
- describe("Componente Input", () => {
14
- it("deve renderizar com label", () => {
15
- render(
16
- <FormWrapper>
17
- <Input label="Nome" name="name" />
18
- </FormWrapper>
19
- );
20
-
21
- expect(screen.getByLabelText("Nome")).toBeInTheDocument();
22
- });
23
-
24
- it("deve renderizar sem formulário quando withoutForm é true", () => {
25
- render(<Input label="Email" name="email" withoutForm={true} />);
26
-
27
- expect(screen.getByLabelText("Email")).toBeInTheDocument();
28
- });
29
-
30
- it("deve exibir mensagem de erro quando fornecida", () => {
31
- render(
32
- <FormWrapper>
33
- <Input label="Senha" name="password" error="Senha é obrigatória" />
34
- </FormWrapper>
35
- );
36
-
37
- expect(screen.getByText("Senha é obrigatória")).toBeInTheDocument();
38
- });
39
-
40
- it("deve aplicar data-testid quando fornecido", () => {
41
- render(
42
- <FormWrapper>
43
- <Input label="Telefone" name="phone" data-testid="phone-input" />
44
- </FormWrapper>
45
- );
46
-
47
- expect(screen.getByTestId("phone-input")).toBeInTheDocument();
48
- });
49
-
50
- it("deve lidar corretamente com a entrada do usuário", async () => {
51
- const user = userEvent.setup();
52
-
53
- render(
54
- <FormWrapper>
55
- <Input label="Usuário" name="username" />
56
- </FormWrapper>
57
- );
58
-
59
- const input = screen.getByLabelText("Usuário");
60
- await user.type(input, "testuser");
61
-
62
- expect(input).toHaveValue("testuser");
63
- });
64
-
65
- it("deve passar props adicionais para o elemento input", () => {
66
- render(
67
- <FormWrapper>
68
- <Input
69
- label="Código"
70
- name="code"
71
- placeholder="Digite o código"
72
- maxLength={6}
73
- />
74
- </FormWrapper>
75
- );
76
-
77
- const input = screen.getByLabelText("Código");
78
- expect(input).toHaveAttribute("placeholder", "Digite o código");
79
- expect(input).toHaveAttribute("maxLength", "6");
80
- });
81
-
82
- it("deve chamar o onChange quando usado sem formulário", async () => {
83
- const handleChange = jest.fn();
84
- const user = userEvent.setup();
85
-
86
- render(
87
- <Input
88
- label="Comentário"
89
- name="comment"
90
- withoutForm={true}
91
- onChange={handleChange}
92
- />
93
- );
94
-
95
- const input = screen.getByLabelText("Comentário");
96
- await user.type(input, "a");
97
-
98
- expect(handleChange).toHaveBeenCalled();
99
- });
100
- });
@@ -1,120 +0,0 @@
1
- import "@testing-library/jest-dom";
2
- import React from "react";
3
- import { render, screen } from "@testing-library/react";
4
- import { InputBase, CustomInputProps } from "..";
5
- import { Form } from "../../../../../../components/ui/form";
6
- import { useForm } from "react-hook-form";
7
-
8
- describe("InputBase", () => {
9
- const MockInput = (props: CustomInputProps) => (
10
- <input
11
- data-testid="mock-input"
12
- {...props}
13
- onChange={(e) => props.onChange?.(e.target.value)}
14
- />
15
- );
16
-
17
- it("deve renderizar sem contexto de formulário", () => {
18
- render(<InputBase data-testid="test">{() => <MockInput />}</InputBase>);
19
- expect(screen.getByTestId("mock-input")).toBeInTheDocument();
20
- });
21
-
22
- it("deve renderizar label e descrição", () => {
23
- render(
24
- <InputBase
25
- label="Test Label"
26
- description="Test Description"
27
- data-testid="test"
28
- >
29
- {() => <MockInput />}
30
- </InputBase>
31
- );
32
-
33
- expect(screen.getByText("Test Label")).toBeInTheDocument();
34
- expect(screen.getByText("Test Description")).toBeInTheDocument();
35
- });
36
-
37
- it("deve mostrar indicador de obrigatório quando required é true", () => {
38
- render(
39
- <InputBase label="Test Label" required data-testid="test">
40
- {() => <MockInput />}
41
- </InputBase>
42
- );
43
-
44
- expect(screen.getByText("Test Label *")).toBeInTheDocument();
45
- });
46
-
47
- it("deve renderizar mensagem de erro", () => {
48
- render(
49
- <InputBase error="Test Error" data-testid="test">
50
- {() => <MockInput />}
51
- </InputBase>
52
- );
53
-
54
- expect(screen.getByText("Test Error")).toBeInTheDocument();
55
- });
56
-
57
- it("deve funcionar com contexto de formulário", () => {
58
- const TestForm = () => {
59
- const form = useForm();
60
- return (
61
- <Form {...form}>
62
- <InputBase name="test" data-testid="test">
63
- {() => <MockInput />}
64
- </InputBase>
65
- </Form>
66
- );
67
- };
68
-
69
- render(<TestForm />);
70
- expect(screen.getByTestId("mock-input")).toBeInTheDocument();
71
- });
72
-
73
- it("deve aplicar className customizada", () => {
74
- render(
75
- <InputBase className="custom-class" data-testid="test">
76
- {() => <MockInput />}
77
- </InputBase>
78
- );
79
-
80
- const container = screen.getByTestId("input-base-test");
81
- expect(container).toHaveClass("custom-class");
82
- });
83
-
84
- it("deve lidar com estado disabled", () => {
85
- render(
86
- <InputBase disabled data-testid="mock-input">
87
- {(props) => <MockInput {...props} />}
88
- </InputBase>
89
- );
90
-
91
- expect(screen.getByTestId("mock-input")).toBeDisabled();
92
- });
93
-
94
- it("deve validar erros do React Hook Form", async () => {
95
- const TestForm = () => {
96
- const form = useForm({
97
- defaultValues: {
98
- test: "",
99
- },
100
- });
101
-
102
- React.useEffect(() => {
103
- form.setError("test", { message: "Test Error" });
104
- }, [form]);
105
-
106
- return (
107
- <Form {...form}>
108
- <InputBase name="test" data-testid="mock-input" required>
109
- {(props) => <MockInput {...props} />}
110
- </InputBase>
111
- </Form>
112
- );
113
- };
114
-
115
- render(<TestForm />);
116
-
117
- const errorMessage = await screen.findByText("Test Error");
118
- expect(errorMessage).toBeInTheDocument();
119
- });
120
- });
@@ -1,95 +0,0 @@
1
- import "@testing-library/jest-dom";
2
- import { render, screen } from "@testing-library/react";
3
- import { NumberInput } from "../number-input";
4
- import userEvent from "@testing-library/user-event";
5
-
6
- describe("NumberInput", () => {
7
- it("deve renderizar o input com label", () => {
8
- render(<NumberInput label="Valor" data-testid="test" />);
9
-
10
- expect(screen.getByTestId("test-number-input")).toBeInTheDocument();
11
- });
12
-
13
- it("deve renderizar a descrição quando fornecida", () => {
14
- render(<NumberInput description="Digite um número" />);
15
-
16
- expect(screen.getByText("Digite um número")).toBeInTheDocument();
17
- });
18
-
19
- it("deve exibir mensagem de erro quando fornecida", () => {
20
- render(<NumberInput error="Valor inválido" />);
21
-
22
- expect(screen.getByText("Valor inválido")).toBeInTheDocument();
23
- });
24
-
25
- it("deve chamar onValueChange com o valor numérico correto", async () => {
26
- const onValueChange = jest.fn();
27
- render(<NumberInput onChange={onValueChange} />);
28
-
29
- const input = screen.getByRole("textbox");
30
- await userEvent.type(input, "123.45");
31
-
32
- expect(onValueChange).toHaveBeenCalledWith(123.45);
33
- });
34
-
35
- it("deve aceitar apenas números e separador decimal", async () => {
36
- const onValueChange = jest.fn();
37
- render(<NumberInput onChange={onValueChange} />);
38
-
39
- const input = screen.getByRole("textbox");
40
- await userEvent.type(input, "abc123.45def");
41
-
42
- expect(input).toHaveValue("123.45");
43
- });
44
-
45
- it("deve renderizar componente adicional quando fornecido", () => {
46
- render(
47
- <NumberInput
48
- component={<button data-testid="extra-component">Extra</button>}
49
- />
50
- );
51
-
52
- expect(screen.getByTestId("extra-component")).toBeInTheDocument();
53
- });
54
-
55
- it("deve aplicar className customizada", () => {
56
- render(<NumberInput className="custom-class" />);
57
-
58
- expect(
59
- screen.getByRole("textbox").parentElement?.parentElement
60
- ).toHaveClass("custom-class");
61
- });
62
-
63
- it("deve formatar o valor inicial corretamente", () => {
64
- render(
65
- <NumberInput
66
- value={1234.56}
67
- data-testid="test"
68
- thousandSeparator="."
69
- decimalSeparator=","
70
- fixedDecimalScale
71
- decimalScale={2}
72
- />
73
- );
74
-
75
- expect(screen.getByTestId("test-number-input")).toHaveValue("1.234,56");
76
- });
77
-
78
- it("deve formatar o valor conforme digitação", async () => {
79
- const user = userEvent.setup();
80
- render(
81
- <NumberInput
82
- data-testid="test"
83
- thousandSeparator="."
84
- decimalSeparator=","
85
- fixedDecimalScale
86
- decimalScale={2}
87
- />
88
- );
89
-
90
- const input = screen.getByTestId("test-number-input");
91
- await user.type(input, "123456");
92
-
93
- expect(input).toHaveValue("123.456,00");
94
- });
95
- });