@1doc/1ds-react 2.9.0 → 2.11.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 (172) hide show
  1. package/CHANGELOG.md +653 -0
  2. package/dist/components/Accordion/Accordion.css +13 -0
  3. package/dist/components/Accordion/Accordion.d.ts +9 -0
  4. package/dist/components/Accordion/Accordion.js +18 -5
  5. package/dist/components/AppSubheader/AppSubheader.css +10 -1
  6. package/dist/components/AttachmentCard/AttachmentCard.css +10 -8
  7. package/dist/components/AttachmentCard/AttachmentCard.d.ts +3 -0
  8. package/dist/components/AttachmentCard/AttachmentCard.js +8 -1
  9. package/dist/components/Badge/Badge.css +27 -5
  10. package/dist/components/Badge/Badge.d.ts +20 -4
  11. package/dist/components/Badge/Badge.js +6 -0
  12. package/dist/components/Button/Button.css +5 -0
  13. package/dist/components/Button/Button.d.ts +3 -0
  14. package/dist/components/Button/Button.js +1 -0
  15. package/dist/components/Calendar/Calendar.css +10 -0
  16. package/dist/components/Calendar/Calendar.d.ts +10 -6
  17. package/dist/components/Calendar/Calendar.js +6 -1
  18. package/dist/components/Coachmark/Coachmark.css +10 -0
  19. package/dist/components/Coachmark/Coachmark.d.ts +14 -1
  20. package/dist/components/Coachmark/Coachmark.js +51 -5
  21. package/dist/components/ColorPicker/ColorPicker.css +8 -1
  22. package/dist/components/ColorPicker/ColorPicker.d.ts +2 -2
  23. package/dist/components/DataTable/DataTable.css +216 -0
  24. package/dist/components/DataTable/DataTable.d.ts +70 -2
  25. package/dist/components/DataTable/DataTable.js +293 -6
  26. package/dist/components/DescriptionList/DescriptionList.css +14 -0
  27. package/dist/components/DescriptionList/DescriptionList.d.ts +3 -0
  28. package/dist/components/DescriptionList/DescriptionList.js +1 -0
  29. package/dist/components/EmptyState/EmptyState.d.ts +4 -2
  30. package/dist/components/EmptyState/EmptyState.js +2 -2
  31. package/dist/components/Input/Input.css +30 -0
  32. package/dist/components/Input/Input.d.ts +10 -0
  33. package/dist/components/Input/Input.js +3 -1
  34. package/dist/components/List/ListItem.d.ts +14 -2
  35. package/dist/components/List/ListItem.js +3 -2
  36. package/dist/components/Modal/Modal.css +19 -0
  37. package/dist/components/Modal/Modal.d.ts +7 -0
  38. package/dist/components/Modal/Modal.js +1 -0
  39. package/dist/components/RichTextEditor/RichTextEditor.css +47 -0
  40. package/dist/components/RichTextEditor/RichTextEditor.d.ts +22 -8
  41. package/dist/components/RichTextEditor/RichTextEditor.js +460 -3
  42. package/dist/components/SegmentedControl/SegmentedControl.css +70 -0
  43. package/dist/components/SegmentedControl/SegmentedControl.d.ts +5 -0
  44. package/dist/components/SegmentedControl/SegmentedControl.js +5 -1
  45. package/dist/components/Select/Select.css +122 -2
  46. package/dist/components/Select/Select.d.ts +33 -4
  47. package/dist/components/Select/Select.js +256 -35
  48. package/dist/components/StatCard/StatCard.css +73 -0
  49. package/dist/components/StatCard/StatCard.d.ts +12 -0
  50. package/dist/components/StatCard/StatCard.js +11 -1
  51. package/dist/components/Tabs/Tabs.css +11 -0
  52. package/dist/components/Tabs/Tabs.d.ts +7 -0
  53. package/dist/components/Tabs/Tabs.js +7 -1
  54. package/dist/components/Tag/Tag.css +3 -1
  55. package/dist/components/Toast/Toast.css +32 -0
  56. package/dist/components/Toast/Toast.d.ts +18 -0
  57. package/dist/components/Toast/Toast.js +14 -1
  58. package/dist/components/Toast/ToastStack.js +8 -1
  59. package/dist/teste/AccordionControlado.test.d.ts +1 -0
  60. package/dist/teste/AccordionControlado.test.js +73 -0
  61. package/dist/teste/AccordionTituloNeutro.test.d.ts +1 -0
  62. package/dist/teste/AccordionTituloNeutro.test.js +23 -0
  63. package/dist/teste/AppSubheaderRegiao.test.d.ts +1 -0
  64. package/dist/teste/AppSubheaderRegiao.test.js +13 -0
  65. package/dist/teste/AttachmentCardLista.test.d.ts +1 -0
  66. package/dist/teste/AttachmentCardLista.test.js +49 -0
  67. package/dist/teste/AttachmentCardSm.test.d.ts +1 -0
  68. package/dist/teste/AttachmentCardSm.test.js +26 -0
  69. package/dist/teste/BadgeAttachedTomEPadding.test.d.ts +1 -0
  70. package/dist/teste/BadgeAttachedTomEPadding.test.js +43 -0
  71. package/dist/teste/BadgeNeutroColado.test.d.ts +1 -0
  72. package/dist/teste/BadgeNeutroColado.test.js +11 -0
  73. package/dist/teste/BadgeTag.test.js +13 -0
  74. package/dist/teste/ButtonDensidade.test.d.ts +1 -0
  75. package/dist/teste/ButtonDensidade.test.js +28 -0
  76. package/dist/teste/CalendarCabecalho.test.d.ts +1 -0
  77. package/dist/teste/CalendarCabecalho.test.js +11 -0
  78. package/dist/teste/CalendarCompacto.test.d.ts +1 -0
  79. package/dist/teste/CalendarCompacto.test.js +29 -0
  80. package/dist/teste/CoachmarkSemVeu.test.d.ts +1 -0
  81. package/dist/teste/CoachmarkSemVeu.test.js +85 -0
  82. package/dist/teste/CoachmarkUmPasso.test.d.ts +1 -0
  83. package/dist/teste/CoachmarkUmPasso.test.js +34 -0
  84. package/dist/teste/ColorPickerXs.test.d.ts +1 -0
  85. package/dist/teste/ColorPickerXs.test.js +21 -0
  86. package/dist/teste/DataTableArvoreEVariantes.test.d.ts +1 -0
  87. package/dist/teste/DataTableArvoreEVariantes.test.js +190 -0
  88. package/dist/teste/DataTableCompactaETom.test.d.ts +1 -0
  89. package/dist/teste/DataTableCompactaETom.test.js +69 -0
  90. package/dist/teste/DescriptionListCompacta.test.d.ts +1 -0
  91. package/dist/teste/DescriptionListCompacta.test.js +22 -0
  92. package/dist/teste/EmptyStateTituloRico.test.d.ts +1 -0
  93. package/dist/teste/EmptyStateTituloRico.test.js +23 -0
  94. package/dist/teste/InputAgrupado.test.d.ts +1 -0
  95. package/dist/teste/InputAgrupado.test.js +24 -0
  96. package/dist/teste/InputGhost.test.d.ts +1 -0
  97. package/dist/teste/InputGhost.test.js +32 -0
  98. package/dist/teste/ListItemTooltipStandalone.test.d.ts +1 -0
  99. package/dist/teste/ListItemTooltipStandalone.test.js +39 -0
  100. package/dist/teste/ModalProporcoes1Doc.test.d.ts +1 -0
  101. package/dist/teste/ModalProporcoes1Doc.test.js +32 -0
  102. package/dist/teste/RichTextEditorAlinhaImagemTabela.test.d.ts +1 -0
  103. package/dist/teste/RichTextEditorAlinhaImagemTabela.test.js +212 -0
  104. package/dist/teste/RichTextEditorEstiloDeTexto.test.d.ts +1 -0
  105. package/dist/teste/RichTextEditorEstiloDeTexto.test.js +120 -0
  106. package/dist/teste/SegmentedControlContador.test.d.ts +1 -0
  107. package/dist/teste/SegmentedControlContador.test.js +39 -0
  108. package/dist/teste/SegmentedControlSuperficie.test.d.ts +1 -0
  109. package/dist/teste/SegmentedControlSuperficie.test.js +39 -0
  110. package/dist/teste/SelectAbertoControladoEFlutuante.test.d.ts +1 -0
  111. package/dist/teste/SelectAbertoControladoEFlutuante.test.js +145 -0
  112. package/dist/teste/SelectFiltroCorETamanho.test.d.ts +1 -0
  113. package/dist/teste/SelectFiltroCorETamanho.test.js +77 -0
  114. package/dist/teste/SelectItemComEscopo.test.d.ts +1 -0
  115. package/dist/teste/SelectItemComEscopo.test.js +54 -0
  116. package/dist/teste/SelectListaFiltro.test.d.ts +1 -0
  117. package/dist/teste/SelectListaFiltro.test.js +102 -0
  118. package/dist/teste/StatCardSecundariaETom.test.d.ts +1 -0
  119. package/dist/teste/StatCardSecundariaETom.test.js +45 -0
  120. package/dist/teste/StatCardTamanhoSm.test.d.ts +1 -0
  121. package/dist/teste/StatCardTamanhoSm.test.js +32 -0
  122. package/dist/teste/TabsCompacta.test.d.ts +1 -0
  123. package/dist/teste/TabsCompacta.test.js +35 -0
  124. package/dist/teste/TagPositivoTingido.test.d.ts +1 -0
  125. package/dist/teste/TagPositivoTingido.test.js +54 -0
  126. package/dist/teste/ToastAcaoEDetalhes.test.d.ts +1 -0
  127. package/dist/teste/ToastAcaoEDetalhes.test.js +94 -0
  128. package/dist/teste/lerArquivo.d.ts +1 -0
  129. package/dist/teste/lerArquivo.js +12 -0
  130. package/dist/utils/html-seguro.d.ts +19 -1
  131. package/dist/utils/html-seguro.js +113 -2
  132. package/dist/utils/toast.d.ts +10 -1
  133. package/dist/utils/toast.js +2 -1
  134. package/package.json +3 -3
  135. package/src/teste/AccordionControlado.test.tsx +87 -0
  136. package/src/teste/AccordionTituloNeutro.test.tsx +25 -0
  137. package/src/teste/AppSubheaderRegiao.test.tsx +17 -0
  138. package/src/teste/AttachmentCardLista.test.tsx +53 -0
  139. package/src/teste/AttachmentCardSm.test.tsx +29 -0
  140. package/src/teste/BadgeAttachedTomEPadding.test.tsx +60 -0
  141. package/src/teste/BadgeNeutroColado.test.tsx +15 -0
  142. package/src/teste/BadgeTag.test.tsx +21 -0
  143. package/src/teste/ButtonDensidade.test.tsx +40 -0
  144. package/src/teste/CalendarCabecalho.test.tsx +11 -0
  145. package/src/teste/CalendarCompacto.test.tsx +37 -0
  146. package/src/teste/CoachmarkSemVeu.test.tsx +101 -0
  147. package/src/teste/CoachmarkUmPasso.test.tsx +45 -0
  148. package/src/teste/ColorPickerXs.test.tsx +22 -0
  149. package/src/teste/DataTableArvoreEVariantes.test.tsx +240 -0
  150. package/src/teste/DataTableCompactaETom.test.tsx +85 -0
  151. package/src/teste/DescriptionListCompacta.test.tsx +24 -0
  152. package/src/teste/EmptyStateTituloRico.test.tsx +34 -0
  153. package/src/teste/InputAgrupado.test.tsx +26 -0
  154. package/src/teste/InputGhost.test.tsx +42 -0
  155. package/src/teste/ListItemTooltipStandalone.test.tsx +64 -0
  156. package/src/teste/ModalProporcoes1Doc.test.tsx +36 -0
  157. package/src/teste/RichTextEditorAlinhaImagemTabela.test.tsx +238 -0
  158. package/src/teste/RichTextEditorEstiloDeTexto.test.tsx +137 -0
  159. package/src/teste/SegmentedControlContador.test.tsx +43 -0
  160. package/src/teste/SegmentedControlSuperficie.test.tsx +52 -0
  161. package/src/teste/SelectAbertoControladoEFlutuante.test.tsx +180 -0
  162. package/src/teste/SelectFiltroCorETamanho.test.tsx +90 -0
  163. package/src/teste/SelectItemComEscopo.test.tsx +69 -0
  164. package/src/teste/SelectListaFiltro.test.tsx +120 -0
  165. package/src/teste/StatCardSecundariaETom.test.tsx +51 -0
  166. package/src/teste/StatCardTamanhoSm.test.tsx +42 -0
  167. package/src/teste/TabsCompacta.test.tsx +45 -0
  168. package/src/teste/TagPositivoTingido.test.tsx +60 -0
  169. package/src/teste/ToastAcaoEDetalhes.test.tsx +109 -0
  170. package/src/teste/lerArquivo.ts +13 -0
  171. package/src/utils/html-seguro.ts +105 -2
  172. package/src/utils/toast.ts +12 -2
