@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.
Files changed (45) hide show
  1. package/dist/index.cjs.js +1 -1
  2. package/dist/index.d.ts +40 -14
  3. package/dist/index.es.js +310 -282
  4. package/package.json +1 -1
  5. package/src/__doc__/V2.mdx +37 -1
  6. package/src/components/accordion/accordion.test.tsx +117 -0
  7. package/src/components/alert-dialog/alert-dialog.test.tsx +208 -22
  8. package/src/components/alert-dialog/alert-dialog.tsx +4 -4
  9. package/src/components/avatar/avatar.test.tsx +166 -29
  10. package/src/components/combobox/combobox.tsx +1 -1
  11. package/src/components/dialog/dialog.tsx +1 -1
  12. package/src/components/input/input.stories.tsx +2 -3
  13. package/src/components/input/input.test.tsx +109 -0
  14. package/src/components/label/label.tsx +1 -1
  15. package/src/components/number-input/number-input.test.tsx +155 -48
  16. package/src/components/toast/toast.tsx +1 -1
  17. package/src/hooks/index.ts +4 -3
  18. package/src/hooks/use-clipboard/index.ts +1 -0
  19. package/src/hooks/use-clipboard/use-clipboard.stories.tsx +168 -0
  20. package/src/hooks/use-clipboard/use-clipboard.test.tsx +83 -0
  21. package/src/hooks/use-clipboard/use-clipboard.tsx +64 -0
  22. package/src/hooks/use-document-title/index.ts +1 -0
  23. package/src/hooks/use-document-title/use-document-title.stories.tsx +72 -0
  24. package/src/hooks/use-document-title/use-document-title.test.tsx +75 -0
  25. package/src/hooks/use-document-title/use-document-title.tsx +32 -0
  26. package/src/hooks/use-hover/index.ts +1 -0
  27. package/src/hooks/use-hover/use-hover.stories.tsx +90 -0
  28. package/src/hooks/use-hover/use-hover.test.tsx +93 -0
  29. package/src/hooks/use-hover/use-hover.tsx +45 -0
  30. package/src/hooks/use-on-mount/index.ts +1 -0
  31. package/src/hooks/use-on-mount/use-on-mount.stories.tsx +85 -0
  32. package/src/hooks/use-on-mount/use-on-mount.test.tsx +44 -0
  33. package/src/hooks/use-on-mount/use-on-mount.tsx +13 -0
  34. package/src/utils/form.tsx +0 -2
  35. package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +0 -43
  36. package/src/hooks/useClipboard/__test__/useClipboard.test.tsx +0 -19
  37. package/src/hooks/useClipboard/index.ts +0 -1
  38. package/src/hooks/useClipboard/useClipboard.tsx +0 -28
  39. package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +0 -26
  40. package/src/hooks/useDocumentTitle/index.ts +0 -1
  41. package/src/hooks/useDocumentTitle/useDocumentTitle.tsx +0 -11
  42. package/src/hooks/useHover/__doc__/useHover.stories.tsx +0 -41
  43. package/src/hooks/useHover/__test__/useHover.test.tsx +0 -45
  44. package/src/hooks/useHover/index.ts +0 -1
  45. package/src/hooks/useHover/useHover.tsx +0 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxcustodia/library",
3
- "version": "2.0.0-alpha.16",
3
+ "version": "2.0.0-alpha.18",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
@@ -1242,4 +1242,40 @@ import {
1242
1242
  </PaginationRoot>;
1243
1243
  ```
1244
1244
 
1245
- **Cambió mucho.** Si tu uso es no-trivial, lee la story `Components/Pagination` en Storybook — ahí están todos los modos (composite default, layout numerado, modo URL con `getPageHref` / `linkComponent`, overrides de textos via `texts`).
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 { AlertDialog } from "../../components";
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 component", () => {
7
- it("should show custom title and description", () => {
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("should cancel action", () => {
21
- const onCancel = vi.fn();
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="Custom title"
26
- description="Custom description"
27
- onClose={onCancel}
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
- expect(onCancel).toHaveBeenCalled();
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("should confirm action", () => {
36
- const onConfirm = vi.fn();
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="Custom title"
41
- description="Custom description"
42
- onAction={onConfirm}
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
- click(screen.getByRole("button", { name: "Aceptar" }));
47
- expect(onConfirm).toHaveBeenCalled();
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 "Cancel" */
201
+ /** Label for the cancel button. @default "Cancelar" */
202
202
  closeText?: ReactNode;
203
- /** Label for the action button. @default "Confirm" */
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 = "Cancel",
258
- actionText = "Confirm",
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 { Avatar } from "../../components";
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
- describe("Avatar component", () => {
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 correctly", () => {
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
- expect(image).toBeInTheDocument();
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("renders correctly without src", () => {
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
- const image = screen.queryByRole("img");
24
- const fallbackText = screen.getByText(nameInitials);
56
+ expect(screen.queryByRole("img")).not.toBeInTheDocument();
57
+ expect(screen.getByText(extractInitials(personName))).toBeInTheDocument();
58
+ });
25
59
 
26
- expect(image).not.toBeInTheDocument();
27
- expect(fallbackText).toBeInTheDocument();
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 correctly with onError", () => {
31
- render(<Avatar alt={personName} src="" />);
71
+ it("renders a badge when provided", () => {
72
+ render(
73
+ <Avatar alt={personName} src={imageSrc} badge={<span>3</span>} />,
74
+ );
32
75
 
33
- const nameInitials = extractInitials(personName);
34
- const image = screen.queryByRole("img");
35
- const fallbackText = screen.getByText(nameInitials);
76
+ const badge = document.querySelector('[data-slot="avatar-badge"]');
77
+ expect(badge).not.toBeNull();
78
+ expect(badge).toHaveTextContent("3");
79
+ });
36
80
 
37
- expect(image).not.toBeInTheDocument();
38
- expect(fallbackText).toBeInTheDocument();
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("renders correctly with imageProps", () => {
42
- const testClassName = "test-class";
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={imageSrc}
47
- imageProps={{ className: testClassName }}
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
- const image = screen.getByRole("img");
52
- expect(image).toHaveClass(testClassName);
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
- it("renders correctly with size variants", () => {
56
- render(<Avatar alt={personName} src={imageSrc} size="sm" />);
57
- const avatar = screen.getByRole("img").parentElement;
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
- expect(avatar).toHaveClass("w-8 h-8");
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="Remove"
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}