@boxcustodia/library 2.0.0-alpha.16 → 2.0.0-alpha.18
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.js +1 -1
- package/dist/index.d.ts +40 -14
- package/dist/index.es.js +310 -282
- package/package.json +1 -1
- package/src/__doc__/V2.mdx +37 -1
- package/src/components/accordion/accordion.test.tsx +117 -0
- package/src/components/alert-dialog/alert-dialog.test.tsx +208 -22
- package/src/components/alert-dialog/alert-dialog.tsx +4 -4
- package/src/components/avatar/avatar.test.tsx +166 -29
- package/src/components/combobox/combobox.tsx +1 -1
- package/src/components/dialog/dialog.tsx +1 -1
- package/src/components/input/input.stories.tsx +2 -3
- package/src/components/input/input.test.tsx +109 -0
- package/src/components/label/label.tsx +1 -1
- package/src/components/number-input/number-input.test.tsx +155 -48
- package/src/components/toast/toast.tsx +1 -1
- package/src/hooks/index.ts +4 -3
- package/src/hooks/use-clipboard/index.ts +1 -0
- package/src/hooks/use-clipboard/use-clipboard.stories.tsx +168 -0
- package/src/hooks/use-clipboard/use-clipboard.test.tsx +83 -0
- package/src/hooks/use-clipboard/use-clipboard.tsx +64 -0
- package/src/hooks/use-document-title/index.ts +1 -0
- package/src/hooks/use-document-title/use-document-title.stories.tsx +72 -0
- package/src/hooks/use-document-title/use-document-title.test.tsx +75 -0
- package/src/hooks/use-document-title/use-document-title.tsx +32 -0
- package/src/hooks/use-hover/index.ts +1 -0
- package/src/hooks/use-hover/use-hover.stories.tsx +90 -0
- package/src/hooks/use-hover/use-hover.test.tsx +93 -0
- package/src/hooks/use-hover/use-hover.tsx +45 -0
- package/src/hooks/use-on-mount/index.ts +1 -0
- package/src/hooks/use-on-mount/use-on-mount.stories.tsx +85 -0
- package/src/hooks/use-on-mount/use-on-mount.test.tsx +44 -0
- package/src/hooks/use-on-mount/use-on-mount.tsx +13 -0
- package/src/utils/form.tsx +0 -2
- package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +0 -43
- package/src/hooks/useClipboard/__test__/useClipboard.test.tsx +0 -19
- package/src/hooks/useClipboard/index.ts +0 -1
- package/src/hooks/useClipboard/useClipboard.tsx +0 -28
- package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +0 -26
- package/src/hooks/useDocumentTitle/index.ts +0 -1
- package/src/hooks/useDocumentTitle/useDocumentTitle.tsx +0 -11
- package/src/hooks/useHover/__doc__/useHover.stories.tsx +0 -41
- package/src/hooks/useHover/__test__/useHover.test.tsx +0 -45
- package/src/hooks/useHover/index.ts +0 -1
- package/src/hooks/useHover/useHover.tsx +0 -40
package/package.json
CHANGED
package/src/__doc__/V2.mdx
CHANGED
|
@@ -1242,4 +1242,40 @@ import {
|
|
|
1242
1242
|
</PaginationRoot>;
|
|
1243
1243
|
```
|
|
1244
1244
|
|
|
1245
|
-
|
|
1245
|
+
---
|
|
1246
|
+
|
|
1247
|
+
## Hooks
|
|
1248
|
+
|
|
1249
|
+
### 1. useClipboard
|
|
1250
|
+
|
|
1251
|
+
**API rediseñada de tupla a objeto.** En v1 el hook devolvía `[copiedText, copy]` — un string con el último valor copiado y la función de copia. En v2 devuelve un objeto con estado completo: `copied` (booleano que se resetea solo), `error` (Error si falla), `copy` y `reset`.
|
|
1252
|
+
|
|
1253
|
+
**Cambios:**
|
|
1254
|
+
|
|
1255
|
+
- Retorno: `[string | null, (text) => Promise<boolean>]` → `{ copy, reset, copied, error }`.
|
|
1256
|
+
- `copy` ya no es `async` ni devuelve `Promise<boolean>` — es fire-and-forget; inspeccioná `error` para detectar fallos.
|
|
1257
|
+
- `copied` se resetea automáticamente después de `timeout` ms (default `2000`). Configurable vía `useClipboard({ timeout })`.
|
|
1258
|
+
- Se agrega `reset()` para limpiar `copied` y `error` manualmente.
|
|
1259
|
+
|
|
1260
|
+
```diff
|
|
1261
|
+
- const [copiedText, copy] = useClipboard();
|
|
1262
|
+
+ const { copy, copied, error, reset } = useClipboard({ timeout: 2000 });
|
|
1263
|
+
|
|
1264
|
+
- <Button onClick={() => copy("hello")}>
|
|
1265
|
+
- {copiedText === "hello" ? "Copied!" : "Copy"}
|
|
1266
|
+
- </Button>
|
|
1267
|
+
+ <Button onClick={() => copy("hello")}>
|
|
1268
|
+
+ {copied ? "Copied!" : "Copy"}
|
|
1269
|
+
+ </Button>
|
|
1270
|
+
```
|
|
1271
|
+
|
|
1272
|
+
---
|
|
1273
|
+
|
|
1274
|
+
### 2. useHover
|
|
1275
|
+
|
|
1276
|
+
**`isHovering` → `hovered`.** Find-and-replace. Los callbacks `onHoverStart` / `onHoverEnd` siguen funcionando igual.
|
|
1277
|
+
|
|
1278
|
+
```diff
|
|
1279
|
+
- const { ref, isHovering } = useHover<HTMLDivElement>();
|
|
1280
|
+
+ const { ref, hovered } = useHover<HTMLDivElement>();
|
|
1281
|
+
```
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
Accordion,
|
|
5
|
+
AccordionContent,
|
|
6
|
+
AccordionItem,
|
|
7
|
+
AccordionRoot,
|
|
8
|
+
AccordionTrigger,
|
|
9
|
+
} from "../../components";
|
|
10
|
+
import { click } from "../../utils/tests";
|
|
11
|
+
|
|
12
|
+
const items = [
|
|
13
|
+
{ value: "item-1", trigger: "First question", content: "First answer" },
|
|
14
|
+
{ value: "item-2", trigger: "Second question", content: "Second answer" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
describe("Accordion component", () => {
|
|
18
|
+
it("should render correctly", () => {
|
|
19
|
+
render(<Accordion data-testid="Accordion" items={items} />);
|
|
20
|
+
const component = screen.getByTestId("Accordion");
|
|
21
|
+
expect(component).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should show all triggers", () => {
|
|
25
|
+
render(<Accordion items={items} />);
|
|
26
|
+
|
|
27
|
+
expect(screen.getByText(/first question/i)).toBeInTheDocument();
|
|
28
|
+
expect(screen.getByText(/second question/i)).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should open a panel when its trigger is clicked", () => {
|
|
32
|
+
render(<Accordion items={items} />);
|
|
33
|
+
|
|
34
|
+
const trigger = screen.getByRole("button", { name: /first question/i });
|
|
35
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
36
|
+
|
|
37
|
+
click(trigger);
|
|
38
|
+
|
|
39
|
+
waitFor(() => {
|
|
40
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
41
|
+
expect(screen.getByText("First answer")).toBeVisible();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should render disabled items as disabled", () => {
|
|
46
|
+
render(
|
|
47
|
+
<Accordion
|
|
48
|
+
items={[
|
|
49
|
+
{ value: "item-1", trigger: "Available", content: "Open me" },
|
|
50
|
+
{
|
|
51
|
+
value: "item-2",
|
|
52
|
+
trigger: "Disabled",
|
|
53
|
+
content: "Cannot open",
|
|
54
|
+
disabled: true,
|
|
55
|
+
},
|
|
56
|
+
]}
|
|
57
|
+
/>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const disabledTrigger = screen.getByRole("button", { name: /disabled/i });
|
|
61
|
+
expect(disabledTrigger).toBeDisabled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should be controlled", () => {
|
|
65
|
+
const mock = vi.fn();
|
|
66
|
+
render(<Accordion items={items} value={["item-1"]} onValueChange={mock} />);
|
|
67
|
+
|
|
68
|
+
const secondTrigger = screen.getByRole("button", {
|
|
69
|
+
name: /second question/i,
|
|
70
|
+
});
|
|
71
|
+
click(secondTrigger);
|
|
72
|
+
|
|
73
|
+
waitFor(() => {
|
|
74
|
+
expect(mock).toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should apply classNames to each slot", () => {
|
|
79
|
+
render(
|
|
80
|
+
<Accordion
|
|
81
|
+
items={items}
|
|
82
|
+
className="root-class"
|
|
83
|
+
classNames={{
|
|
84
|
+
item: "item-class",
|
|
85
|
+
trigger: "trigger-class",
|
|
86
|
+
content: "content-class",
|
|
87
|
+
}}
|
|
88
|
+
/>,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(document.querySelector('[data-slot="accordion"]')).toHaveClass(
|
|
92
|
+
"root-class",
|
|
93
|
+
);
|
|
94
|
+
expect(document.querySelector('[data-slot="accordion-item"]')).toHaveClass(
|
|
95
|
+
"item-class",
|
|
96
|
+
);
|
|
97
|
+
expect(
|
|
98
|
+
document.querySelector('[data-slot="accordion-trigger"]'),
|
|
99
|
+
).toHaveClass("trigger-class");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should render correctly using primitives", () => {
|
|
103
|
+
render(
|
|
104
|
+
<AccordionRoot defaultValue={["item-1"]}>
|
|
105
|
+
<AccordionItem value="item-1">
|
|
106
|
+
<AccordionTrigger>Primitive trigger</AccordionTrigger>
|
|
107
|
+
<AccordionContent>Primitive content</AccordionContent>
|
|
108
|
+
</AccordionItem>
|
|
109
|
+
</AccordionRoot>,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(screen.getByText(/primitive trigger/i)).toBeInTheDocument();
|
|
113
|
+
expect(
|
|
114
|
+
screen.getByRole("button", { name: /primitive trigger/i }),
|
|
115
|
+
).toHaveAttribute("aria-expanded", "true");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -1,49 +1,235 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
AlertDialog,
|
|
5
|
+
AlertDialogClose,
|
|
6
|
+
AlertDialogDescription,
|
|
7
|
+
AlertDialogFooter,
|
|
8
|
+
AlertDialogHeader,
|
|
9
|
+
AlertDialogPopup,
|
|
10
|
+
AlertDialogPrimitive,
|
|
11
|
+
AlertDialogRoot,
|
|
12
|
+
AlertDialogTitle,
|
|
13
|
+
AlertDialogTrigger,
|
|
14
|
+
Button,
|
|
15
|
+
} from "../../components";
|
|
4
16
|
import { click } from "../../utils/tests";
|
|
5
17
|
|
|
6
|
-
describe("AlertDialog
|
|
7
|
-
it("
|
|
18
|
+
describe("AlertDialog composite", () => {
|
|
19
|
+
it("renders title and description when open", () => {
|
|
8
20
|
render(
|
|
9
|
-
<AlertDialog
|
|
10
|
-
open
|
|
11
|
-
title="Custom title"
|
|
12
|
-
description="Custom description"
|
|
13
|
-
/>,
|
|
21
|
+
<AlertDialog open title="Custom title" description="Custom description" />,
|
|
14
22
|
);
|
|
15
23
|
|
|
16
24
|
expect(screen.getByText("Custom title")).toBeInTheDocument();
|
|
17
25
|
expect(screen.getByText("Custom description")).toBeInTheDocument();
|
|
18
26
|
});
|
|
19
27
|
|
|
20
|
-
it("
|
|
21
|
-
|
|
28
|
+
it("uses Spanish defaults for cancel and action buttons", () => {
|
|
29
|
+
render(<AlertDialog open title="Title" />);
|
|
30
|
+
|
|
31
|
+
expect(
|
|
32
|
+
screen.getByRole("button", { name: "Cancelar" }),
|
|
33
|
+
).toBeInTheDocument();
|
|
34
|
+
expect(
|
|
35
|
+
screen.getByRole("button", { name: "Confirmar" }),
|
|
36
|
+
).toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("renders custom closeText and actionText", () => {
|
|
22
40
|
render(
|
|
23
41
|
<AlertDialog
|
|
24
42
|
open
|
|
25
|
-
title="
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
title="Delete"
|
|
44
|
+
closeText="Mantener"
|
|
45
|
+
actionText="Eliminar"
|
|
28
46
|
/>,
|
|
29
47
|
);
|
|
30
48
|
|
|
49
|
+
expect(screen.getByRole("button", { name: "Mantener" })).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByRole("button", { name: "Eliminar" })).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("calls onClose when cancel button is clicked", () => {
|
|
54
|
+
const onClose = vi.fn();
|
|
55
|
+
render(<AlertDialog open title="Title" onClose={onClose} />);
|
|
56
|
+
|
|
31
57
|
click(screen.getByRole("button", { name: "Cancelar" }));
|
|
32
|
-
|
|
58
|
+
|
|
59
|
+
expect(onClose).toHaveBeenCalledOnce();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("calls onAction when action button is clicked", () => {
|
|
63
|
+
const onAction = vi.fn();
|
|
64
|
+
render(<AlertDialog open title="Title" onAction={onAction} />);
|
|
65
|
+
|
|
66
|
+
click(screen.getByRole("button", { name: "Confirmar" }));
|
|
67
|
+
|
|
68
|
+
expect(onAction).toHaveBeenCalledOnce();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("opens on trigger click", async () => {
|
|
72
|
+
render(
|
|
73
|
+
<AlertDialog
|
|
74
|
+
trigger={<Button>Abrir</Button>}
|
|
75
|
+
title="Title"
|
|
76
|
+
description="Description"
|
|
77
|
+
/>,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(screen.queryByText("Title")).not.toBeInTheDocument();
|
|
81
|
+
|
|
82
|
+
click(screen.getByRole("button", { name: "Abrir" }));
|
|
83
|
+
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(screen.getByText("Title")).toBeInTheDocument();
|
|
86
|
+
expect(screen.getByText("Description")).toBeInTheDocument();
|
|
87
|
+
});
|
|
33
88
|
});
|
|
34
89
|
|
|
35
|
-
it("
|
|
36
|
-
|
|
90
|
+
it("closes after clicking the action button", async () => {
|
|
91
|
+
render(
|
|
92
|
+
<AlertDialog
|
|
93
|
+
trigger={<Button>Abrir</Button>}
|
|
94
|
+
title="Title"
|
|
95
|
+
actionText="Confirmar"
|
|
96
|
+
/>,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
click(screen.getByRole("button", { name: "Abrir" }));
|
|
100
|
+
await waitFor(() =>
|
|
101
|
+
expect(screen.getByText("Title")).toBeInTheDocument(),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
click(screen.getByRole("button", { name: "Confirmar" }));
|
|
105
|
+
|
|
106
|
+
await waitFor(() =>
|
|
107
|
+
expect(screen.queryByText("Title")).not.toBeInTheDocument(),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("renders children between description and footer", () => {
|
|
112
|
+
render(
|
|
113
|
+
<AlertDialog open title="Title" description="Description">
|
|
114
|
+
<div data-testid="custom-body">Extra content</div>
|
|
115
|
+
</AlertDialog>,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(screen.getByTestId("custom-body")).toBeInTheDocument();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("omits the header when no title or description is provided", () => {
|
|
122
|
+
render(<AlertDialog open />);
|
|
123
|
+
|
|
124
|
+
expect(
|
|
125
|
+
document.querySelector('[data-slot="alert-dialog-header"]'),
|
|
126
|
+
).toBeNull();
|
|
127
|
+
expect(
|
|
128
|
+
screen.getByRole("button", { name: "Cancelar" }),
|
|
129
|
+
).toBeInTheDocument();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("applies className to the popup and classNames to internal slots", () => {
|
|
37
133
|
render(
|
|
38
134
|
<AlertDialog
|
|
39
135
|
open
|
|
40
|
-
title="
|
|
41
|
-
description="
|
|
42
|
-
|
|
136
|
+
title="Title"
|
|
137
|
+
description="Description"
|
|
138
|
+
className="popup-class"
|
|
139
|
+
classNames={{
|
|
140
|
+
backdrop: "backdrop-class",
|
|
141
|
+
header: "header-class",
|
|
142
|
+
title: "title-class",
|
|
143
|
+
description: "description-class",
|
|
144
|
+
footer: "footer-class",
|
|
145
|
+
closeButton: "close-class",
|
|
146
|
+
actionButton: "action-class",
|
|
147
|
+
}}
|
|
43
148
|
/>,
|
|
44
149
|
);
|
|
45
150
|
|
|
46
|
-
|
|
47
|
-
|
|
151
|
+
expect(
|
|
152
|
+
document.querySelector('[data-slot="alert-dialog-popup"]'),
|
|
153
|
+
).toHaveClass("popup-class");
|
|
154
|
+
expect(
|
|
155
|
+
document.querySelector('[data-slot="alert-dialog-backdrop"]'),
|
|
156
|
+
).toHaveClass("backdrop-class");
|
|
157
|
+
expect(
|
|
158
|
+
document.querySelector('[data-slot="alert-dialog-header"]'),
|
|
159
|
+
).toHaveClass("header-class");
|
|
160
|
+
expect(screen.getByText("Title")).toHaveClass("title-class");
|
|
161
|
+
expect(screen.getByText("Description")).toHaveClass("description-class");
|
|
162
|
+
expect(
|
|
163
|
+
document.querySelector('[data-slot="alert-dialog-footer"]'),
|
|
164
|
+
).toHaveClass("footer-class");
|
|
165
|
+
expect(screen.getByRole("button", { name: "Cancelar" })).toHaveClass(
|
|
166
|
+
"close-class",
|
|
167
|
+
);
|
|
168
|
+
expect(screen.getByRole("button", { name: "Confirmar" })).toHaveClass(
|
|
169
|
+
"action-class",
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("AlertDialog primitives", () => {
|
|
175
|
+
const setup = () =>
|
|
176
|
+
render(
|
|
177
|
+
<AlertDialogRoot>
|
|
178
|
+
<AlertDialogTrigger render={<Button>Abrir</Button>} />
|
|
179
|
+
<AlertDialogPopup>
|
|
180
|
+
<AlertDialogHeader>
|
|
181
|
+
<AlertDialogTitle>Title</AlertDialogTitle>
|
|
182
|
+
<AlertDialogDescription>Description</AlertDialogDescription>
|
|
183
|
+
</AlertDialogHeader>
|
|
184
|
+
<AlertDialogFooter>
|
|
185
|
+
<AlertDialogClose render={<Button>Cancelar</Button>} />
|
|
186
|
+
<AlertDialogClose render={<Button>Confirmar</Button>} />
|
|
187
|
+
</AlertDialogFooter>
|
|
188
|
+
</AlertDialogPopup>
|
|
189
|
+
</AlertDialogRoot>,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
it("opens on trigger click", async () => {
|
|
193
|
+
setup();
|
|
194
|
+
|
|
195
|
+
click(screen.getByRole("button", { name: "Abrir" }));
|
|
196
|
+
|
|
197
|
+
await waitFor(() => {
|
|
198
|
+
expect(screen.getByText("Title")).toBeInTheDocument();
|
|
199
|
+
expect(screen.getByText("Description")).toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("closes when AlertDialogClose is clicked", async () => {
|
|
204
|
+
setup();
|
|
205
|
+
|
|
206
|
+
click(screen.getByRole("button", { name: "Abrir" }));
|
|
207
|
+
await waitFor(() =>
|
|
208
|
+
expect(screen.getByText("Title")).toBeInTheDocument(),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
click(screen.getByRole("button", { name: "Cancelar" }));
|
|
212
|
+
|
|
213
|
+
await waitFor(() =>
|
|
214
|
+
expect(screen.queryByText("Title")).not.toBeInTheDocument(),
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("is controlled by the open prop on the Root", () => {
|
|
219
|
+
render(
|
|
220
|
+
<AlertDialogRoot open>
|
|
221
|
+
<AlertDialogPopup>
|
|
222
|
+
<AlertDialogTitle>Controlled title</AlertDialogTitle>
|
|
223
|
+
</AlertDialogPopup>
|
|
224
|
+
</AlertDialogRoot>,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(screen.getByText("Controlled title")).toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("exposes the Base UI namespace via AlertDialogPrimitive", () => {
|
|
231
|
+
expect(AlertDialogPrimitive).toBeDefined();
|
|
232
|
+
expect(AlertDialogPrimitive.Root).toBeDefined();
|
|
233
|
+
expect(AlertDialogPrimitive.Popup).toBeDefined();
|
|
48
234
|
});
|
|
49
235
|
});
|
|
@@ -198,9 +198,9 @@ export type AlertDialogProps = Omit<AlertDialogBase.Root.Props, "children"> & {
|
|
|
198
198
|
onAction?: () => void;
|
|
199
199
|
/** Visual variant for the action button. @default "default" */
|
|
200
200
|
variant?: VariantProps<typeof buttonVariants>["variant"];
|
|
201
|
-
/** Label for the cancel button. @default "
|
|
201
|
+
/** Label for the cancel button. @default "Cancelar" */
|
|
202
202
|
closeText?: ReactNode;
|
|
203
|
-
/** Label for the action button. @default "
|
|
203
|
+
/** Label for the action button. @default "Confirmar" */
|
|
204
204
|
actionText?: ReactNode;
|
|
205
205
|
/** Styles the dialog popup panel. */
|
|
206
206
|
className?: string;
|
|
@@ -254,8 +254,8 @@ export function AlertDialog({
|
|
|
254
254
|
onClose,
|
|
255
255
|
onAction,
|
|
256
256
|
variant = "default",
|
|
257
|
-
closeText = "
|
|
258
|
-
actionText = "
|
|
257
|
+
closeText = "Cancelar",
|
|
258
|
+
actionText = "Confirmar",
|
|
259
259
|
className,
|
|
260
260
|
classNames,
|
|
261
261
|
...props
|
|
@@ -1,61 +1,198 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
Avatar,
|
|
5
|
+
AvatarBadge,
|
|
6
|
+
AvatarFallback,
|
|
7
|
+
AvatarGroup,
|
|
8
|
+
AvatarGroupCount,
|
|
9
|
+
AvatarImage,
|
|
10
|
+
AvatarPrimitive,
|
|
11
|
+
AvatarRoot,
|
|
12
|
+
} from "../../components";
|
|
4
13
|
import { extractInitials } from "../../utils";
|
|
5
14
|
|
|
6
|
-
|
|
15
|
+
// Base UI's Avatar uses `new window.Image()` to detect loading state, which
|
|
16
|
+
// never resolves in jsdom. Mock it so a non-empty src completes synchronously
|
|
17
|
+
// and Base UI's fast path (`image.complete && naturalWidth > 0`) renders the
|
|
18
|
+
// underlying `<img>` immediately.
|
|
19
|
+
const OriginalImage = globalThis.Image;
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
class MockImage {
|
|
22
|
+
onload: (() => void) | null = null;
|
|
23
|
+
onerror: (() => void) | null = null;
|
|
24
|
+
referrerPolicy = "";
|
|
25
|
+
crossOrigin: string | null = null;
|
|
26
|
+
naturalWidth = 0;
|
|
27
|
+
complete = false;
|
|
28
|
+
set src(value: string) {
|
|
29
|
+
if (value) {
|
|
30
|
+
this.naturalWidth = 1;
|
|
31
|
+
this.complete = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
vi.stubGlobal("Image", MockImage);
|
|
36
|
+
});
|
|
37
|
+
afterAll(() => {
|
|
38
|
+
vi.stubGlobal("Image", OriginalImage);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("Avatar composite", () => {
|
|
7
42
|
const personName = "lorem ipsum";
|
|
8
43
|
const imageSrc = "https://i.pravatar.cc/300";
|
|
9
44
|
|
|
10
|
-
it("renders
|
|
45
|
+
it("renders the image with src and alt", () => {
|
|
11
46
|
render(<Avatar alt={personName} src={imageSrc} />);
|
|
12
|
-
const image = screen.getByRole("img");
|
|
13
47
|
|
|
14
|
-
|
|
48
|
+
const image = screen.getByRole("img");
|
|
15
49
|
expect(image).toHaveAttribute("alt", personName);
|
|
16
50
|
expect(image).toHaveAttribute("src", imageSrc);
|
|
17
51
|
});
|
|
18
52
|
|
|
19
|
-
it("
|
|
53
|
+
it("falls back to the alt initials when no src is provided", () => {
|
|
20
54
|
render(<Avatar alt={personName} src="" />);
|
|
21
|
-
const nameInitials = extractInitials(personName);
|
|
22
55
|
|
|
23
|
-
|
|
24
|
-
|
|
56
|
+
expect(screen.queryByRole("img")).not.toBeInTheDocument();
|
|
57
|
+
expect(screen.getByText(extractInitials(personName))).toBeInTheDocument();
|
|
58
|
+
});
|
|
25
59
|
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
it("renders a custom fallback node when provided", () => {
|
|
61
|
+
render(
|
|
62
|
+
<Avatar alt={personName} src="" fallback={<span>Custom</span>} />,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(screen.getByText("Custom")).toBeInTheDocument();
|
|
66
|
+
expect(
|
|
67
|
+
screen.queryByText(extractInitials(personName)),
|
|
68
|
+
).not.toBeInTheDocument();
|
|
28
69
|
});
|
|
29
70
|
|
|
30
|
-
it("renders
|
|
31
|
-
render(
|
|
71
|
+
it("renders a badge when provided", () => {
|
|
72
|
+
render(
|
|
73
|
+
<Avatar alt={personName} src={imageSrc} badge={<span>3</span>} />,
|
|
74
|
+
);
|
|
32
75
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
76
|
+
const badge = document.querySelector('[data-slot="avatar-badge"]');
|
|
77
|
+
expect(badge).not.toBeNull();
|
|
78
|
+
expect(badge).toHaveTextContent("3");
|
|
79
|
+
});
|
|
36
80
|
|
|
37
|
-
|
|
38
|
-
|
|
81
|
+
it("omits the badge slot when no badge is provided", () => {
|
|
82
|
+
render(<Avatar alt={personName} src={imageSrc} />);
|
|
83
|
+
|
|
84
|
+
expect(
|
|
85
|
+
document.querySelector('[data-slot="avatar-badge"]'),
|
|
86
|
+
).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("applies the size variant via data-size and class", () => {
|
|
90
|
+
render(<Avatar alt={personName} src={imageSrc} size="sm" />);
|
|
91
|
+
|
|
92
|
+
const root = document.querySelector('[data-slot="avatar"]');
|
|
93
|
+
expect(root).toHaveAttribute("data-size", "sm");
|
|
94
|
+
expect(root).toHaveClass("size-8");
|
|
39
95
|
});
|
|
40
96
|
|
|
41
|
-
it("
|
|
42
|
-
|
|
97
|
+
it("defaults size to md when omitted", () => {
|
|
98
|
+
render(<Avatar alt={personName} src={imageSrc} />);
|
|
99
|
+
|
|
100
|
+
const root = document.querySelector('[data-slot="avatar"]');
|
|
101
|
+
expect(root).toHaveAttribute("data-size", "md");
|
|
102
|
+
expect(root).toHaveClass("size-10");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("applies className to the root and classNames to each slot", () => {
|
|
43
106
|
render(
|
|
44
107
|
<Avatar
|
|
45
108
|
alt={personName}
|
|
46
|
-
src=
|
|
47
|
-
|
|
109
|
+
src=""
|
|
110
|
+
badge={<span>!</span>}
|
|
111
|
+
className="root-class"
|
|
112
|
+
classNames={{
|
|
113
|
+
image: "image-class",
|
|
114
|
+
fallback: "fallback-class",
|
|
115
|
+
badge: "badge-class",
|
|
116
|
+
}}
|
|
48
117
|
/>,
|
|
49
118
|
);
|
|
50
119
|
|
|
51
|
-
|
|
52
|
-
|
|
120
|
+
expect(document.querySelector('[data-slot="avatar"]')).toHaveClass(
|
|
121
|
+
"root-class",
|
|
122
|
+
);
|
|
123
|
+
expect(
|
|
124
|
+
document.querySelector('[data-slot="avatar-fallback"]'),
|
|
125
|
+
).toHaveClass("fallback-class");
|
|
126
|
+
expect(document.querySelector('[data-slot="avatar-badge"]')).toHaveClass(
|
|
127
|
+
"badge-class",
|
|
128
|
+
);
|
|
53
129
|
});
|
|
130
|
+
});
|
|
54
131
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
132
|
+
describe("Avatar primitives", () => {
|
|
133
|
+
it("composes Root, Image, and Fallback directly", () => {
|
|
134
|
+
render(
|
|
135
|
+
<AvatarRoot>
|
|
136
|
+
<AvatarImage src="https://i.pravatar.cc/300" alt="user" />
|
|
137
|
+
<AvatarFallback>U</AvatarFallback>
|
|
138
|
+
</AvatarRoot>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(screen.getByRole("img")).toHaveAttribute("alt", "user");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("renders the Fallback when no Image is provided", () => {
|
|
145
|
+
render(
|
|
146
|
+
<AvatarRoot>
|
|
147
|
+
<AvatarFallback>FB</AvatarFallback>
|
|
148
|
+
</AvatarRoot>,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(screen.getByText("FB")).toBeInTheDocument();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("renders AvatarBadge as an absolutely positioned slot", () => {
|
|
155
|
+
render(
|
|
156
|
+
<AvatarRoot>
|
|
157
|
+
<AvatarFallback>U</AvatarFallback>
|
|
158
|
+
<AvatarBadge>1</AvatarBadge>
|
|
159
|
+
</AvatarRoot>,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const badge = document.querySelector('[data-slot="avatar-badge"]');
|
|
163
|
+
expect(badge).toHaveTextContent("1");
|
|
164
|
+
expect(badge).toHaveClass("absolute");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("renders AvatarGroup wrapping multiple avatars", () => {
|
|
168
|
+
render(
|
|
169
|
+
<AvatarGroup data-testid="group">
|
|
170
|
+
<AvatarRoot>
|
|
171
|
+
<AvatarFallback>A</AvatarFallback>
|
|
172
|
+
</AvatarRoot>
|
|
173
|
+
<AvatarRoot>
|
|
174
|
+
<AvatarFallback>B</AvatarFallback>
|
|
175
|
+
</AvatarRoot>
|
|
176
|
+
</AvatarGroup>,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const group = screen.getByTestId("group");
|
|
180
|
+
expect(group).toHaveAttribute("data-slot", "avatar-group");
|
|
181
|
+
expect(group.querySelectorAll('[data-slot="avatar"]')).toHaveLength(2);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("renders AvatarGroupCount with the chosen size", () => {
|
|
185
|
+
render(<AvatarGroupCount size="lg">+3</AvatarGroupCount>);
|
|
186
|
+
|
|
187
|
+
const count = document.querySelector('[data-slot="avatar-group-count"]');
|
|
188
|
+
expect(count).toHaveTextContent("+3");
|
|
189
|
+
expect(count).toHaveClass("size-14");
|
|
190
|
+
});
|
|
58
191
|
|
|
59
|
-
|
|
192
|
+
it("exposes the Base UI namespace via AvatarPrimitive", () => {
|
|
193
|
+
expect(AvatarPrimitive).toBeDefined();
|
|
194
|
+
expect(AvatarPrimitive.Root).toBeDefined();
|
|
195
|
+
expect(AvatarPrimitive.Image).toBeDefined();
|
|
196
|
+
expect(AvatarPrimitive.Fallback).toBeDefined();
|
|
60
197
|
});
|
|
61
198
|
});
|
|
@@ -545,7 +545,7 @@ export function ComboboxChipRemove(
|
|
|
545
545
|
): React.ReactElement {
|
|
546
546
|
return (
|
|
547
547
|
<ComboboxPrimitive.ChipRemove
|
|
548
|
-
aria-label="
|
|
548
|
+
aria-label="Quitar"
|
|
549
549
|
className="h-full shrink-0 cursor-pointer px-1 opacity-80 hover:opacity-100 [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5"
|
|
550
550
|
data-slot="combobox-chip-remove"
|
|
551
551
|
{...props}
|