@@ -194,7 +194,9 @@
194
194
 
195
195
  .umds-tag--outlined.umds-tag--positive {
196
196
  border-color: var(--umds-semantic-color-feedback-positive-pure);
197
- color: var(--umds-semantic-color-feedback-positive-pure);
197
+ /* O contorno transparente deixa o texto sobre o fundo de quem está atrás (um card tingido de azul-claro).
198
+ positive-pure sobre primary-light dá 3,6:1; positive-dark dá 7,9:1 e é o par de texto do `soft`. */
199
+ color: var(--umds-semantic-color-feedback-positive-dark);
198
200
  }
199
201
 
200
202
  .umds-tag--outlined.umds-tag--negative {
@@ -123,6 +123,38 @@
123
123
  line-height: 1.5;
124
124
  }
125
125
 
126
+ /* ── Detalhes (details) e ação (actionLabel) ── */
127
+ .umds-toast__details {
128
+ margin: 0;
129
+ padding-left: var(--umds-primitive-spacing-xxxs); /* 16px */
130
+ font-size: var(--umds-primitive-font-size-xxxs); /* 12px */
131
+ line-height: 1.5;
132
+ }
133
+
134
+ .umds-toast__action {
135
+ align-self: flex-start;
136
+ display: inline-flex;
137
+ align-items: center;
138
+ /* Alvo mínimo de 24px (WCAG 2.5.8). */
139
+ min-height: var(--umds-primitive-spacing-xxs);
140
+ margin: 0;
141
+ padding: 0;
142
+ border: none;
143
+ background: none;
144
+ color: inherit;
145
+ font-family: inherit;
146
+ font-size: var(--umds-primitive-font-size-xxxs); /* 12px */
147
+ font-weight: var(--umds-primitive-font-weight-bold);
148
+ text-decoration: underline;
149
+ cursor: pointer;
150
+ }
151
+
152
+ .umds-toast__action:focus-visible {
153
+ outline: 2px solid currentColor;
154
+ outline-offset: 2px;
155
+ border-radius: var(--umds-primitive-border-radius-xs);
156
+ }
157
+
126
158
  /* ── Fechar ── */
