@1doc/1ds-react 2.13.0 → 2.15.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +62 -59
  2. package/dist/components/AILabel/AILabel.css +1 -1
  3. package/dist/components/Alert/Alert.css +1 -1
  4. package/dist/components/AppShell/AppShell.css +4 -0
  5. package/dist/components/AttachmentCard/AttachmentCard.css +2 -2
  6. package/dist/components/Avatar/Avatar.d.ts +8 -3
  7. package/dist/components/Avatar/Avatar.js +28 -4
  8. package/dist/components/BottomNavigation/BottomNavigation.css +2 -2
  9. package/dist/components/BottomSheet/BottomSheet.css +2 -2
  10. package/dist/components/Box/Box.css +3 -3
  11. package/dist/components/Button/Button.css +6 -1
  12. package/dist/components/Button/Button.d.ts +11 -2
  13. package/dist/components/Button/Button.js +15 -3
  14. package/dist/components/Card/Card.css +5 -5
  15. package/dist/components/Chart/Chart.css +1 -1
  16. package/dist/components/Coachmark/Coachmark.css +1 -1
  17. package/dist/components/Combobox/Combobox.css +1 -1
  18. package/dist/components/ConfirmDialog/ConfirmDialog.css +1 -1
  19. package/dist/components/Counter/Counter.css +43 -0
  20. package/dist/components/Counter/Counter.d.ts +57 -0
  21. package/dist/components/Counter/Counter.js +99 -0
  22. package/dist/components/DatePicker/DatePicker.css +1 -1
  23. package/dist/components/Drawer/Drawer.css +2 -2
  24. package/dist/components/Dropdown/Dropdown.css +3 -1
  25. package/dist/components/Dropdown/Dropdown.d.ts +17 -0
  26. package/dist/components/Dropdown/Dropdown.js +22 -2
  27. package/dist/components/EmptyState/EmptyState.css +12 -0
  28. package/dist/components/EmptyState/EmptyState.d.ts +3 -0
  29. package/dist/components/EmptyState/EmptyState.js +1 -0
  30. package/dist/components/Kanban/Kanban.css +2 -2
  31. package/dist/components/Kanban/KanbanCard.css +2 -2
  32. package/dist/components/List/List.css +83 -0
  33. package/dist/components/List/List.d.ts +3 -0
  34. package/dist/components/List/List.js +6 -2
  35. package/dist/components/List/ListItem.d.ts +19 -0
  36. package/dist/components/List/ListItem.js +5 -1
  37. package/dist/components/Modal/Modal.css +3 -4
  38. package/dist/components/MultiSelect/MultiSelect.css +1 -1
  39. package/dist/components/OrgChart/OrgChart.css +2 -2
  40. package/dist/components/PhoneInput/PhoneInput.css +2 -2
  41. package/dist/components/Popover/Popover.css +1 -1
  42. package/dist/components/SegmentedControl/SegmentedControl.css +1 -1
  43. package/dist/components/Select/Select.css +1 -1
  44. package/dist/components/StatCard/StatCard.css +99 -5
  45. package/dist/components/StatCard/StatCard.d.ts +12 -6
  46. package/dist/components/StatCard/StatCard.js +6 -2
  47. package/dist/components/Switch/Switch.css +2 -2
  48. package/dist/components/Text/Text.d.ts +6 -0
  49. package/dist/components/TimePicker/TimePicker.css +1 -1
  50. package/dist/components/Toast/Toast.css +2 -2
  51. package/dist/components/Tooltip/Tooltip.css +23 -3
  52. package/dist/components/Tooltip/Tooltip.js +33 -2
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.js +1 -0
  55. package/dist/teste/PedidosMatheus.test.d.ts +1 -0
  56. package/dist/teste/PedidosMatheus.test.js +91 -0
  57. package/dist/teste/PedidosMatheus2.test.d.ts +1 -0
  58. package/dist/teste/PedidosMatheus2.test.js +82 -0
  59. package/dist/teste/SegmentedControlSuperficie.test.js +1 -1
  60. package/dist/teste/StatCardTamanhoSm.test.js +8 -0
  61. package/package.json +3 -3
  62. package/src/index.ts +2 -0
  63. package/src/teste/PedidosMatheus.test.tsx +129 -0
  64. package/src/teste/PedidosMatheus2.test.tsx +102 -0
  65. package/src/teste/SegmentedControlSuperficie.test.tsx +1 -1
  66. package/src/teste/StatCardTamanhoSm.test.tsx +9 -0
@@ -123,7 +123,7 @@
123
123
  background: var(--umds-semantic-color-background-elevated); /* superfície elevada: igual ao white no light; mais clara no dark (decisão 25/07) */
124
124
  border: 1px solid var(--umds-semantic-color-neutral-light-medium); /* #EBEBEB */
125
125
  border-radius: var(--umds-primitive-border-radius-sm); /* 8px — protótipo --radius-lg 12px, aproximado para sm (coerente com input e dígitos) */
