@fabio.caffarello/react-design-system 1.2.0 → 1.3.0
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/dist/index.cjs +4 -4
- package/dist/index.js +696 -246
- package/dist/ui/atoms/ErrorMessage/ErrorMessage.d.ts +18 -0
- package/dist/ui/atoms/ErrorMessage/ErrorMessage.stories.d.ts +7 -0
- package/dist/ui/atoms/ErrorMessage/ErrorMessage.test.d.ts +1 -0
- package/dist/ui/atoms/Label/Label.d.ts +20 -0
- package/dist/ui/atoms/Label/Label.stories.d.ts +8 -0
- package/dist/ui/atoms/Label/Label.test.d.ts +1 -0
- package/dist/ui/atoms/NavLink/NavLink.d.ts +20 -0
- package/dist/ui/atoms/NavLink/NavLink.stories.d.ts +8 -0
- package/dist/ui/atoms/NavLink/NavLink.test.d.ts +1 -0
- package/dist/ui/atoms/index.d.ts +3 -0
- package/dist/ui/molecules/Breadcrumb/Breadcrumb.d.ts +28 -0
- package/dist/ui/molecules/Breadcrumb/Breadcrumb.stories.d.ts +9 -0
- package/dist/ui/molecules/Breadcrumb/Breadcrumb.test.d.ts +1 -0
- package/dist/ui/molecules/Form/Form.d.ts +24 -0
- package/dist/ui/molecules/Form/Form.stories.d.ts +9 -0
- package/dist/ui/molecules/Form/Form.test.d.ts +1 -0
- package/dist/ui/molecules/Pagination/Pagination.d.ts +28 -0
- package/dist/ui/molecules/Pagination/Pagination.stories.d.ts +10 -0
- package/dist/ui/molecules/Pagination/Pagination.test.d.ts +1 -0
- package/dist/ui/molecules/index.d.ts +4 -0
- package/dist/ui/organisms/Modal/Modal.d.ts +25 -0
- package/dist/ui/organisms/Modal/Modal.stories.d.ts +9 -0
- package/dist/ui/organisms/Modal/Modal.test.d.ts +1 -0
- package/dist/ui/organisms/Table/Table.d.ts +35 -0
- package/dist/ui/organisms/Table/Table.stories.d.ts +9 -0
- package/dist/ui/organisms/Table/Table.test.d.ts +1 -0
- package/dist/ui/organisms/index.d.ts +3 -0
- package/package.json +9 -1
- package/src/ui/atoms/ErrorMessage/ErrorMessage.stories.tsx +81 -0
- package/src/ui/atoms/ErrorMessage/ErrorMessage.test.tsx +40 -0
- package/src/ui/atoms/ErrorMessage/ErrorMessage.tsx +62 -0
- package/src/ui/atoms/Label/Label.stories.tsx +94 -0
- package/src/ui/atoms/Label/Label.test.tsx +47 -0
- package/src/ui/atoms/Label/Label.tsx +51 -0
- package/src/ui/atoms/NavLink/NavLink.stories.tsx +71 -0
- package/src/ui/atoms/NavLink/NavLink.test.tsx +44 -0
- package/src/ui/atoms/NavLink/NavLink.tsx +63 -0
- package/src/ui/atoms/index.ts +6 -0
- package/src/ui/molecules/Breadcrumb/Breadcrumb.stories.tsx +75 -0
- package/src/ui/molecules/Breadcrumb/Breadcrumb.test.tsx +89 -0
- package/src/ui/molecules/Breadcrumb/Breadcrumb.tsx +79 -0
- package/src/ui/molecules/Form/Form.stories.tsx +195 -0
- package/src/ui/molecules/Form/Form.test.tsx +87 -0
- package/src/ui/molecules/Form/Form.tsx +76 -0
- package/src/ui/molecules/Pagination/Pagination.stories.tsx +116 -0
- package/src/ui/molecules/Pagination/Pagination.test.tsx +112 -0
- package/src/ui/molecules/Pagination/Pagination.tsx +168 -0
- package/src/ui/molecules/index.ts +7 -0
- package/src/ui/organisms/Modal/Modal.stories.tsx +102 -0
- package/src/ui/organisms/Modal/Modal.test.tsx +111 -0
- package/src/ui/organisms/Modal/Modal.tsx +203 -0
- package/src/ui/organisms/Table/Table.stories.tsx +137 -0
- package/src/ui/organisms/Table/Table.test.tsx +109 -0
- package/src/ui/organisms/Table/Table.tsx +128 -0
- package/src/ui/organisms/index.ts +5 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Form from "./Form";
|
|
3
|
+
import { Input, Label, Button, Textarea, Select, ErrorMessage } from "../../atoms";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Form> = {
|
|
6
|
+
title: "UI/Molecules/Form",
|
|
7
|
+
component: Form,
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component: "A wrapper component for forms with validation states, error/success messages, and layout.",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
loading: {
|
|
17
|
+
control: "boolean",
|
|
18
|
+
description: "Whether the form is in a loading state",
|
|
19
|
+
},
|
|
20
|
+
error: {
|
|
21
|
+
control: "text",
|
|
22
|
+
description: "Global error message to display",
|
|
23
|
+
},
|
|
24
|
+
success: {
|
|
25
|
+
control: "text",
|
|
26
|
+
description: "Success message to display",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const Default: StoryObj<typeof Form> = {
|
|
32
|
+
args: {
|
|
33
|
+
onSubmit: (e) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
alert("Form submitted!");
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
render: (args) => (
|
|
39
|
+
<Form {...args} className="max-w-md">
|
|
40
|
+
<div className="space-y-2">
|
|
41
|
+
<Label htmlFor="name" variant="required">
|
|
42
|
+
Name
|
|
43
|
+
</Label>
|
|
44
|
+
<Input id="name" name="name" placeholder="Enter your name" required />
|
|
45
|
+
</div>
|
|
46
|
+
<div className="space-y-2">
|
|
47
|
+
<Label htmlFor="email" variant="required">
|
|
48
|
+
Email
|
|
49
|
+
</Label>
|
|
50
|
+
<Input id="email" name="email" type="email" placeholder="Enter your email" required />
|
|
51
|
+
</div>
|
|
52
|
+
<Button type="submit" variant="regular">
|
|
53
|
+
Submit
|
|
54
|
+
</Button>
|
|
55
|
+
</Form>
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const WithError: StoryObj<typeof Form> = {
|
|
60
|
+
args: {
|
|
61
|
+
error: "Please fix the errors below and try again.",
|
|
62
|
+
onSubmit: (e) => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
render: (args) => (
|
|
67
|
+
<Form {...args} className="max-w-md">
|
|
68
|
+
<div className="space-y-2">
|
|
69
|
+
<Label htmlFor="email" variant="required">
|
|
70
|
+
Email
|
|
71
|
+
</Label>
|
|
72
|
+
<Input
|
|
73
|
+
id="email"
|
|
74
|
+
name="email"
|
|
75
|
+
type="email"
|
|
76
|
+
aria-invalid="true"
|
|
77
|
+
aria-describedby="email-error"
|
|
78
|
+
/>
|
|
79
|
+
<ErrorMessage message="Please enter a valid email address" id="email-error" />
|
|
80
|
+
</div>
|
|
81
|
+
<Button type="submit" variant="regular">
|
|
82
|
+
Submit
|
|
83
|
+
</Button>
|
|
84
|
+
</Form>
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const WithSuccess: StoryObj<typeof Form> = {
|
|
89
|
+
args: {
|
|
90
|
+
success: "Form submitted successfully!",
|
|
91
|
+
onSubmit: (e) => {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
render: (args) => (
|
|
96
|
+
<Form {...args} className="max-w-md">
|
|
97
|
+
<div className="space-y-2">
|
|
98
|
+
<Label htmlFor="name" variant="required">
|
|
99
|
+
Name
|
|
100
|
+
</Label>
|
|
101
|
+
<Input id="name" name="name" />
|
|
102
|
+
</div>
|
|
103
|
+
<Button type="submit" variant="regular">
|
|
104
|
+
Submit
|
|
105
|
+
</Button>
|
|
106
|
+
</Form>
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const Loading: StoryObj<typeof Form> = {
|
|
111
|
+
args: {
|
|
112
|
+
loading: true,
|
|
113
|
+
onSubmit: (e) => {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
render: (args) => (
|
|
118
|
+
<Form {...args} className="max-w-md">
|
|
119
|
+
<div className="space-y-2">
|
|
120
|
+
<Label htmlFor="name" variant="required">
|
|
121
|
+
Name
|
|
122
|
+
</Label>
|
|
123
|
+
<Input id="name" name="name" disabled />
|
|
124
|
+
</div>
|
|
125
|
+
<Button type="submit" variant="regular" disabled>
|
|
126
|
+
Submitting...
|
|
127
|
+
</Button>
|
|
128
|
+
</Form>
|
|
129
|
+
),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const CompleteForm: StoryObj<typeof Form> = {
|
|
133
|
+
args: {
|
|
134
|
+
onSubmit: (e) => {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
alert("Form submitted!");
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
render: (args) => (
|
|
140
|
+
<Form {...args} className="max-w-md space-y-4">
|
|
141
|
+
<div className="space-y-2">
|
|
142
|
+
<Label htmlFor="title" variant="required">
|
|
143
|
+
Title
|
|
144
|
+
</Label>
|
|
145
|
+
<Input id="title" name="title" placeholder="Enter title" required />
|
|
146
|
+
</div>
|
|
147
|
+
<div className="space-y-2">
|
|
148
|
+
<Label htmlFor="description" variant="required">
|
|
149
|
+
Description
|
|
150
|
+
</Label>
|
|
151
|
+
<Textarea id="description" name="description" rows={4} placeholder="Enter description" required />
|
|
152
|
+
</div>
|
|
153
|
+
<div className="space-y-2">
|
|
154
|
+
<Label htmlFor="status" variant="required">
|
|
155
|
+
Status
|
|
156
|
+
</Label>
|
|
157
|
+
<Select
|
|
158
|
+
id="status"
|
|
159
|
+
name="status"
|
|
160
|
+
options={[
|
|
161
|
+
{ value: "DRAFT", label: "Draft" },
|
|
162
|
+
{ value: "ACTIVE", label: "Active" },
|
|
163
|
+
{ value: "COMPLETED", label: "Completed" },
|
|
164
|
+
]}
|
|
165
|
+
placeholder="Select status"
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
<div className="space-y-2">
|
|
169
|
+
<Label htmlFor="priority" variant="optional">
|
|
170
|
+
Priority
|
|
171
|
+
</Label>
|
|
172
|
+
<Select
|
|
173
|
+
id="priority"
|
|
174
|
+
name="priority"
|
|
175
|
+
options={[
|
|
176
|
+
{ value: "LOW", label: "Low" },
|
|
177
|
+
{ value: "MEDIUM", label: "Medium" },
|
|
178
|
+
{ value: "HIGH", label: "High" },
|
|
179
|
+
]}
|
|
180
|
+
placeholder="Select priority"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
<div className="flex gap-2">
|
|
184
|
+
<Button type="submit" variant="regular">
|
|
185
|
+
Submit
|
|
186
|
+
</Button>
|
|
187
|
+
<Button type="button" variant="secondary">
|
|
188
|
+
Cancel
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
</Form>
|
|
192
|
+
),
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export default meta;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import Form from "./Form";
|
|
4
|
+
import { Input, Button } from "../../atoms";
|
|
5
|
+
|
|
6
|
+
describe("Form", () => {
|
|
7
|
+
it("renders form with children", () => {
|
|
8
|
+
const { container } = render(
|
|
9
|
+
<Form>
|
|
10
|
+
<Input name="test" />
|
|
11
|
+
</Form>
|
|
12
|
+
);
|
|
13
|
+
const form = container.querySelector("form");
|
|
14
|
+
expect(form).toBeInTheDocument();
|
|
15
|
+
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("calls onSubmit when form is submitted", () => {
|
|
19
|
+
const handleSubmit = vi.fn((e) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
});
|
|
22
|
+
const { container } = render(
|
|
23
|
+
<Form onSubmit={handleSubmit}>
|
|
24
|
+
<Button type="submit">Submit</Button>
|
|
25
|
+
</Form>
|
|
26
|
+
);
|
|
27
|
+
const form = container.querySelector("form");
|
|
28
|
+
if (form) {
|
|
29
|
+
fireEvent.submit(form);
|
|
30
|
+
}
|
|
31
|
+
expect(handleSubmit).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("displays error message when error prop is provided", () => {
|
|
35
|
+
render(
|
|
36
|
+
<Form error="Something went wrong">
|
|
37
|
+
<Input name="test" />
|
|
38
|
+
</Form>
|
|
39
|
+
);
|
|
40
|
+
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByText("Something went wrong")).toHaveAttribute("role", "alert");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("displays success message when success prop is provided", () => {
|
|
45
|
+
render(
|
|
46
|
+
<Form success="Form submitted successfully!">
|
|
47
|
+
<Input name="test" />
|
|
48
|
+
</Form>
|
|
49
|
+
);
|
|
50
|
+
expect(screen.getByText("Form submitted successfully!")).toBeInTheDocument();
|
|
51
|
+
expect(screen.getByText("Form submitted successfully!")).toHaveAttribute("role", "alert");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("does not call onSubmit when loading", () => {
|
|
55
|
+
const handleSubmit = vi.fn();
|
|
56
|
+
const { container } = render(
|
|
57
|
+
<Form onSubmit={handleSubmit} loading={true}>
|
|
58
|
+
<Button type="submit">Submit</Button>
|
|
59
|
+
</Form>
|
|
60
|
+
);
|
|
61
|
+
const form = container.querySelector("form");
|
|
62
|
+
if (form) {
|
|
63
|
+
fireEvent.submit(form);
|
|
64
|
+
}
|
|
65
|
+
expect(handleSubmit).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("applies custom className", () => {
|
|
69
|
+
const { container } = render(
|
|
70
|
+
<Form className="custom-class">
|
|
71
|
+
<Input name="test" />
|
|
72
|
+
</Form>
|
|
73
|
+
);
|
|
74
|
+
const form = container.querySelector("form");
|
|
75
|
+
expect(form).toHaveClass("custom-class");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("has noValidate attribute", () => {
|
|
79
|
+
const { container } = render(
|
|
80
|
+
<Form>
|
|
81
|
+
<Input name="test" />
|
|
82
|
+
</Form>
|
|
83
|
+
);
|
|
84
|
+
const form = container.querySelector("form");
|
|
85
|
+
expect(form).toHaveAttribute("noValidate");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { FormHTMLAttributes, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
interface Props extends FormHTMLAttributes<HTMLFormElement> {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;
|
|
6
|
+
loading?: boolean;
|
|
7
|
+
error?: string | null;
|
|
8
|
+
success?: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Form Component
|
|
13
|
+
*
|
|
14
|
+
* A wrapper component for forms with validation states and layout.
|
|
15
|
+
* Follows Atomic Design principles as a Molecule component.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <Form onSubmit={handleSubmit} loading={isSubmitting}>
|
|
20
|
+
* <Input name="email" />
|
|
21
|
+
* <Button type="submit">Submit</Button>
|
|
22
|
+
* </Form>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export default function Form({
|
|
26
|
+
children,
|
|
27
|
+
onSubmit,
|
|
28
|
+
loading = false,
|
|
29
|
+
error = null,
|
|
30
|
+
success = null,
|
|
31
|
+
className = "",
|
|
32
|
+
...props
|
|
33
|
+
}: Props) {
|
|
34
|
+
const baseClasses = [
|
|
35
|
+
"space-y-4",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const classes = [
|
|
39
|
+
...baseClasses,
|
|
40
|
+
className,
|
|
41
|
+
].filter(Boolean).join(" ");
|
|
42
|
+
|
|
43
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
if (onSubmit && !loading) {
|
|
46
|
+
onSubmit(e);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<form
|
|
52
|
+
className={classes}
|
|
53
|
+
onSubmit={handleSubmit}
|
|
54
|
+
noValidate
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
{error && (
|
|
59
|
+
<div
|
|
60
|
+
role="alert"
|
|
61
|
+
className="p-3 text-sm text-red-800 bg-red-50 border border-red-200 rounded"
|
|
62
|
+
>
|
|
63
|
+
{error}
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
{success && (
|
|
67
|
+
<div
|
|
68
|
+
role="alert"
|
|
69
|
+
className="p-3 text-sm text-green-800 bg-green-50 border border-green-200 rounded"
|
|
70
|
+
>
|
|
71
|
+
{success}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</form>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import Pagination from "./Pagination";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Pagination> = {
|
|
6
|
+
title: "UI/Molecules/Pagination",
|
|
7
|
+
component: Pagination,
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component: "A pagination component for navigating through pages of data. Supports page info and ellipsis for large page counts.",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
currentPage: {
|
|
17
|
+
control: "number",
|
|
18
|
+
description: "Current page number (1-based)",
|
|
19
|
+
},
|
|
20
|
+
totalPages: {
|
|
21
|
+
control: "number",
|
|
22
|
+
description: "Total number of pages",
|
|
23
|
+
},
|
|
24
|
+
showPageInfo: {
|
|
25
|
+
control: "boolean",
|
|
26
|
+
description: "Whether to show page information",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const Default: StoryObj<typeof Pagination> = {
|
|
32
|
+
render: () => {
|
|
33
|
+
const [page, setPage] = useState(1);
|
|
34
|
+
return (
|
|
35
|
+
<Pagination
|
|
36
|
+
currentPage={page}
|
|
37
|
+
totalPages={10}
|
|
38
|
+
onPageChange={setPage}
|
|
39
|
+
totalItems={100}
|
|
40
|
+
itemsPerPage={10}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const FirstPage: StoryObj<typeof Pagination> = {
|
|
47
|
+
render: () => {
|
|
48
|
+
const [page, setPage] = useState(1);
|
|
49
|
+
return (
|
|
50
|
+
<Pagination
|
|
51
|
+
currentPage={page}
|
|
52
|
+
totalPages={10}
|
|
53
|
+
onPageChange={setPage}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const MiddlePage: StoryObj<typeof Pagination> = {
|
|
60
|
+
render: () => {
|
|
61
|
+
const [page, setPage] = useState(5);
|
|
62
|
+
return (
|
|
63
|
+
<Pagination
|
|
64
|
+
currentPage={page}
|
|
65
|
+
totalPages={10}
|
|
66
|
+
onPageChange={setPage}
|
|
67
|
+
totalItems={100}
|
|
68
|
+
itemsPerPage={10}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const LastPage: StoryObj<typeof Pagination> = {
|
|
75
|
+
render: () => {
|
|
76
|
+
const [page, setPage] = useState(10);
|
|
77
|
+
return (
|
|
78
|
+
<Pagination
|
|
79
|
+
currentPage={page}
|
|
80
|
+
totalPages={10}
|
|
81
|
+
onPageChange={setPage}
|
|
82
|
+
totalItems={100}
|
|
83
|
+
itemsPerPage={10}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const FewPages: StoryObj<typeof Pagination> = {
|
|
90
|
+
render: () => {
|
|
91
|
+
const [page, setPage] = useState(2);
|
|
92
|
+
return (
|
|
93
|
+
<Pagination
|
|
94
|
+
currentPage={page}
|
|
95
|
+
totalPages={3}
|
|
96
|
+
onPageChange={setPage}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const WithoutPageInfo: StoryObj<typeof Pagination> = {
|
|
103
|
+
render: () => {
|
|
104
|
+
const [page, setPage] = useState(1);
|
|
105
|
+
return (
|
|
106
|
+
<Pagination
|
|
107
|
+
currentPage={page}
|
|
108
|
+
totalPages={10}
|
|
109
|
+
onPageChange={setPage}
|
|
110
|
+
showPageInfo={false}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export default meta;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import Pagination from "./Pagination";
|
|
4
|
+
|
|
5
|
+
describe("Pagination", () => {
|
|
6
|
+
it("renders pagination controls", () => {
|
|
7
|
+
const handlePageChange = vi.fn();
|
|
8
|
+
render(
|
|
9
|
+
<Pagination
|
|
10
|
+
currentPage={1}
|
|
11
|
+
totalPages={5}
|
|
12
|
+
onPageChange={handlePageChange}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
expect(screen.getByText("Previous")).toBeInTheDocument();
|
|
16
|
+
expect(screen.getByText("Next")).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("disables Previous button on first page", () => {
|
|
20
|
+
const handlePageChange = vi.fn();
|
|
21
|
+
render(
|
|
22
|
+
<Pagination
|
|
23
|
+
currentPage={1}
|
|
24
|
+
totalPages={5}
|
|
25
|
+
onPageChange={handlePageChange}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
const prevButton = screen.getByText("Previous");
|
|
29
|
+
expect(prevButton).toBeDisabled();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("disables Next button on last page", () => {
|
|
33
|
+
const handlePageChange = vi.fn();
|
|
34
|
+
render(
|
|
35
|
+
<Pagination
|
|
36
|
+
currentPage={5}
|
|
37
|
+
totalPages={5}
|
|
38
|
+
onPageChange={handlePageChange}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
const nextButton = screen.getByText("Next");
|
|
42
|
+
expect(nextButton).toBeDisabled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("calls onPageChange when Next is clicked", () => {
|
|
46
|
+
const handlePageChange = vi.fn();
|
|
47
|
+
render(
|
|
48
|
+
<Pagination
|
|
49
|
+
currentPage={1}
|
|
50
|
+
totalPages={5}
|
|
51
|
+
onPageChange={handlePageChange}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
const nextButton = screen.getByText("Next");
|
|
55
|
+
fireEvent.click(nextButton);
|
|
56
|
+
expect(handlePageChange).toHaveBeenCalledWith(2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("calls onPageChange when Previous is clicked", () => {
|
|
60
|
+
const handlePageChange = vi.fn();
|
|
61
|
+
render(
|
|
62
|
+
<Pagination
|
|
63
|
+
currentPage={2}
|
|
64
|
+
totalPages={5}
|
|
65
|
+
onPageChange={handlePageChange}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
const prevButton = screen.getByText("Previous");
|
|
69
|
+
fireEvent.click(prevButton);
|
|
70
|
+
expect(handlePageChange).toHaveBeenCalledWith(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("calls onPageChange when page number is clicked", () => {
|
|
74
|
+
const handlePageChange = vi.fn();
|
|
75
|
+
render(
|
|
76
|
+
<Pagination
|
|
77
|
+
currentPage={1}
|
|
78
|
+
totalPages={5}
|
|
79
|
+
onPageChange={handlePageChange}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
const page3 = screen.getByLabelText("Go to page 3");
|
|
83
|
+
fireEvent.click(page3);
|
|
84
|
+
expect(handlePageChange).toHaveBeenCalledWith(3);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("shows page info when provided", () => {
|
|
88
|
+
render(
|
|
89
|
+
<Pagination
|
|
90
|
+
currentPage={2}
|
|
91
|
+
totalPages={10}
|
|
92
|
+
onPageChange={() => {}}
|
|
93
|
+
totalItems={100}
|
|
94
|
+
itemsPerPage={10}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
expect(screen.getByText(/Showing 11 to 20 of 100 results/)).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("highlights current page", () => {
|
|
101
|
+
render(
|
|
102
|
+
<Pagination
|
|
103
|
+
currentPage={3}
|
|
104
|
+
totalPages={5}
|
|
105
|
+
onPageChange={() => {}}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
const currentPageButton = screen.getByLabelText("Go to page 3");
|
|
109
|
+
expect(currentPageButton).toHaveAttribute("aria-current", "page");
|
|
110
|
+
expect(currentPageButton).toHaveClass("bg-indigo-600", "text-white");
|
|
111
|
+
});
|
|
112
|
+
});
|