127
159
  .umds-toast__close {
128
160
  display: flex;
@@ -4,6 +4,12 @@ import './Toast.css';
4
4
  * na raiz do documento (não fica presa ao overflow do elemento pai).
5
5
  * Comunica o resultado de uma ação (sucesso, erro, aviso ou informação) e pode
6
6
  * fechar automaticamente após um tempo.
7
+ *
8
+ * Com `actionLabel` e `onAction`, o toast ganha uma ação ("Desfazer", "Ver * processo"): um botão depois do texto. Acioná-la chama `onAction` e fecha o
9
+ * toast. Toast com ação não deve sumir sozinho antes de dar tempo de agir
10
+ * (WCAG 2.2.1): no `toast()` imperativo o padrão passa a ser `duration: 0`, e no
11
+ * declarativo vale o que você passar. `details` é uma lista de linhas
12
+ * curtas abaixo da descrição (o que deu errado, item a item).
7
13
  * @quandoUsar Retorno TRANSITÓRIO e não-bloqueante de uma ação recém-executada: "Processo salvo", "Link copiado". Aparece sobreposto e some sozinho.
8
14
  * @quandoNaoUsar Para mensagem que precisa continuar visível ou ser relida — use `Alert`. Para erro que impede o usuário de prosseguir, use `Alert` junto ao campo.
9
15
  */
@@ -28,6 +34,18 @@ export interface ToastProps {
28
34
  * Descrição adicional exibida abaixo da mensagem.
29
35
  */
30
36
  description?: string;
37
+ /**
38
+ * Lista de detalhes, uma linha por item, abaixo da descrição ("Anexo 3 acima * de 10 MB", "Despacho sem assunto").
39
+ */
40
+ details?: string[];
41
+ /**
42
+ * Rótulo do botão de ação ("Desfazer"). O botão só aparece com `onAction`.
43
+ */
44
+ actionLabel?: string;
45
+ /**
46
+ * Chamado ao acionar o botão de ação. Depois dele o toast fecha (`onClose`).
47
+ */
48
+ onAction?: (event?: any) => void;
31
49
  /**
32
50
  * Posição na tela onde o toast é fixado.
33
51
  * @default 'top-right'
@@ -31,6 +31,19 @@ function Toast(props) {
31
31
  return faCircleXmark;
32
32
  return faCircleInfo;
33
33
  }
34
+ function detailList() {
35
+ return props.details || [];
36
+ }
37
+ function hasAction() {
38
+ return !!(props.actionLabel && props.onAction);
39
+ }
40
+ function handleAction(event) {
41
+ if (event)
42
+ event.stopPropagation();
43
+ if (props.onAction)
44
+ props.onAction(event);
45
+ handleClose(event);
46
+ }
34
47
  function handleClose(event) {
35
48
  if (event)
36
49
  event.stopPropagation();
@@ -58,6 +71,6 @@ function Toast(props) {
58
71
  }
59
72
  };
60
73
  }, []);
61
- return (_jsx(Portal, { container: props.portalContainer, children: _jsx("div", { role: liveRole(), "aria-live": ariaLive(), children: props.open ? (_jsxs("div", { className: toastClasses(), children: [_jsx("div", { className: "umds-toast__icon-wrap", children: _jsx("span", { "aria-hidden": "true", className: "umds-toast__icon", children: _jsx(Icon, { variant: "inherit", customSize: "14px", icon: iconName() }) }) }), _jsxs("div", { className: "umds-toast__body", children: [_jsx("span", { className: "umds-toast__message", children: props.message }), props.description ? (_jsx("span", { className: "umds-toast__description", children: props.description })) : null] }), props.closable !== false ? (_jsx("button", { type: "button", className: "umds-toast__close", onClick: (event) => handleClose(event), "aria-label": props.closeLabel || "Fechar notificação", children: _jsx(Icon, { variant: "inherit", customSize: "18px", icon: faXmark }) })) : null] })) : null }) }));
74
+ return (_jsx(Portal, { container: props.portalContainer, children: _jsx("div", { role: liveRole(), "aria-live": ariaLive(), children: props.open ? (_jsxs("div", { className: toastClasses(), children: [_jsx("div", { className: "umds-toast__icon-wrap", children: _jsx("span", { "aria-hidden": "true", className: "umds-toast__icon", children: _jsx(Icon, { variant: "inherit", customSize: "14px", icon: iconName() }) }) }), _jsxs("div", { className: "umds-toast__body", children: [_jsx("span", { className: "umds-toast__message", children: props.message }), props.description ? (_jsx("span", { className: "umds-toast__description", children: props.description })) : null, detailList().length > 0 ? (_jsx("ul", { className: "umds-toast__details", children: detailList()?.map((detalhe) => (_jsx("li", { children: detalhe }, detalhe))) })) : null, hasAction() ? (_jsx("button", { type: "button", className: "umds-toast__action", onClick: (event) => handleAction(event), children: props.actionLabel })) : null] }), props.closable !== false ? (_jsx("button", { type: "button", className: "umds-toast__close", onClick: (event) => handleClose(event), "aria-label": props.closeLabel || "Fechar notificação", children: _jsx(Icon, { variant: "inherit", customSize: "18px", icon: faXmark }) })) : null] })) : null }) }));
62
75
  }
63
76
  export default Toast;
@@ -17,6 +17,13 @@ function ToastStack(props) {
17
17
  .filter(Boolean)
18
18
  .join(" ");
19
19
  }
20
+ function actionLabelOf(item) {
21
+ return item.onAction ? item.actionLabel : undefined;
22
+ }
23
+ function handleItemAction(item) {
24
+ if (item.onAction)
25
+ item.onAction();
26
+ }
20
27
  function handleItemClose(id) {
21
28
  dismissToast(id);
22
29
  }
@@ -33,6 +40,6 @@ function ToastStack(props) {
33
40
  }
34
41
  };
35
42
  }, []);
36
- return (_jsx("div", { ref: stackRef, className: stackClasses(), children: items?.map((item) => (_jsx(Toast, { open: true, message: item.message, description: item.description, variant: item.variant, duration: item.duration, closable: item.closable, portalContainer: stackRef.current, onClose: (event) => handleItemClose(item.id) }, item.id))) }));
43
+ return (_jsx("div", { ref: stackRef, className: stackClasses(), children: items?.map((item) => (_jsx(Toast, { open: true, message: item.message, description: item.description, details: item.details, actionLabel: actionLabelOf(item), onAction: (event) => handleItemAction(item), variant: item.variant, duration: item.duration, closable: item.closable, portalContainer: stackRef.current, onClose: (event) => handleItemClose(item.id) }, item.id))) }));
37
44
  }
38
45
  export default ToastStack;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { useState } from "react";
4
+ import { fireEvent, render, screen } from "@testing-library/react";
5
+ import Accordion from "../components/Accordion/Accordion";
6
+ const ITENS = [
7
+ { id: "prazos", title: "Prazos", content: "Dez dias úteis.", defaultOpen: true },
8
+ { id: "anexos", title: "Anexos", content: "Até 10 MB." },
9
+ { id: "sigilo", title: "Sigilo", content: "Só o setor." },
10
+ ];
11
+ const aberto = (nome) => screen.getByRole("button", { name: nome }).getAttribute("aria-expanded") === "true";
12
+ describe("Accordion · não controlado (como antes)", () => {
13
+ it("começa pelos defaultOpen e o clique abre e fecha; onOpenChange avisa a nova lista", () => {
14
+ const aoMudar = vi.fn();
15
+ render(_jsx(Accordion, { items: ITENS, multiple: true, onOpenChange: aoMudar }));
16
+ expect(aberto("Prazos")).toBe(true);
17
+ fireEvent.click(screen.getByRole("button", { name: "Anexos" }));
18
+ expect(aberto("Anexos")).toBe(true);
19
+ expect(aoMudar).toHaveBeenLastCalledWith(["prazos", "anexos"]);
20
+ fireEvent.click(screen.getByRole("button", { name: "Prazos" }));
21
+ expect(aberto("Prazos")).toBe(false);
22
+ expect(aoMudar).toHaveBeenLastCalledWith(["anexos"]);
23
+ });
24
+ it("sem multiple, abrir um fecha o outro", () => {
25
+ render(_jsx(Accordion, { items: ITENS }));
26
+ fireEvent.click(screen.getByRole("button", { name: "Anexos" }));
27
+ expect(aberto("Anexos")).toBe(true);
28
+ expect(aberto("Prazos")).toBe(false);
29
+ });
30
+ });
31
+ describe("Accordion · controlado (open e onOpenChange)", () => {
32
+ it("open manda: o que está em open está aberto, ignorando defaultOpen", () => {
33
+ render(_jsx(Accordion, { items: ITENS, open: ["sigilo"] }));
34
+ expect(aberto("Sigilo")).toBe(true);
35
+ expect(aberto("Prazos")).toBe(false);
36
+ });
37
+ it("o clique só pede (onOpenChange); sem o app mudar open, nada abre", () => {
38
+ const aoMudar = vi.fn();
39
+ render(_jsx(Accordion, { items: ITENS, multiple: true, open: ["prazos"], onOpenChange: aoMudar }));
40
+ fireEvent.click(screen.getByRole("button", { name: "Anexos" }));
41
+ expect(aoMudar).toHaveBeenCalledWith(["prazos", "anexos"]);
42
+ expect(aberto("Anexos")).toBe(false);
43
+ });
44
+ it("o app guarda a escolha: com o estado dele, abre, fecha e a lista chega em ordem", () => {
45
+ let ultima = [];
46
+ function App() {
47
+ const [abertos, setAbertos] = useState([]);
48
+ return (_jsx(Accordion, { items: ITENS, multiple: true, open: abertos, onOpenChange: (ids) => {
49
+ ultima = ids;
50
+ setAbertos(ids);
51
+ } }));
52
+ }
53
+ render(_jsx(App, {}));
54
+ expect(aberto("Prazos")).toBe(false);
55
+ fireEvent.click(screen.getByRole("button", { name: "Sigilo" }));
56
+ fireEvent.click(screen.getByRole("button", { name: "Anexos" }));
57
+ expect(ultima).toEqual(["sigilo", "anexos"]);
58
+ expect(aberto("Sigilo") && aberto("Anexos")).toBe(true);
59
+ fireEvent.click(screen.getByRole("button", { name: "Sigilo" }));
60
+ expect(ultima).toEqual(["anexos"]);
61
+ expect(aberto("Sigilo")).toBe(false);
62
+ });
63
+ it("open vazio é controlado (tudo fechado), não ausência", () => {
64
+ render(_jsx(Accordion, { items: ITENS, open: [] }));
65
+ expect(aberto("Prazos")).toBe(false);
66
+ });
67
+ it("item desabilitado não pede nada", () => {
68
+ const aoMudar = vi.fn();
69
+ render(_jsx(Accordion, { items: [{ id: "x", title: "Bloqueado", content: "...", disabled: true }], open: [], onOpenChange: aoMudar }));
70
+ fireEvent.click(screen.getByRole("button", { name: "Bloqueado" }));
71
+ expect(aoMudar).not.toHaveBeenCalled();
72
+ });
73
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render } from "@testing-library/react";
4
+ import Accordion from "../components/Accordion/Accordion";
5
+ const ITENS = [
6
+ { id: "a", title: "Prazos", content: "Dez dias úteis." },
7
+ { id: "b", title: "Anexos", content: "Até 10 MB." },
8
+ ];
9
+ describe("Accordion · titleVariant", () => {
10
+ it("neutral liga a classe do título neutro; o padrão e default não", () => {
11
+ const { container, rerender } = render(_jsx(Accordion, { items: ITENS, titleVariant: "neutral" }));
12
+ const raiz = () => container.firstElementChild;
13
+ expect(raiz()).toHaveClass("umds-accordion--title-neutral");
14
+ rerender(_jsx(Accordion, { items: ITENS, titleVariant: "default" }));
15
+ expect(raiz()).not.toHaveClass("umds-accordion--title-neutral");
16
+ rerender(_jsx(Accordion, { items: ITENS }));
17
+ expect(raiz()).not.toHaveClass("umds-accordion--title-neutral");
18
+ });
19
+ it("combina com a variant do accordion", () => {
20
+ const { container } = render(_jsx(Accordion, { items: ITENS, variant: "separated", titleVariant: "neutral" }));
21
+ expect(container.firstElementChild).toHaveClass("umds-accordion--separated", "umds-accordion--title-neutral");
22
+ });
23
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render } from "@testing-library/react";
4
+ import AppSubheader from "../components/AppSubheader/AppSubheader";
5
+ import Breadcrumb from "../components/Breadcrumb/Breadcrumb";
6
+ /** O jsdom não tem layout: a geometria real é conferida pela story "Só a página atual" (play, no navegador); a regra de CSS, pelo teste do Vue. */
7
+ describe("AppSubheader · região do contexto", () => {
8
+ it("a trilha só com a página atual não tem nada focável dentro da região", () => {
9
+ const { container } = render(_jsx(AppSubheader, { children: _jsx(Breadcrumb, { items: [{ label: "Painel" }] }) }));
10
+ const contexto = container.querySelector(".umds-app-subheader__contexto");
11
+ expect(contexto.querySelector("a, button, [tabindex]")).toBeNull();
12
+ });
13
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render, screen } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import AttachmentCard from "../components/AttachmentCard/AttachmentCard";
6
+ const DETALHES = {
7
+ description: "Documento legível.",
8
+ fields: [
9
+ { label: "Nome completo", value: "Marcos Vinícius Tavares", copyable: true },
10
+ { label: "CPF", value: "123.456.789-09" },
11
+ ],
12
+ };
13
+ async function abrirPainel() {
14
+ const usuario = userEvent.setup();
15
+ render(_jsx(AttachmentCard, { fileName: "cnh.pdf", documentType: "CNH", status: "positive", statusLabel: "Pr\u00E9-aprovado", statusDetails: DETALHES }));
16
+ await usuario.click(screen.getByRole("button", { name: /Pré-aprovado/ }));
17
+ return usuario;
18
+ }
19
+ describe("AttachmentCard · o <dl> do painel é válido (axe definition-list)", () => {
20
+ it("o dl só tem div filhos, e cada div só tem dt e dd, nessa ordem", async () => {
21
+ await abrirPainel();
22
+ const dl = document.querySelector(".umds-attachment-card__campos");
23
+ expect(dl.tagName).toBe("DL");
24
+ const grupos = [...dl.children];
25
+ expect(grupos).toHaveLength(2);
26
+ for (const grupo of grupos) {
27
+ expect(grupo.tagName).toBe("DIV");
28
+ expect([...grupo.children].map((f) => f.tagName)).toEqual(["DT", "DD"]);
29
+ }
30
+ });
31
+ it("o botão de copiar mora dentro do dd, ao lado do valor, e continua com nome", async () => {
32
+ await abrirPainel();
33
+ const dd = document.querySelector(".umds-attachment-card__campo-valor");
34
+ expect(dd.tagName).toBe("DD");
35
+ expect(dd.querySelector(".umds-attachment-card__campo-valor-texto")).toHaveTextContent("Marcos Vinícius Tavares");
36
+ const botao = dd.querySelector("button");
37
+ expect(botao).toHaveAttribute("aria-label", "Copiar Nome completo");
38
+ // nenhum botão solto direto num div do dl
39
+ for (const grupo of document.querySelectorAll(".umds-attachment-card__campo")) {
40
+ expect(grupo.querySelector(":scope > button")).toBeNull();
41
+ }
42
+ });
43
+ it("o campo sem copyable não tem botão e o valor segue no dd", async () => {
44
+ await abrirPainel();
45
+ const campos = document.querySelectorAll(".umds-attachment-card__campo");
46
+ expect(campos[1].querySelector("button")).toBeNull();
47
+ expect(campos[1].querySelector("dd")).toHaveTextContent("123.456.789-09");
48
+ });
49
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render } from "@testing-library/react";
4
+ import AttachmentCard from "../components/AttachmentCard/AttachmentCard";
5
+ const tamanho = (el) => el.style.width;
6
+ describe("AttachmentCard · size", () => {
7
+ it("md (padrão): ícone do preview de 48px e o do rodapé no sm do Icon", () => {
8
+ const { container } = render(_jsx(AttachmentCard, { fileName: "Protocolo 104-2026.pdf" }));
9
+ expect(container.firstElementChild).not.toHaveClass("umds-attachment-card--sm");
10
+ expect(tamanho(container.querySelector(".umds-attachment-card__preview .umds-icon"))).toBe("48px");
11
+ const rodape = container.querySelector(".umds-attachment-card__rodape-icone .umds-icon");
12
+ expect(rodape).toHaveClass("umds-icon--sm");
13
+ expect(rodape.style.width).toBe("");
14
+ });
15
+ it("sm: ícone do preview de 40px e o do rodapé de 12px", () => {
16
+ const { container } = render(_jsx(AttachmentCard, { fileName: "Protocolo 104-2026.pdf", size: "sm" }));
17
+ expect(container.firstElementChild).toHaveClass("umds-attachment-card--sm");
18
+ expect(tamanho(container.querySelector(".umds-attachment-card__preview .umds-icon"))).toBe("40px");
19
+ expect(tamanho(container.querySelector(".umds-attachment-card__rodape-icone .umds-icon"))).toBe("12px");
20
+ });
21
+ it("o ícone central escolhido continua valendo no sm", () => {
22
+ const { container } = render(_jsx(AttachmentCard, { fileName: "a.pdf", size: "sm", documentType: "CNH", status: "positive", statusLabel: "Pr\u00E9-aprovado" }));
23
+ expect(container.querySelector(".umds-attachment-card__tipo-texto")).toHaveTextContent("CNH");
24
+ expect(container.querySelector(".umds-attachment-card__status-texto")).toHaveTextContent("Pré-aprovado");
25
+ });
26
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render } from "@testing-library/react";
4
+ import Badge from "../components/Badge/Badge";
5
+ import { lerArquivo } from "./lerArquivo";
6
+ const cssBadge = await lerArquivo("../core/src/components/Badge/Badge.css");
7
+ const css = cssBadge.replace(/\r\n/g, "\n");
8
+ const bloco = (seletor) => {
9
+ const inicio = css.indexOf(seletor + " {");
10
+ return css.slice(inicio, css.indexOf("}", inicio));
11
+ };
12
+ describe("Badge 2xs · padding lateral de 8px", () => {
13
+ it("a regra do 2xs usa o degrau nano (8px) dos lados e o micro (2px) em cima e embaixo", () => {
14
+ expect(bloco(".umds-badge--2xs")).toMatch(/padding:\s*var\(--umds-primitive-spacing-micro\)\s+var\(--umds-primitive-spacing-nano\)/);
15
+ });
16
+ it("os outros tamanhos não mudam de padding", () => {
17
+ expect(bloco(".umds-badge--xs")).toMatch(/padding:\s*var\(--umds-primitive-spacing-micro\)\s+var\(--umds-primitive-spacing-quarck\)/);
18
+ expect(bloco(".umds-badge--md")).toMatch(/padding:\s*var\(--umds-primitive-spacing-quarck\)\s+var\(--umds-primitive-spacing-nano\)/);
19
+ });
20
+ });
21
+ describe("Badge attached · 22px e tom escuro", () => {
22
+ it("o rótulo colado tem altura mínima de 22px (24 - 2), de qualquer tamanho", () => {
23
+ expect(bloco(".umds-badge--attached")).toMatch(/min-height:\s*calc\(var\(--umds-primitive-spacing-xxs\)\s*-\s*var\(--umds-primitive-spacing-micro\)\)/);
24
+ const { container } = render(_jsx(Badge, { attached: true, size: "xs", variant: "neutral", appearance: "solid", children: "Tarefa manual" }));
25
+ expect(container.firstElementChild).toHaveClass("umds-badge--attached", "umds-badge--xs");
26
+ });
27
+ it("attachedTone dark escreve a classe; sem ela e com light, não", () => {
28
+ const { container } = render(_jsxs(_Fragment, { children: [_jsx(Badge, { attached: true, attachedTone: "dark", variant: "neutral", appearance: "solid", children: "A" }), _jsx(Badge, { attached: true, variant: "neutral", appearance: "solid", children: "B" }), _jsx(Badge, { attached: true, attachedTone: "light", variant: "neutral", appearance: "solid", children: "C" })] }));
29
+ expect(container.children[0]).toHaveClass("umds-badge--attached-dark");
30
+ expect(container.children[1]).not.toHaveClass("umds-badge--attached-dark");
31
+ expect(container.children[2]).not.toHaveClass("umds-badge--attached-dark");
32
+ });
33
+ it("sem attached o tom é ignorado", () => {
34
+ const { container } = render(_jsx(Badge, { attachedTone: "dark", variant: "neutral", appearance: "solid", children: "A" }));
35
+ expect(container.firstElementChild).not.toHaveClass("umds-badge--attached-dark");
36
+ });
37
+ it("CSS: o escuro é neutral-dark-down com texto branco, só no neutro sólido colado", () => {
38
+ const regra = bloco(".umds-badge--attached.umds-badge--neutral.umds-badge--solid.umds-badge--attached-dark");
39
+ expect(regra).toMatch(/background-color:\s*var\(--umds-semantic-color-neutral-dark-down\)/);
40
+ expect(regra).toMatch(/color:\s*var\(--umds-semantic-color-neutral-light-pure\)/);
41
+ expect(bloco(".umds-badge--attached.umds-badge--neutral.umds-badge--solid")).toMatch(/neutral-light-medium/);
42
+ });
43
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render } from "@testing-library/react";
4
+ import Badge from "../components/Badge/Badge";
5
+ /** A cascata de CSS de arquivo não existe no jsdom: a regra é conferida na fonte pelo teste do Vue; aqui, as classes no DOM. */
6
+ describe("Badge attached neutral", () => {
7
+ it("o neutro colado e sólido leva as três classes que a regra de CSS escolhe", () => {
8
+ const { container } = render(_jsx(Badge, { attached: true, variant: "neutral", appearance: "solid", children: "Tarefa manual" }));
9
+ expect(container.firstElementChild).toHaveClass("umds-badge--attached", "umds-badge--neutral", "umds-badge--solid");
10
+ });
11
+ });
@@ -40,3 +40,16 @@ describe("Tag compacta, soft, pílula e novidade", () => {
40
40
  expect(raiz(container)).toHaveClass("umds-tag--new", "umds-tag--soft");
41
41
  });
42
42
  });
43
+ describe("Badge attachedSide", () => {
44
+ it("right escreve o modificador do canto direito; left e o padrão não", () => {
45
+ const { container } = render(_jsxs(_Fragment, { children: [_jsx(Badge, { attached: true, attachedSide: "right", children: "A" }), _jsx(Badge, { attached: true, attachedSide: "left", children: "B" }), _jsx(Badge, { attached: true, children: "C" })] }));
46
+ expect(container.children[0]).toHaveClass("umds-badge--attached", "umds-badge--attached-right");
47
+ expect(container.children[1]).not.toHaveClass("umds-badge--attached-right");
48
+ expect(container.children[2]).not.toHaveClass("umds-badge--attached-right");
49
+ });
50
+ it("attachedSide sem attached não faz nada", () => {
51
+ const { container } = render(_jsx(Badge, { attachedSide: "right", children: "A" }));
52
+ expect(raiz(container)).not.toHaveClass("umds-badge--attached-right");
53
+ expect(raiz(container)).not.toHaveClass("umds-badge--attached");
54
+ });
55
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render, screen } from "@testing-library/react";
4
+ import Button from "../components/Button/Button";
5
+ import { lerArquivo } from "./lerArquivo";
6
+ const cssButton = await lerArquivo("../core/src/components/Button/Button.css");
7
+ const css = cssButton.replace(/\r\n/g, "\n");
8
+ const bloco = (seletor) => {
9
+ const inicio = css.indexOf(seletor + " {");
10
+ return css.slice(inicio, css.indexOf("}", inicio));
11
+ };
12
+ describe("Button · density", () => {
13
+ it("comfortable escreve a classe; sem a prop e com default o botão é o de antes", () => {
14
+ render(_jsxs(_Fragment, { children: [_jsx(Button, { size: "sm", density: "comfortable", children: "A" }), _jsx(Button, { size: "sm", children: "B" }), _jsx(Button, { size: "sm", density: "default", children: "C" })] }));
15
+ expect(screen.getByRole("button", { name: "A" })).toHaveClass("umds-button--sm", "umds-button--comfortable");
16
+ expect(screen.getByRole("button", { name: "B" })).not.toHaveClass("umds-button--comfortable");
17
+ expect(screen.getByRole("button", { name: "C" })).not.toHaveClass("umds-button--comfortable");
18
+ });
19
+ it("CSS: o sm confortável tem 12px de padding lateral; o sm de antes segue em 8px; o só-ícone fica de fora", () => {
20
+ expect(bloco(".umds-button--sm.umds-button--comfortable:not(.umds-button--icon)")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-mini\)/);
21
+ expect(bloco(".umds-button--sm")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-nano\)/);
22
+ });
23
+ it("os outros tamanhos mantêm o padding (a densidade só mexe no sm)", () => {
24
+ expect(bloco(".umds-button--xs")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-mini\)/);
25
+ expect(bloco(".umds-button--md")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-xxxs\)/);
26
+ expect(bloco(".umds-button--lg")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-xxs\)/);
27
+ });
28
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render, screen } from "@testing-library/react";
4
+ import Calendar from "../components/Calendar/Calendar";
5
+ describe("Calendar · cabeçalho", () => {
6
+ it("headerStart e headerEnd (React: props com nó) chegam ao cabeçalho", () => {
7
+ render(_jsx(Calendar, { events: [], defaultDate: "2026-10-08", headerStart: _jsx("span", { children: "Todos os documentos" }), headerEnd: _jsx("span", { children: "Legenda" }) }));
8
+ expect(screen.getByText("Todos os documentos")).toBeInTheDocument();
9
+ expect(screen.getByText("Legenda")).toBeInTheDocument();
10
+ });
11
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import Calendar from "../components/Calendar/Calendar";
4
+ import { render } from "@testing-library/react";
5
+ import { lerArquivo } from "./lerArquivo";
6
+ const cssCalendar = await lerArquivo("../core/src/components/Calendar/Calendar.css");
7
+ const CSS = { Calendar: cssCalendar };
8
+ const css = (componente) => CSS[componente].replace(/\r\n/g, "\n");
9
+ const bloco = (texto, seletor) => {
10
+ const inicio = texto.indexOf(seletor + " {");
11
+ expect(inicio, seletor).toBeGreaterThanOrEqual(0);
12
+ return texto.slice(inicio, texto.indexOf("}", inicio));
13
+ };
14
+ describe("Calendar · density compact", () => {
15
+ it("compact escreve a classe na raiz; sem a prop, não", () => {
16
+ const { container } = render(_jsxs(_Fragment, { children: [_jsx(Calendar, { today: "2026-05-15", defaultDate: "2026-05-15", density: "compact" }), _jsx(Calendar, { today: "2026-05-15", defaultDate: "2026-05-15" })] }));
17
+ expect(container.children[0]).toHaveClass("umds-calendar--compact", "umds-calendar--month");
18
+ expect(container.children[1]).not.toHaveClass("umds-calendar--compact");
19
+ });
20
+ it("CSS: evento de 20px (16 + 4), 4px de padding lateral e linha de 16px; o título só é cortado quando passa da largura", () => {
21
+ const t = css("Calendar");
22
+ const compacto = bloco(t, ".umds-calendar--compact .umds-calendar__event");
23
+ expect(compacto).toMatch(/min-height:\s*calc\(var\(--umds-primitive-spacing-xxxs\)\s*\+\s*var\(--umds-primitive-spacing-quarck\)\)/);
24
+ expect(compacto).toMatch(/padding:\s*var\(--umds-primitive-spacing-micro\)\s+var\(--umds-primitive-spacing-quarck\)/);
25
+ expect(compacto).toMatch(/line-height:\s*var\(--umds-primitive-spacing-xxxs\)/);
26
+ const titulo = bloco(t, ".umds-calendar__event-title");
27
+ expect(titulo).toMatch(/text-overflow:\s*ellipsis/);
28
+ });
29
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
4
+ import Coachmark from "../components/Coachmark/Coachmark";
5
+ const PASSOS = [{ target: "#alvo-a", title: "Novo: arrastar", content: "Arraste a linha para trocar a ordem." }];
6
+ function Pagina(props) {
7
+ return (_jsxs("div", { children: [_jsx("button", { id: "alvo-a", children: "A" }), _jsx("button", { id: "fora", children: "Fora" }), _jsx(Coachmark, { steps: PASSOS, open: true, ...props })] }));
8
+ }
9
+ describe("Coachmark · modal={false}", () => {
10
+ it("sem véu nem recorte; o cartão é um diálogo não modal e a raiz deixa o ponteiro passar", () => {
11
+ const { baseElement } = render(_jsx(Pagina, { modal: false }));
12
+ expect(baseElement.querySelector(".umds-coachmark__veil")).toBeNull();
13
+ expect(baseElement.querySelector(".umds-coachmark__cutout")).toBeNull();
14
+ expect(baseElement.querySelector(".umds-coachmark")).toHaveClass("umds-coachmark--modeless");
15
+ expect(screen.getByRole("dialog", { name: "Novo: arrastar" })).toHaveAttribute("aria-modal", "false");
16
+ });
17
+ it("o padrão continua modal: véu, recorte e aria-modal verdadeiro", () => {
18
+ const { baseElement } = render(_jsx(Pagina, {}));
19
+ expect(baseElement.querySelector(".umds-coachmark__veil")).not.toBeNull();
20
+ expect(baseElement.querySelector(".umds-coachmark__cutout")).not.toBeNull();
21
+ expect(baseElement.querySelector(".umds-coachmark")).not.toHaveClass("umds-coachmark--modeless");
22
+ expect(screen.getByRole("dialog")).toHaveAttribute("aria-modal", "true");
23
+ const { unmount } = render(_jsx(Pagina, { modal: true }));
24
+ unmount();
25
+ });
26
+ it("o foco entra no cartão, mas o Tab não fica preso", async () => {
27
+ render(_jsx(Pagina, { modal: false }));
28
+ const cartao = screen.getByRole("dialog");
29
+ await waitFor(() => expect(cartao).toHaveFocus());
30
+ const concluir = screen.getByRole("button", { name: "Concluir" });
31
+ concluir.focus();
32
+ const evento = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
33
+ concluir.dispatchEvent(evento);
34
+ expect(evento.defaultPrevented).toBe(false);
35
+ });
36
+ it("no modal o Tab continua preso (o último volta ao primeiro)", () => {
37
+ render(_jsx(Pagina, {}));
38
+ const concluir = screen.getByRole("button", { name: "Concluir" });
39
+ concluir.focus();
40
+ const evento = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
41
+ concluir.dispatchEvent(evento);
42
+ expect(evento.defaultPrevented).toBe(true);
43
+ });
44
+ it("a página segue clicável: o clique fora não é capturado e o tour não fecha sozinho", () => {
45
+ const aoFechar = vi.fn();
46
+ render(_jsx(Pagina, { modal: false, onClose: aoFechar }));
47
+ fireEvent.click(screen.getByRole("button", { name: "Fora" }));
48
+ expect(aoFechar).not.toHaveBeenCalled();
49
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
50
+ });
51
+ it("o Esc fecha de qualquer lugar da página, uma vez só", () => {
52
+ const aoFechar = vi.fn();
53
+ render(_jsx(Pagina, { modal: false, onClose: aoFechar }));
54
+ const fora = screen.getByRole("button", { name: "Fora" });
55
+ fora.focus();
56
+ fireEvent.keyDown(fora, { key: "Escape" });
57
+ expect(aoFechar).toHaveBeenCalledTimes(1);
58
+ expect(aoFechar).toHaveBeenCalledWith("escape");
59
+ });
60
+ it("o Esc dentro do cartão também fecha uma vez só", async () => {
61
+ const aoFechar = vi.fn();
62
+ render(_jsx(Pagina, { modal: false, onClose: aoFechar }));
63
+ await waitFor(() => expect(screen.getByRole("dialog")).toHaveFocus());
64
+ fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" });
65
+ expect(aoFechar).toHaveBeenCalledTimes(1);
66
+ });
67
+ it("depois de fechar, o ouvinte do Esc sai do documento", () => {
68
+ const aoFechar = vi.fn();
69
+ const { rerender } = render(_jsx(Pagina, { modal: false, onClose: aoFechar }));
70
+ rerender(_jsx(Pagina, { modal: false, open: false, onClose: aoFechar }));
71
+ fireEvent.keyDown(document, { key: "Escape" });
72
+ expect(aoFechar).not.toHaveBeenCalled();
73
+ });
74
+ it("ao fechar, o foco volta ao elemento de antes, se a pessoa não foi a outro lugar", async () => {
75
+ const { rerender } = render(_jsx(Pagina, { modal: false, open: false }));
76
+ const gatilho = screen.getByRole("button", { name: "Fora" });
77
+ gatilho.focus();
78
+ expect(gatilho).toHaveFocus();
79
+ rerender(_jsx(Pagina, { modal: false, open: true }));
80
+ // O foco entra no cartão depois da montagem (um quadro de animação).
81
+ await waitFor(() => expect(screen.getByRole("dialog")).toHaveFocus());
82
+ rerender(_jsx(Pagina, { modal: false, open: false }));
83
+ await waitFor(() => expect(gatilho).toHaveFocus());
84
+ });
85
+ });
@@ -0,0 +1 @@
1
+ export {};