126
- box-shadow: var(--umds-semantic-shadow-md);
126
+ box-shadow: var(--umds-semantic-shadow-bottom-md);
127
127
  padding: var(--umds-primitive-spacing-xxxs); /* 16px — protótipo --space-4 */
128
128
  box-sizing: border-box;
129
129
  animation: umds-timepicker-in var(--umds-semantic-motion-duration-fast) var(--umds-semantic-motion-easing-standard) both;
@@ -4,7 +4,7 @@
4
4
  Variantes semânticas (prop `variant`): positive, negative, info, neutral
5
5
  (warning do protótipo mesclado no info — ambos laranja feedback-info).
6
6
  Tokens do protótipo mapeados para os tokens 1DS.
7
- Snap 16/07 (decisões novas): sombra → --umds-semantic-shadow-md (token novo,
7
+ Snap 16/07 (decisões novas): sombra → --umds-semantic-shadow-bottom-md (token novo,
8
8
  valor idêntico ao literal do protótipo); radius 12px → md 16px (overlay
9
9
  flutuante → md, como Dropdown/Modal); radius 6px → sm 8px; font-size
10
10
  11px → xxxs 12px; spacing 12px → xxxs 16px (mesmas decisões do Alert —
@@ -24,7 +24,7 @@
24
24
  padding: var(--umds-primitive-spacing-xxxs, 16px); /* era 12px 16px (protótipo --space-3/--space-4) — snap 16/07 */
25
25
  border-radius: var(--umds-primitive-border-radius-sm); /* 8px universal (rodada ago/2026) — a exceção "overlay flutuante → md" caiu */
26
26
  border: 1px solid transparent;
27
- box-shadow: var(--umds-semantic-shadow-md); /* protótipo --shadow-md — token novo com valor idêntico */
27
+ box-shadow: var(--umds-semantic-shadow-bottom-md); /* protótipo --shadow-md — token novo com valor idêntico */
28
28
  font-family: var(--umds-semantic-font-family-base);
29
29
  box-sizing: border-box;
30
30
  /* O Portal usa display: contents e não posiciona nada — o próprio toast
@@ -8,6 +8,9 @@
8
8
  ───────────────────────────────────────────── */
9
9
 
10
10
  .umds-tooltip {
11
+ /* Bloco de contenção da cópia escondida (__descricao, absolute): sem ele,
12
+ ela escapa do overflow de uma área que rola e estica a página. */
13
+ position: relative;
11
14
  display: inline-flex;
12
15
  height: fit-content;
13
16
  }
@@ -44,9 +47,12 @@
44
47
  flex-direction: column;
45
48
  position: relative;
46
49
  padding: var(--umds-primitive-spacing-mini); /* protótipo --space-3: sem equivalente 1DS exato */
47
- background-color: var(--umds-semantic-color-neutral-light-pure);
50
+ /* Superfície elevada, como o Card: igual ao branco nos temas claros e no
51
+ alto contraste; nos escuros é mais clara que a página (o neutral-light-pure
52
+ de antes era quase da cor do fundo, e o balão sumia). */
53
+ background-color: var(--umds-semantic-color-background-elevated);
48
54
  border-radius: var(--umds-primitive-border-radius-sm); /* 8px */
49
- box-shadow: var(--umds-primitive-shadow-level-2);
55
+ box-shadow: var(--umds-semantic-shadow-bottom-lg);
50
56
  font-family: var(--umds-semantic-font-family-base);
51
57
  font-size: var(--umds-primitive-font-size-xxxs); /* 12px */
52
58
  line-height: 1.25;
@@ -90,7 +96,7 @@
90
96
  position: absolute;
91
97
  width: 8px;
92
98
  height: 8px;
93
- background-color: var(--umds-semantic-color-neutral-light-pure);
99
+ background-color: var(--umds-semantic-color-background-elevated);
94
100
  transform: rotate(45deg);
95
101
  }
96
102
 
@@ -178,6 +184,20 @@
178
184
  border-color: var(--umds-semantic-color-background-dark);
179
185
  }
180
186
 
187
+ /* ── Temas escuros (1doc-dark e os *-dark da paleta) ──
188
+ A sombra preta não aparece sobre fundo escuro: o balão ganha o traço do Card
189
+ (neutral-light-medium), por dentro para não mudar o tamanho. A seta troca o
190
+ traço das duas faces expostas pelo mesmo tom e continua o contorno. O
191
+ variant="dark" (balão claro invertido) não precisa: ele contrasta sozinho. */
192
+ [data-theme$="-dark"] .umds-tooltip__bubble:not(.umds-tooltip__bubble--dark) {
193
+ outline: var(--umds-primitive-border-width-hairline) solid var(--umds-semantic-color-neutral-light-medium);
194
+ outline-offset: calc(-1 * var(--umds-primitive-border-width-hairline));
195
+ }
196
+
197
+ [data-theme$="-dark"] .umds-tooltip__bubble:not(.umds-tooltip__bubble--dark) .umds-tooltip__arrow {
198
+ border-color: var(--umds-semantic-color-neutral-light-medium);
199
+ }
200
+
181
201
  /* ── Alto contraste ──
182
202
  O balão usa neutral-light-pure, que neste tema é preto como a página, e a
183
203
  sombra não aparece: sem traço o balão some. Outline para dentro (não
@@ -2,7 +2,7 @@
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import './Tooltip.css';
4
4
  import { useState, useRef, useEffect } from "react";
5
- import { computePosition, autoUpdate, offset, flip, shift, } from "@floating-ui/dom";
5
+ import { computePosition, autoUpdate, offset, flip, shift, arrow, } from "@floating-ui/dom";
6
6
  import Icon from "../Icon/Icon";
7
7
  import Portal from "../Portal/Portal";
8
8
  import { gerarId } from "../../utils/id";
@@ -28,6 +28,7 @@ function Tooltip(props) {
28
28
  const [posY, setPosY] = useState(() => 0);
29
29
  const [finalSide, setFinalSide] = useState(() => "top");
30
30
  const [finalAlign, setFinalAlign] = useState(() => "center");
31
+ const [arrowOffset, setArrowOffset] = useState(() => -1);
31
32
  const [bubbleId, setBubbleId] = useState(() => "");
32
33
  const [descId, setDescId] = useState(() => "");
33
34
  function bubbleIdText() {
@@ -92,6 +93,12 @@ function Tooltip(props) {
92
93
  padding: 8,
93
94
  }),
94
95
  ];
96
+ // A seta acompanha o gatilho quando o balão é empurrado para caber.
97
+ if (arrowRef.current)
98
+ middlewares.push(arrow({
99
+ element: arrowRef.current,
100
+ padding: 8,
101
+ }));
95
102
  computePosition(triggerRef.current, floatingRef.current, {
96
103
  placement,
97
104
  strategy: "fixed",
@@ -105,6 +112,14 @@ function Tooltip(props) {
105
112
  const parts = position.placement.split("-");
106
113
  setFinalSide(parts[0]);
107
114
  setFinalAlign(parts[1] || "center");
115
+ // Só quando o shift deslocou o balão a seta sai da posição estática
116
+ // (as 12 classes side×align): nos outros casos o visual não muda.
117
+ const deslocamento = position.middlewareData.shift;
118
+ const seta = position.middlewareData.arrow;
119
+ const empurrado = !!deslocamento && (deslocamento.x !== 0 || deslocamento.y !== 0);
120
+ const vertical = parts[0] === "left" || parts[0] === "right";
121
+ const eixo = seta ? (vertical ? seta.y : seta.x) : undefined;
122
+ setArrowOffset(empurrado && typeof eixo === "number" ? eixo : -1);
108
123
  setPositioned(true);
109
124
  });
110
125
  }
@@ -273,6 +288,22 @@ function Tooltip(props) {
273
288
  "umds-tooltip__arrow--align-" + finalAlign,
274
289
  ].join(" ");
275
290
  }
291
+ function arrowStyle() {
292
+ if (arrowOffset < 0)
293
+ return undefined;
294
+ if (finalSide === "left" || finalSide === "right") {
295
+ return {
296
+ top: arrowOffset + "px",
297
+ bottom: "auto",
298
+ transform: "rotate(45deg)",
299
+ };
300
+ }
301
+ return {
302
+ left: arrowOffset + "px",
303
+ right: "auto",
304
+ transform: "rotate(45deg)",
305
+ };
306
+ }
276
307
  useEffect(() => {
277
308
  if (!triggerRef.current)
278
309
  return;
@@ -315,6 +346,6 @@ function Tooltip(props) {
315
346
  stopKeydownWatch();
316
347
  };
317
348
  }, []);
318
- return (_jsxs("span", { ref: triggerRef, className: ["umds-tooltip", props.className].filter(Boolean).join(" "), onMouseEnter: (event) => handleOpen(), onMouseLeave: (event) => handleClose(event), children: [_jsx(_Fragment, { children: props.children }), _jsxs("span", { className: "umds-tooltip__descricao", "aria-hidden": "true", id: descIdText(), children: [props.title ? _jsx(_Fragment, { children: tituloDescrito() }) : null, props.content ? _jsx(_Fragment, { children: props.content }) : null] }), _jsx(Portal, { children: mounted ? (_jsx("span", { ref: floatingRef, className: floatingClasses(), style: floatingStyle(), children: _jsxs("span", { role: "tooltip", className: bubbleClasses(), id: bubbleIdText(), "aria-hidden": !open, children: [_jsxs("span", { className: "umds-tooltip__row", children: [props.icon ? (_jsx(Icon, { size: "sm", variant: "inherit", icon: props.icon })) : null, _jsxs("span", { className: "umds-tooltip__text", children: [props.title ? (_jsx("span", { className: "umds-tooltip__title", children: props.title })) : null, props.content ? (_jsx("span", { className: "umds-tooltip__content", children: _jsx(_Fragment, { children: props.content }) })) : null] })] }), _jsx("span", { ref: arrowRef, className: arrowClasses() })] }) })) : null })] }));
349
+ return (_jsxs("span", { ref: triggerRef, className: ["umds-tooltip", props.className].filter(Boolean).join(" "), onMouseEnter: (event) => handleOpen(), onMouseLeave: (event) => handleClose(event), children: [_jsx(_Fragment, { children: props.children }), _jsxs("span", { className: "umds-tooltip__descricao", "aria-hidden": "true", id: descIdText(), children: [props.title ? _jsx(_Fragment, { children: tituloDescrito() }) : null, props.content ? _jsx(_Fragment, { children: props.content }) : null] }), _jsx(Portal, { children: mounted ? (_jsx("span", { ref: floatingRef, className: floatingClasses(), style: floatingStyle(), children: _jsxs("span", { role: "tooltip", className: bubbleClasses(), id: bubbleIdText(), "aria-hidden": !open, children: [_jsxs("span", { className: "umds-tooltip__row", children: [props.icon ? (_jsx(Icon, { size: "sm", variant: "inherit", icon: props.icon })) : null, _jsxs("span", { className: "umds-tooltip__text", children: [props.title ? (_jsx("span", { className: "umds-tooltip__title", children: props.title })) : null, props.content ? (_jsx("span", { className: "umds-tooltip__content", children: _jsx(_Fragment, { children: props.content }) })) : null] })] }), _jsx("span", { ref: arrowRef, className: arrowClasses(), style: arrowStyle() })] }) })) : null })] }));
319
350
  }
320
351
  export default Tooltip;
package/dist/index.d.ts CHANGED
@@ -50,6 +50,8 @@ export { default as Input } from "./components/Input/Input";
50
50
  export { default as Kanban } from "./components/Kanban/Kanban";
51
51
  export { default as KanbanCard } from "./components/Kanban/KanbanCard";
52
52
  export { default as KanbanColumn } from "./components/Kanban/KanbanColumn";
53
+ export { default as Counter } from "./components/Counter/Counter";
54
+ export type * from "./components/Counter/Counter";
53
55
  export { default as Kbd } from "./components/Kbd/Kbd";
54
56
  export { default as Link } from "./components/Link/Link";
55
57
  export { default as LineChart } from "./components/Chart/LineChart";
package/dist/index.js CHANGED
@@ -56,6 +56,7 @@ export { default as Input } from "./components/Input/Input";
56
56
  export { default as Kanban } from "./components/Kanban/Kanban";
57
57
  export { default as KanbanCard } from "./components/Kanban/KanbanCard";
58
58
  export { default as KanbanColumn } from "./components/Kanban/KanbanColumn";
59
+ export { default as Counter } from "./components/Counter/Counter";
59
60
  export { default as Kbd } from "./components/Kbd/Kbd";
60
61
  export { default as Link } from "./components/Link/Link";
61
62
  export { default as LineChart } from "./components/Chart/LineChart";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { act, fireEvent, render, screen } from "@testing-library/react";
4
+ import Button from "../components/Button/Button";
5
+ import Counter from "../components/Counter/Counter";
6
+ import Dropdown from "../components/Dropdown/Dropdown";
7
+ import ListItem from "../components/List/ListItem";
8
+ import StatCard from "../components/StatCard/StatCard";
9
+ describe("Counter", () => {
10
+ it("sem animação mostra o valor final formatado em pt-BR", () => {
11
+ const { container } = render(_jsx(Counter, { value: 1284, disableAnimation: true }));
12
+ expect(container.querySelector(".umds-counter__numero")).toHaveTextContent("1.284");
13
+ expect(container.querySelector(".umds-counter__numero")).toHaveAttribute("aria-hidden", "true");
14
+ expect(container.querySelector(".umds-counter__leitura")).toHaveTextContent("1.284");
15
+ });
16
+ it("casas decimais seguem o value; prefixo e sufixo entram no texto", () => {
17
+ const { container } = render(_jsxs(_Fragment, { children: [_jsx(Counter, { value: 22.31, suffix: "%", disableAnimation: true }), _jsx(Counter, { value: 15890.5, prefix: "R$ ", decimals: 2, disableAnimation: true })] }));
18
+ const leituras = container.querySelectorAll(".umds-counter__leitura");
19
+ expect(leituras[0]).toHaveTextContent("22,31%");
20
+ expect(leituras[1].textContent).toBe("R$ 15.890,50");
21
+ });
22
+ it("conta do from até o valor e termina no valor exato", async () => {
23
+ vi.useFakeTimers();
24
+ const { container } = render(_jsx(Counter, { value: 500, duration: 100 }));
25
+ const numero = () => container.querySelector(".umds-counter__numero").textContent;
26
+ expect(numero()).toBe("0");
27
+ await act(async () => {
28
+ vi.advanceTimersByTime(500);
29
+ });
30
+ expect(numero()).toBe("500");
31
+ // A reserva e a leitura já nascem com o valor final.
32
+ expect(container.querySelector(".umds-counter__leitura")).toHaveTextContent("500");
33
+ vi.useRealTimers();
34
+ });
35
+ });
36
+ describe("ListItem · actions", () => {
37
+ it("as ações ficam no li, fora do elemento clicável", () => {
38
+ const { container } = render(_jsx("ul", { children: _jsx(ListItem, { href: "#doc", title: "Memorando", actions: _jsx("button", { type: "button", children: "Mais" }) }) }));
39
+ const acao = screen.getByRole("button", { name: "Mais" });
40
+ expect(acao.closest("a")).toBeNull();
41
+ expect(acao.parentElement).toHaveClass("umds-list-item__actions");
42
+ expect(container.querySelector("li > a")).toHaveAttribute("href", "#doc");
43
+ });
44
+ });
45
+ describe("Button · href", () => {
46
+ it("com href é um link; desabilitado volta a ser botão", () => {
47
+ render(_jsxs(_Fragment, { children: [_jsx(Button, { href: "#inbox", children: "Inbox" }), _jsx(Button, { href: "#cal", target: "_blank", children: "Calend\u00E1rio" }), _jsx(Button, { href: "#fila", disabled: true, children: "Fila" })] }));
48
+ expect(screen.getByRole("link", { name: "Inbox" })).toHaveAttribute("href", "#inbox");
49
+ expect(screen.getByRole("link", { name: "Calendário" })).toHaveAttribute("rel", "noopener noreferrer");
50
+ expect(screen.getByRole("button", { name: "Fila" })).toBeDisabled();
51
+ });
52
+ it("loading segura a navegação do link", () => {
53
+ render(_jsx(Button, { href: "#inbox", loading: true, loadingPosition: "start", children: "Inbox" }));
54
+ const link = screen.getByRole("link", { name: "Inbox" });
55
+ const evento = new MouseEvent("click", { bubbles: true, cancelable: true });
56
+ link.dispatchEvent(evento);
57
+ expect(evento.defaultPrevented).toBe(true);
58
+ });
59
+ });
60
+ describe("Dropdown · href", () => {
61
+ it("item com href vira <a role=menuitem>, e o onSelect continua", async () => {
62
+ const onSelect = vi.fn();
63
+ render(_jsx(Dropdown, { ariaLabel: "A\u00E7\u00F5es", onSelect: onSelect, items: [
64
+ { key: "assinar", label: "Assinar", href: "#assinar" },
65
+ { key: "arquivar", label: "Arquivar" },
66
+ ], children: _jsx("button", { type: "button", children: "Abrir" }) }));
67
+ fireEvent.click(screen.getByRole("button", { name: "Abrir" }));
68
+ const assinar = await screen.findByRole("menuitem", { name: "Assinar" });
69
+ expect(assinar.tagName).toBe("A");
70
+ expect(assinar).toHaveAttribute("href", "#assinar");
71
+ expect(screen.getByRole("menuitem", { name: "Arquivar" }).tagName).toBe("BUTTON");
72
+ fireEvent.click(assinar);
73
+ expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ key: "assinar" }));
74
+ });
75
+ });
76
+ describe("StatCard · loading e secondaryTone", () => {
77
+ it("loading troca os números por Skeleton e marca aria-busy", () => {
78
+ const { container } = render(_jsx(StatCard, { value: "22,31%", label: "Qualidade", secondaryValue: "14,11%", trendLabel: "+2%", trend: "up", loading: true }));
79
+ const cartao = container.querySelector(".umds-stat-card");
80
+ expect(cartao).toHaveAttribute("aria-busy", "true");
81
+ expect(cartao.textContent).not.toContain("22,31%");
82
+ expect(cartao.textContent).not.toContain("14,11%");
83
+ expect(cartao.textContent).not.toContain("+2%");
84
+ expect(cartao.textContent).toContain("Qualidade");
85
+ });
86
+ it("secondaryTone escreve a classe; default não", () => {
87
+ const { container } = render(_jsxs(_Fragment, { children: [_jsx(StatCard, { value: "1", secondaryValue: "2", secondaryTone: "positive" }), _jsx(StatCard, { value: "1", secondaryValue: "2", secondaryTone: "default" })] }));
88
+ expect(container.children[0]).toHaveClass("umds-stat-card--secundario-positive");
89
+ expect(container.children[1].className).not.toContain("secundario-");
90
+ });
91
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,82 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import Avatar from "../components/Avatar/Avatar";
5
+ import Dropdown from "../components/Dropdown/Dropdown";
6
+ import EmptyState from "../components/EmptyState/EmptyState";
7
+ import List from "../components/List/List";
8
+ import ListItem from "../components/List/ListItem";
9
+ import { lerArquivo } from "./lerArquivo";
10
+ describe("Avatar · fallback e name", () => {
11
+ it("a foto que falha volta para as iniciais do name", () => {
12
+ const onError = vi.fn();
13
+ const { container } = render(_jsx(Avatar, { src: "/quebrada.png", name: "Cris Lima", onError: onError }));
14
+ fireEvent.error(container.querySelector("img"));
15
+ expect(container.querySelector("img")).toBeNull();
16
+ expect(container.textContent).toBe("CL");
17
+ expect(container.firstElementChild).toHaveClass("umds-avatar--letter");
18
+ expect(onError).toHaveBeenCalledTimes(1);
19
+ });
20
+ it("sem name, a falha mostra o children", () => {
21
+ const { container } = render(_jsx(Avatar, { src: "/quebrada.png", children: "ML" }));
22
+ fireEvent.error(container.querySelector("img"));
23
+ expect(container.textContent).toBe("ML");
24
+ });
25
+ it("um src novo tenta a foto de novo", () => {
26
+ const { container, rerender } = render(_jsx(Avatar, { src: "/quebrada.png", name: "Cris Lima" }));
27
+ fireEvent.error(container.querySelector("img"));
28
+ rerender(_jsx(Avatar, { src: "/boa.png", name: "Cris Lima" }));
29
+ expect(container.querySelector("img")).toHaveAttribute("src", "/boa.png");
30
+ });
31
+ it("name gera as iniciais (primeiro e último nome) e o nome acessível", () => {
32
+ const { container } = render(_jsx(Avatar, { name: "Matheus de Souza" }));
33
+ expect(container.textContent).toBe("MS");
34
+ expect(container.firstElementChild).toHaveAttribute("aria-label", "Matheus de Souza");
35
+ expect(container.firstElementChild).toHaveAttribute("role", "img");
36
+ });
37
+ });
38
+ describe("Dropdown · onSelect no item", () => {
39
+ it("chama a ação do item com os campos próprios, e depois o onSelect do Dropdown", async () => {
40
+ const ordem = [];
41
+ const doItem = vi.fn((item) => ordem.push("item:" + item.command));
42
+ const doMenu = vi.fn(() => ordem.push("menu"));
43
+ render(_jsx(Dropdown, { ariaLabel: "A\u00E7\u00F5es", onSelect: doMenu, items: [{ key: "a", label: "Assinar", command: "assinar", onSelect: doItem }], children: _jsx("button", { type: "button", children: "Abrir" }) }));
44
+ fireEvent.click(screen.getByRole("button", { name: "Abrir" }));
45
+ fireEvent.click(await screen.findByRole("menuitem", { name: "Assinar" }));
46
+ expect(ordem).toEqual(["item:assinar", "menu"]);
47
+ });
48
+ });
49
+ describe("ListItem · variant e li", () => {
50
+ it("variant primary marca a classe sem aria-current", () => {
51
+ render(_jsx("ul", { children: _jsx(ListItem, { variant: "primary", href: "#c", title: "Circular" }) }));
52
+ const link = screen.getByRole("link", { name: "Circular" });
53
+ expect(link).toHaveClass("umds-list-item--primary");
54
+ expect(link).not.toHaveAttribute("aria-current");
55
+ });
56
+ it("liClassName e liStyle vão para o li", () => {
57
+ const { container } = render(_jsx("ul", { children: _jsx(ListItem, { title: "Memorando", liClassName: "entra", liStyle: { "--ordem": 2 }, onClick: () => { } }) }));
58
+ const li = container.querySelector("li");
59
+ expect(li).toHaveClass("umds-list__item");
60
+ expect(li).toHaveClass("entra");
61
+ expect(li.style.getPropertyValue("--ordem")).toBe("2");
62
+ });
63
+ });
64
+ describe("EmptyState · fill e List · emptyFill", () => {
65
+ it("fill escreve a classe; List repassa emptyFill", () => {
66
+ const { container } = render(_jsxs(_Fragment, { children: [_jsx(EmptyState, { title: "Vazio", fill: true }), _jsx(List, { ariaLabel: "Docs", isEmpty: true, emptyFill: true })] }));
67
+ expect(container.querySelector(".umds-empty-state--fill")).not.toBeNull();
68
+ expect(container.querySelector(".umds-list--empty-fill .umds-empty-state--fill")).not.toBeNull();
69
+ });
70
+ });
71
+ describe("AppShell e StatCard · CSS", () => {
72
+ it("o AppShell dá a cor de texto do tema a quem herda", async () => {
73
+ const css = (await lerArquivo("../core/src/components/AppShell/AppShell.css")).replace(/\r\n/g, "\n");
74
+ const bloco = css.slice(css.indexOf(".umds-app-shell {"), css.indexOf("}", css.indexOf(".umds-app-shell {")));
75
+ expect(bloco).toContain("color: var(--umds-semantic-color-neutral-dark-pure)");
76
+ });
77
+ it("a StatCard com segunda medida empilha em largura pequena", async () => {
78
+ const css = (await lerArquivo("../core/src/components/StatCard/StatCard.css")).replace(/\r\n/g, "\n");
79
+ expect(css).toContain("container-type: inline-size");
80
+ expect(css).toMatch(/@container \(max-width: 240px\)/);
81
+ });
82
+ });
@@ -27,7 +27,7 @@ describe("SegmentedControl · variant surface", () => {
27
27
  const escolhido = bloco(t, ".umds-segmented--surface .umds-segmented__input:checked + .umds-segmented__item,\n.umds-segmented--surface .umds-segmented__input:checked + .umds-segmented__item:hover");
28
28
  expect(escolhido).toMatch(/background-color:\s*var\(--umds-semantic-color-background-white\)/);
29
29
  expect(escolhido).toMatch(/font-weight:\s*var\(--umds-primitive-font-weight-bold\)/);
30
- expect(escolhido).toMatch(/box-shadow:\s*var\(--umds-semantic-shadow-md\)/);
30
+ expect(escolhido).toMatch(/box-shadow:\s*var\(--umds-semantic-shadow-bottom-md\)/);
31
31
  expect(bloco(t, ".umds-segmented--surface .umds-segmented__track")).toMatch(/background-color:\s*var\(--umds-semantic-color-neutral-light-medium\)/);
32
32
  expect(t).toMatch(/\.umds-segmented__input:checked \+ \.umds-segmented__item,\n\.umds-segmented__input:checked \+ \.umds-segmented__item:hover \{\n\s*background-color:\s*var\(--umds-semantic-color-primary-pure\)/);
33
33
  });
@@ -23,6 +23,14 @@ describe("StatCard · size", () => {
23
23
  const { container } = render(_jsx(StatCard, { value: 44, label: "N\u00E3o lidos", size: "sm", labelPosition: "top" }));
24
24
  expect(container.firstElementChild).toHaveClass("umds-stat-card--sm", "umds-stat-card--rotulo-topo");
25
25
  });
26
+ it("a segunda medida tem o mesmo tamanho e peso do número, no sm e no rótulo no topo", () => {
27
+ const sm = bloco(".umds-stat-card.umds-stat-card--sm .umds-stat-card__valor-secundario");
28
+ expect(sm).toMatch(/font-size:\s*1\.125rem/);
29
+ expect(sm).toMatch(/font-weight:\s*var\(--umds-primitive-font-weight-extrabold\)/);
30
+ const topo = bloco(".umds-stat-card--rotulo-topo .umds-stat-card__valor-secundario");
31
+ expect(topo).toMatch(/font-size:\s*var\(--umds-primitive-font-size-lg\)/);
32
+ expect(topo).toMatch(/font-weight:\s*var\(--umds-primitive-font-weight-extrabold\)/);
33
+ });
26
34
  it("o número continua antes do rótulo no HTML", () => {
27
35
  const { container } = render(_jsx(StatCard, { value: 44, label: "N\u00E3o lidos", size: "sm" }));
28
36
  const valor = container.querySelector(".umds-stat-card__valor");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1doc/1ds-react",
3
- "version": "2.13.0",
3
+ "version": "2.15.0",
4
4
  "main": "dist/index.js",
5
5
  "sideEffects": [
6
6
  "**/*.css"
@@ -45,8 +45,8 @@
45
45
  "react-dom": "^18.0.0",
46
46
  "typescript": "^5.0.0",
47
47
  "vitest": "^3.2.7",
48
- "@1doc/1ds-tokens": "0.7.0",
49
- "@1doc/1ds-core": "2.13.0"
48
+ "@1doc/1ds-core": "2.15.0",
49
+ "@1doc/1ds-tokens": "0.8.0"
50
50
  },
51
51
  "publishConfig": {
52
52
  "access": "public"
package/src/index.ts CHANGED
@@ -58,6 +58,8 @@ export { default as Input } from "./components/Input/Input";
58
58
  export { default as Kanban } from "./components/Kanban/Kanban";
59
59
  export { default as KanbanCard } from "./components/Kanban/KanbanCard";
60
60
  export { default as KanbanColumn } from "./components/Kanban/KanbanColumn";
61
+ export { default as Counter } from "./components/Counter/Counter";
62
+ export type * from "./components/Counter/Counter";
61
63
  export { default as Kbd } from "./components/Kbd/Kbd";
62
64
  export { default as Link } from "./components/Link/Link";
63
65
  export { default as LineChart } from "./components/Chart/LineChart";
@@ -0,0 +1,129 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { act, fireEvent, render, screen } from "@testing-library/react";
3
+ import Button from "../components/Button/Button";
4
+ import Counter from "../components/Counter/Counter";
5
+ import Dropdown from "../components/Dropdown/Dropdown";
6
+ import ListItem from "../components/List/ListItem";
7
+ import StatCard from "../components/StatCard/StatCard";
8
+
9
+ describe("Counter", () => {
10
+ it("sem animação mostra o valor final formatado em pt-BR", () => {
11
+ const { container } = render(<Counter value={1284} disableAnimation />);
12
+ expect(container.querySelector(".umds-counter__numero")).toHaveTextContent("1.284");
13
+ expect(container.querySelector(".umds-counter__numero")).toHaveAttribute("aria-hidden", "true");
14
+ expect(container.querySelector(".umds-counter__leitura")).toHaveTextContent("1.284");
15
+ });
16
+
17
+ it("casas decimais seguem o value; prefixo e sufixo entram no texto", () => {
18
+ const { container } = render(
19
+ <>
20
+ <Counter value={22.31} suffix="%" disableAnimation />
21
+ <Counter value={15890.5} prefix="R$ " decimals={2} disableAnimation />
22
+ </>,
23
+ );
24
+ const leituras = container.querySelectorAll(".umds-counter__leitura");
25
+ expect(leituras[0]).toHaveTextContent("22,31%");
26
+ expect(leituras[1].textContent).toBe("R$ 15.890,50");
27
+ });
28
+
29
+ it("conta do from até o valor e termina no valor exato", async () => {
30
+ vi.useFakeTimers();
31
+ const { container } = render(<Counter value={500} duration={100} />);
32
+ const numero = () => container.querySelector(".umds-counter__numero")!.textContent;
33
+ expect(numero()).toBe("0");
34
+ await act(async () => {
35
+ vi.advanceTimersByTime(500);
36
+ });
37
+ expect(numero()).toBe("500");
38
+ // A reserva e a leitura já nascem com o valor final.
39
+ expect(container.querySelector(".umds-counter__leitura")).toHaveTextContent("500");
40
+ vi.useRealTimers();
41
+ });
42
+ });
43
+
44
+ describe("ListItem · actions", () => {
45
+ it("as ações ficam no li, fora do elemento clicável", () => {
46
+ const { container } = render(
47
+ <ul>
48
+ <ListItem href="#doc" title="Memorando" actions={<button type="button">Mais</button>} />
49
+ </ul>,
50
+ );
51
+ const acao = screen.getByRole("button", { name: "Mais" });
52
+ expect(acao.closest("a")).toBeNull();
53
+ expect(acao.parentElement).toHaveClass("umds-list-item__actions");
54
+ expect(container.querySelector("li > a")).toHaveAttribute("href", "#doc");
55
+ });
56
+ });
57
+
58
+ describe("Button · href", () => {
59
+ it("com href é um link; desabilitado volta a ser botão", () => {
60
+ render(
61
+ <>
62
+ <Button href="#inbox">Inbox</Button>
63
+ <Button href="#cal" target="_blank">Calendário</Button>
64
+ <Button href="#fila" disabled>Fila</Button>
65
+ </>,
66
+ );
67
+ expect(screen.getByRole("link", { name: "Inbox" })).toHaveAttribute("href", "#inbox");
68
+ expect(screen.getByRole("link", { name: "Calendário" })).toHaveAttribute("rel", "noopener noreferrer");
69
+ expect(screen.getByRole("button", { name: "Fila" })).toBeDisabled();
70
+ });
71
+
72
+ it("loading segura a navegação do link", () => {
73
+ render(<Button href="#inbox" loading loadingPosition="start">Inbox</Button>);
74
+ const link = screen.getByRole("link", { name: "Inbox" });
75
+ const evento = new MouseEvent("click", { bubbles: true, cancelable: true });
76
+ link.dispatchEvent(evento);
77
+ expect(evento.defaultPrevented).toBe(true);
78
+ });
79
+ });
80
+
81
+ describe("Dropdown · href", () => {
82
+ it("item com href vira <a role=menuitem>, e o onSelect continua", async () => {
83
+ const onSelect = vi.fn();
84
+ render(
85
+ <Dropdown
86
+ ariaLabel="Ações"
87
+ onSelect={onSelect}
88
+ items={[
89
+ { key: "assinar", label: "Assinar", href: "#assinar" },
90
+ { key: "arquivar", label: "Arquivar" },
91
+ ]}
92
+ >
93
+ <button type="button">Abrir</button>
94
+ </Dropdown>,
95
+ );
96
+ fireEvent.click(screen.getByRole("button", { name: "Abrir" }));
97
+ const assinar = await screen.findByRole("menuitem", { name: "Assinar" });
98
+ expect(assinar.tagName).toBe("A");
99
+ expect(assinar).toHaveAttribute("href", "#assinar");
100
+ expect(screen.getByRole("menuitem", { name: "Arquivar" }).tagName).toBe("BUTTON");
101
+ fireEvent.click(assinar);
102
+ expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ key: "assinar" }));
103
+ });
104
+ });
105
+
106
+ describe("StatCard · loading e secondaryTone", () => {
107
+ it("loading troca os números por Skeleton e marca aria-busy", () => {
108
+ const { container } = render(
109
+ <StatCard value="22,31%" label="Qualidade" secondaryValue="14,11%" trendLabel="+2%" trend="up" loading />,
110
+ );
111
+ const cartao = container.querySelector(".umds-stat-card")!;
112
+ expect(cartao).toHaveAttribute("aria-busy", "true");
113
+ expect(cartao.textContent).not.toContain("22,31%");
114
+ expect(cartao.textContent).not.toContain("14,11%");
115
+ expect(cartao.textContent).not.toContain("+2%");
116
+ expect(cartao.textContent).toContain("Qualidade");
117
+ });
118
+
119
+ it("secondaryTone escreve a classe; default não", () => {
120
+ const { container } = render(
121
+ <>
122
+ <StatCard value="1" secondaryValue="2" secondaryTone="positive" />
123
+ <StatCard value="1" secondaryValue="2" secondaryTone="default" />
124
+ </>,
125
+ );
126
+ expect(container.children[0]).toHaveClass("umds-stat-card--secundario-positive");
127
+ expect(container.children[1].className).not.toContain("secundario-");
128
+ });
129
+ });