@1doc/1ds-react 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +434 -0
- package/dist/components/Accordion/Accordion.css +13 -0
- package/dist/components/Accordion/Accordion.d.ts +3 -0
- package/dist/components/Accordion/Accordion.js +1 -0
- package/dist/components/AppSubheader/AppSubheader.css +10 -1
- package/dist/components/AttachmentCard/AttachmentCard.css +10 -8
- package/dist/components/AttachmentCard/AttachmentCard.d.ts +3 -0
- package/dist/components/AttachmentCard/AttachmentCard.js +8 -1
- package/dist/components/Badge/Badge.css +14 -1
- package/dist/components/Badge/Badge.d.ts +11 -2
- package/dist/components/Badge/Badge.js +3 -0
- package/dist/components/Calendar/Calendar.d.ts +7 -6
- package/dist/components/Coachmark/Coachmark.css +10 -0
- package/dist/components/Coachmark/Coachmark.d.ts +12 -0
- package/dist/components/Coachmark/Coachmark.js +46 -5
- package/dist/components/ColorPicker/ColorPicker.css +8 -1
- package/dist/components/ColorPicker/ColorPicker.d.ts +2 -2
- package/dist/components/DataTable/DataTable.css +174 -0
- package/dist/components/DataTable/DataTable.d.ts +63 -2
- package/dist/components/DataTable/DataTable.js +278 -6
- package/dist/components/DescriptionList/DescriptionList.css +14 -0
- package/dist/components/DescriptionList/DescriptionList.d.ts +3 -0
- package/dist/components/DescriptionList/DescriptionList.js +1 -0
- package/dist/components/EmptyState/EmptyState.d.ts +4 -2
- package/dist/components/EmptyState/EmptyState.js +2 -2
- package/dist/components/Input/Input.css +21 -0
- package/dist/components/Input/Input.d.ts +7 -0
- package/dist/components/Input/Input.js +2 -1
- package/dist/components/List/ListItem.d.ts +14 -2
- package/dist/components/List/ListItem.js +3 -2
- package/dist/components/RichTextEditor/RichTextEditor.css +47 -0
- package/dist/components/RichTextEditor/RichTextEditor.d.ts +22 -8
- package/dist/components/RichTextEditor/RichTextEditor.js +460 -3
- package/dist/components/SegmentedControl/SegmentedControl.css +24 -0
- package/dist/components/SegmentedControl/SegmentedControl.d.ts +2 -0
- package/dist/components/SegmentedControl/SegmentedControl.js +4 -1
- package/dist/components/Select/Select.css +94 -0
- package/dist/components/Select/Select.d.ts +26 -4
- package/dist/components/Select/Select.js +210 -35
- package/dist/components/StatCard/StatCard.css +65 -0
- package/dist/components/StatCard/StatCard.d.ts +9 -0
- package/dist/components/StatCard/StatCard.js +10 -1
- package/dist/components/Toast/Toast.css +32 -0
- package/dist/components/Toast/Toast.d.ts +18 -0
- package/dist/components/Toast/Toast.js +14 -1
- package/dist/components/Toast/ToastStack.js +8 -1
- package/dist/teste/AccordionTituloNeutro.test.d.ts +1 -0
- package/dist/teste/AccordionTituloNeutro.test.js +23 -0
- package/dist/teste/AppSubheaderRegiao.test.d.ts +1 -0
- package/dist/teste/AppSubheaderRegiao.test.js +13 -0
- package/dist/teste/AttachmentCardLista.test.d.ts +1 -0
- package/dist/teste/AttachmentCardLista.test.js +49 -0
- package/dist/teste/AttachmentCardSm.test.d.ts +1 -0
- package/dist/teste/AttachmentCardSm.test.js +26 -0
- package/dist/teste/BadgeNeutroColado.test.d.ts +1 -0
- package/dist/teste/BadgeNeutroColado.test.js +11 -0
- package/dist/teste/BadgeTag.test.js +13 -0
- package/dist/teste/CalendarCabecalho.test.d.ts +1 -0
- package/dist/teste/CalendarCabecalho.test.js +11 -0
- package/dist/teste/CoachmarkSemVeu.test.d.ts +1 -0
- package/dist/teste/CoachmarkSemVeu.test.js +85 -0
- package/dist/teste/ColorPickerXs.test.d.ts +1 -0
- package/dist/teste/ColorPickerXs.test.js +21 -0
- package/dist/teste/DataTableArvoreEVariantes.test.d.ts +1 -0
- package/dist/teste/DataTableArvoreEVariantes.test.js +190 -0
- package/dist/teste/DescriptionListCompacta.test.d.ts +1 -0
- package/dist/teste/DescriptionListCompacta.test.js +22 -0
- package/dist/teste/EmptyStateTituloRico.test.d.ts +1 -0
- package/dist/teste/EmptyStateTituloRico.test.js +23 -0
- package/dist/teste/InputAgrupado.test.d.ts +1 -0
- package/dist/teste/InputAgrupado.test.js +24 -0
- package/dist/teste/ListItemTooltipStandalone.test.d.ts +1 -0
- package/dist/teste/ListItemTooltipStandalone.test.js +39 -0
- package/dist/teste/RichTextEditorAlinhaImagemTabela.test.d.ts +1 -0
- package/dist/teste/RichTextEditorAlinhaImagemTabela.test.js +212 -0
- package/dist/teste/RichTextEditorEstiloDeTexto.test.d.ts +1 -0
- package/dist/teste/RichTextEditorEstiloDeTexto.test.js +120 -0
- package/dist/teste/SegmentedControlContador.test.d.ts +1 -0
- package/dist/teste/SegmentedControlContador.test.js +39 -0
- package/dist/teste/SelectAbertoControladoEFlutuante.test.d.ts +1 -0
- package/dist/teste/SelectAbertoControladoEFlutuante.test.js +145 -0
- package/dist/teste/SelectFiltroCorETamanho.test.d.ts +1 -0
- package/dist/teste/SelectFiltroCorETamanho.test.js +77 -0
- package/dist/teste/StatCardSecundariaETom.test.d.ts +1 -0
- package/dist/teste/StatCardSecundariaETom.test.js +45 -0
- package/dist/teste/ToastAcaoEDetalhes.test.d.ts +1 -0
- package/dist/teste/ToastAcaoEDetalhes.test.js +94 -0
- package/dist/utils/html-seguro.d.ts +19 -1
- package/dist/utils/html-seguro.js +113 -2
- package/dist/utils/toast.d.ts +10 -1
- package/dist/utils/toast.js +2 -1
- package/package.json +2 -2
- package/src/teste/AccordionTituloNeutro.test.tsx +25 -0
- package/src/teste/AppSubheaderRegiao.test.tsx +17 -0
- package/src/teste/AttachmentCardLista.test.tsx +53 -0
- package/src/teste/AttachmentCardSm.test.tsx +29 -0
- package/src/teste/BadgeNeutroColado.test.tsx +15 -0
- package/src/teste/BadgeTag.test.tsx +21 -0
- package/src/teste/CalendarCabecalho.test.tsx +11 -0
- package/src/teste/CoachmarkSemVeu.test.tsx +101 -0
- package/src/teste/ColorPickerXs.test.tsx +22 -0
- package/src/teste/DataTableArvoreEVariantes.test.tsx +240 -0
- package/src/teste/DescriptionListCompacta.test.tsx +24 -0
- package/src/teste/EmptyStateTituloRico.test.tsx +34 -0
- package/src/teste/InputAgrupado.test.tsx +26 -0
- package/src/teste/ListItemTooltipStandalone.test.tsx +64 -0
- package/src/teste/RichTextEditorAlinhaImagemTabela.test.tsx +238 -0
- package/src/teste/RichTextEditorEstiloDeTexto.test.tsx +137 -0
- package/src/teste/SegmentedControlContador.test.tsx +43 -0
- package/src/teste/SelectAbertoControladoEFlutuante.test.tsx +180 -0
- package/src/teste/SelectFiltroCorETamanho.test.tsx +90 -0
- package/src/teste/StatCardSecundariaETom.test.tsx +51 -0
- package/src/teste/ToastAcaoEDetalhes.test.tsx +109 -0
- package/src/utils/html-seguro.ts +105 -2
- package/src/utils/toast.ts +12 -2
|
@@ -5,13 +5,19 @@
|
|
|
5
5
|
* fora (um valor guardado, um texto colado) pode trazer `<img onerror>`,
|
|
6
6
|
* `<a href="javascript:...">`, `<script>`, `style` e classes de outro site. Este
|
|
7
7
|
* módulo reduz o HTML ao que o editor sabe escrever: negrito, itálico,
|
|
8
|
-
* sublinhado, tachado, listas, parágrafos, quebras, títulos, citação
|
|
8
|
+
* sublinhado, tachado, listas, parágrafos, quebras, títulos, citação, link,
|
|
9
|
+
* estilo de texto por token (`color`, `font-size`, `font-family` em `<span>`),
|
|
10
|
+
* alinhamento de bloco (`text-align`), imagem (`http(s)`, caminho ou `data:image`
|
|
11
|
+
* em PNG, JPEG, GIF e WebP) e tabela simples.
|
|
9
12
|
*
|
|
10
13
|
* Roda no navegador (precisa de DOM). Dentro de um `<template>` o HTML é inerte:
|
|
11
14
|
* nada executa e nenhuma imagem carrega enquanto se higieniza. Sem DOM
|
|
12
15
|
* (servidor), devolve o texto escapado.
|
|
13
16
|
*/
|
|
14
|
-
const PERMITIDAS = ["A", "B", "STRONG", "I", "EM", "U", "S", "STRIKE", "DEL", "UL", "OL", "LI", "P", "BR", "DIV", "SPAN", "H1", "H2", "H3", "H4", "BLOCKQUOTE"];
|
|
17
|
+
const PERMITIDAS = ["A", "B", "STRONG", "I", "EM", "U", "S", "STRIKE", "DEL", "UL", "OL", "LI", "P", "BR", "DIV", "SPAN", "H1", "H2", "H3", "H4", "BLOCKQUOTE", "IMG", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"];
|
|
18
|
+
/** Blocos que aceitam `text-align`. */
|
|
19
|
+
const ALINHAVEIS = ["P", "DIV", "H1", "H2", "H3", "H4", "LI", "BLOCKQUOTE", "TH", "TD"];
|
|
20
|
+
const ALINHAMENTOS = ["left", "center", "right", "justify"];
|
|
15
21
|
const DESCARTADAS = ["SCRIPT", "STYLE", "IFRAME", "OBJECT", "EMBED", "LINK", "META", "TEMPLATE", "NOSCRIPT", "SVG", "MATH", "FORM", "INPUT", "BUTTON", "TEXTAREA", "SELECT"];
|
|
16
22
|
const escapar = (texto) => texto.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
17
23
|
/**
|
|
@@ -44,6 +50,68 @@ export const normalizarEndereco = (bruto) => {
|
|
|
44
50
|
return "";
|
|
45
51
|
return "https://" + texto;
|
|
46
52
|
};
|
|
53
|
+
/** Cor, tamanho e família de fonte só valem como TOKEN do DS: `var(--umds-...)`. Cor em hex, rgb ou nome sai. */
|
|
54
|
+
const COR_TOKEN = /^var\(--umds-(?:primitive|semantic)-[a-z0-9-]+\)$/;
|
|
55
|
+
const TAMANHO_TOKEN = /^var\(--umds-primitive-font-size-[a-z0-9]+\)$/;
|
|
56
|
+
const FAMILIA_TOKEN = /^var\(--umds-(?:primitive|semantic)-font-family-(?:base|mono)\)$/;
|
|
57
|
+
/**
|
|
58
|
+
* Declaração de estilo de texto que o editor aceita: `color`, `font-size` e `font-family`,
|
|
59
|
+
* cada uma só com um token do DS. Devolve a declaração limpa ("color: var(--x);") ou "".
|
|
60
|
+
*/
|
|
61
|
+
export const estiloDeTexto = (propriedade, valor) => {
|
|
62
|
+
const prop = String(propriedade || "").trim().toLowerCase();
|
|
63
|
+
const val = String(valor || "").trim();
|
|
64
|
+
if (prop === "color" && COR_TOKEN.test(val))
|
|
65
|
+
return "color: " + val + ";";
|
|
66
|
+
if (prop === "font-size" && TAMANHO_TOKEN.test(val))
|
|
67
|
+
return "font-size: " + val + ";";
|
|
68
|
+
if (prop === "font-family" && FAMILIA_TOKEN.test(val))
|
|
69
|
+
return "font-family: " + val + ";";
|
|
70
|
+
return "";
|
|
71
|
+
};
|
|
72
|
+
/** As declarações válidas de um atributo `style` (só color, font-size e font-family por token). */
|
|
73
|
+
const estiloDoSpan = (estilo) => {
|
|
74
|
+
const partes = String(estilo || "").split(";");
|
|
75
|
+
let saida = "";
|
|
76
|
+
for (let i = 0; i < partes.length; i++) {
|
|
77
|
+
const corte = partes[i].indexOf(":");
|
|
78
|
+
if (corte === -1)
|
|
79
|
+
continue;
|
|
80
|
+
const decl = estiloDeTexto(partes[i].slice(0, corte), partes[i].slice(corte + 1));
|
|
81
|
+
if (decl)
|
|
82
|
+
saida += (saida ? " " : "") + decl;
|
|
83
|
+
}
|
|
84
|
+
return saida;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Endereço de imagem que o editor aceita: `http(s)`, caminho (`/x`, `./x`, `../x`) ou
|
|
88
|
+
* `data:image` em base64 de PNG, JPEG, GIF ou WebP (SVG fica de fora: carrega script).
|
|
89
|
+
* Devolve o endereço limpo, ou "" se for perigoso ou vazio.
|
|
90
|
+
*/
|
|
91
|
+
export const imagemSegura = (bruto) => {
|
|
92
|
+
const texto = (bruto || "").trim();
|
|
93
|
+
if (!texto)
|
|
94
|
+
return "";
|
|
95
|
+
const semControle = texto.replace(/[\u0000-\u001f\u007f\s]+/g, "");
|
|
96
|
+
if (/^data:image\/(png|jpe?g|gif|webp);base64,[a-z0-9+/]+=*$/i.test(semControle))
|
|
97
|
+
return semControle;
|
|
98
|
+
// `//host/x` (sem esquema) carregaria de outro site sem o autor ter pedido: fica de fora.
|
|
99
|
+
if (/^(https?:\/\/|\/(?!\/)|\.\.?\/)/i.test(semControle))
|
|
100
|
+
return texto;
|
|
101
|
+
return "";
|
|
102
|
+
};
|
|
103
|
+
/** O `text-align` de um elemento, pelo `style` ou pelo atributo `align`: left, center, right, justify ou "". */
|
|
104
|
+
const alinhamentoDe = (elemento) => {
|
|
105
|
+
const estilo = String(elemento.getAttribute("style") || "");
|
|
106
|
+
const achado = /(?:^|;)\s*text-align\s*:\s*([a-z]+)\s*(?:;|$)/i.exec(estilo);
|
|
107
|
+
const valor = (achado ? achado[1] : String(elemento.getAttribute("align") || "")).toLowerCase();
|
|
108
|
+
return ALINHAMENTOS.indexOf(valor) !== -1 ? valor : "";
|
|
109
|
+
};
|
|
110
|
+
/** Número inteiro de 2 a 99 em `colspan`/`rowspan`; o resto some. */
|
|
111
|
+
const vao = (valor) => {
|
|
112
|
+
const n = parseInt(String(valor || ""), 10);
|
|
113
|
+
return n >= 2 && n <= 99 ? String(n) : "";
|
|
114
|
+
};
|
|
47
115
|
const limparNo = (no, saida, documento) => {
|
|
48
116
|
const filhos = Array.prototype.slice.call(no.childNodes);
|
|
49
117
|
for (let i = 0; i < filhos.length; i++) {
|
|
@@ -62,7 +130,36 @@ const limparNo = (no, saida, documento) => {
|
|
|
62
130
|
limparNo(filho, saida, documento);
|
|
63
131
|
continue;
|
|
64
132
|
}
|
|
133
|
+
if (tag === "IMG") {
|
|
134
|
+
// Imagem sem endereço seguro some inteira (não deixa um <img> quebrado nem carregando de fora).
|
|
135
|
+
const origem = imagemSegura(filho.getAttribute("src") || "");
|
|
136
|
+
if (!origem)
|
|
137
|
+
continue;
|
|
138
|
+
const imagem = documento.createElement("img");
|
|
139
|
+
imagem.setAttribute("src", origem);
|
|
140
|
+
imagem.setAttribute("alt", String(filho.getAttribute("alt") || ""));
|
|
141
|
+
saida.appendChild(imagem);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
65
144
|
const novo = documento.createElement(tag.toLowerCase());
|
|
145
|
+
if (ALINHAVEIS.indexOf(tag) !== -1) {
|
|
146
|
+
const alinhamento = alinhamentoDe(filho);
|
|
147
|
+
if (alinhamento)
|
|
148
|
+
novo.setAttribute("style", "text-align: " + alinhamento + ";");
|
|
149
|
+
}
|
|
150
|
+
if (tag === "SPAN") {
|
|
151
|
+
const estiloValido = estiloDoSpan(filho.getAttribute("style") || "");
|
|
152
|
+
if (estiloValido)
|
|
153
|
+
novo.setAttribute("style", estiloValido);
|
|
154
|
+
}
|
|
155
|
+
if (tag === "TD" || tag === "TH") {
|
|
156
|
+
const colunas = vao(filho.getAttribute("colspan"));
|
|
157
|
+
const linhas = vao(filho.getAttribute("rowspan"));
|
|
158
|
+
if (colunas)
|
|
159
|
+
novo.setAttribute("colspan", colunas);
|
|
160
|
+
if (linhas)
|
|
161
|
+
novo.setAttribute("rowspan", linhas);
|
|
162
|
+
}
|
|
66
163
|
if (tag === "A") {
|
|
67
164
|
const href = enderecoSeguro(filho.getAttribute("href") || "");
|
|
68
165
|
if (href) {
|
|
@@ -85,8 +182,22 @@ export const sanitizarHtml = (html) => {
|
|
|
85
182
|
limparNo(molde.content, saida, document);
|
|
86
183
|
return saida.innerHTML;
|
|
87
184
|
};
|
|
185
|
+
/** A tabela vazia que o editor insere: `linhas` × `colunas` células com uma quebra, para o cursor entrar. */
|
|
186
|
+
export const htmlDaTabela = (linhas, colunas) => {
|
|
187
|
+
let celulas = "";
|
|
188
|
+
for (let c = 0; c < colunas; c++)
|
|
189
|
+
celulas += "<td><br></td>";
|
|
190
|
+
let corpo = "";
|
|
191
|
+
for (let l = 0; l < linhas; l++)
|
|
192
|
+
corpo += "<tr>" + celulas + "</tr>";
|
|
193
|
+
return "<table><tbody>" + corpo + "</tbody></table><p><br></p>";
|
|
194
|
+
};
|
|
195
|
+
/** O `<img>` que o editor insere (endereço já seguro). */
|
|
196
|
+
export const htmlDaImagem = (endereco, alt) => '<img src="' + escapar(endereco) + '" alt="' + escapar(alt || "") + '">';
|
|
88
197
|
/** O editor está "vazio": sem texto, sem lista e sem quebra solta (o `<br>` que o navegador deixa). */
|
|
89
198
|
export const htmlVazio = (html) => {
|
|
199
|
+
if (/<(img|table)[\s>]/i.test(html || ""))
|
|
200
|
+
return false;
|
|
90
201
|
const texto = (html || "").replace(/<br\s*\/?>/gi, "").replace(/<\/?(div|p|span)[^>]*>/gi, "").trim();
|
|
91
202
|
return texto === "";
|
|
92
203
|
};
|
package/dist/utils/toast.d.ts
CHANGED
|
@@ -21,7 +21,13 @@ export interface ToastOptions {
|
|
|
21
21
|
description?: string;
|
|
22
22
|
/** Variante semântica @default 'info' */
|
|
23
23
|
variant?: ToastVariant;
|
|
24
|
-
/**
|
|
24
|
+
/** Linhas de detalhe abaixo da descrição */
|
|
25
|
+
details?: string[];
|
|
26
|
+
/** Rótulo do botão de ação ("Desfazer"); só aparece com `onAction` */
|
|
27
|
+
actionLabel?: string;
|
|
28
|
+
/** Chamado ao acionar o botão de ação; depois dele o toast fecha */
|
|
29
|
+
onAction?: (event?: any) => void;
|
|
30
|
+
/** Auto-dismiss em ms; 0 = não fecha sozinho @default 5000 (0 quando há ação: dá tempo de agir, WCAG 2.2.1) */
|
|
25
31
|
duration?: number;
|
|
26
32
|
/** Exibe o botão de fechar @default true */
|
|
27
33
|
closable?: boolean;
|
|
@@ -30,6 +36,9 @@ export interface ToastData {
|
|
|
30
36
|
id: number;
|
|
31
37
|
message: string;
|
|
32
38
|
description?: string;
|
|
39
|
+
details?: string[];
|
|
40
|
+
actionLabel?: string;
|
|
41
|
+
onAction?: (event?: any) => void;
|
|
33
42
|
variant: ToastVariant;
|
|
34
43
|
duration: number;
|
|
35
44
|
closable: boolean;
|
package/dist/utils/toast.js
CHANGED
|
@@ -26,7 +26,8 @@ export const toast = (options) => {
|
|
|
26
26
|
const id = sequence;
|
|
27
27
|
items = [...items, {
|
|
28
28
|
variant: "info",
|
|
29
|
-
|
|
29
|
+
// Toast com ação não some antes de dar tempo de agir.
|
|
30
|
+
duration: options.actionLabel && options.onAction ? 0 : 5000,
|
|
30
31
|
closable: true,
|
|
31
32
|
...options,
|
|
32
33
|
id
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1doc/1ds-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"react-dom": "^18.0.0",
|
|
46
46
|
"typescript": "^5.0.0",
|
|
47
47
|
"vitest": "^3.2.7",
|
|
48
|
-
"@1doc/1ds-core": "2.
|
|
48
|
+
"@1doc/1ds-core": "2.10.0",
|
|
49
49
|
"@1doc/1ds-tokens": "0.5.0"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import Accordion from "../components/Accordion/Accordion";
|
|
4
|
+
|
|
5
|
+
const ITENS = [
|
|
6
|
+
{ id: "a", title: "Prazos", content: "Dez dias úteis." },
|
|
7
|
+
{ id: "b", title: "Anexos", content: "Até 10 MB." },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
describe("Accordion · titleVariant", () => {
|
|
11
|
+
it("neutral liga a classe do título neutro; o padrão e default não", () => {
|
|
12
|
+
const { container, rerender } = render(<Accordion items={ITENS} titleVariant="neutral" />);
|
|
13
|
+
const raiz = () => container.firstElementChild as HTMLElement;
|
|
14
|
+
expect(raiz()).toHaveClass("umds-accordion--title-neutral");
|
|
15
|
+
rerender(<Accordion items={ITENS} titleVariant="default" />);
|
|
16
|
+
expect(raiz()).not.toHaveClass("umds-accordion--title-neutral");
|
|
17
|
+
rerender(<Accordion items={ITENS} />);
|
|
18
|
+
expect(raiz()).not.toHaveClass("umds-accordion--title-neutral");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("combina com a variant do accordion", () => {
|
|
22
|
+
const { container } = render(<Accordion items={ITENS} variant="separated" titleVariant="neutral" />);
|
|
23
|
+
expect(container.firstElementChild).toHaveClass("umds-accordion--separated", "umds-accordion--title-neutral");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import AppSubheader from "../components/AppSubheader/AppSubheader";
|
|
4
|
+
import Breadcrumb from "../components/Breadcrumb/Breadcrumb";
|
|
5
|
+
|
|
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(
|
|
10
|
+
<AppSubheader>
|
|
11
|
+
<Breadcrumb items={[{ label: "Painel" }]} />
|
|
12
|
+
</AppSubheader>,
|
|
13
|
+
);
|
|
14
|
+
const contexto = container.querySelector(".umds-app-subheader__contexto") as HTMLElement;
|
|
15
|
+
expect(contexto.querySelector("a, button, [tabindex]")).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import AttachmentCard from "../components/AttachmentCard/AttachmentCard";
|
|
5
|
+
|
|
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
|
+
|
|
14
|
+
async function abrirPainel() {
|
|
15
|
+
const usuario = userEvent.setup();
|
|
16
|
+
render(<AttachmentCard fileName="cnh.pdf" documentType="CNH" status="positive" statusLabel="Pré-aprovado" statusDetails={DETALHES} />);
|
|
17
|
+
await usuario.click(screen.getByRole("button", { name: /Pré-aprovado/ }));
|
|
18
|
+
return usuario;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("AttachmentCard · o <dl> do painel é válido (axe definition-list)", () => {
|
|
22
|
+
it("o dl só tem div filhos, e cada div só tem dt e dd, nessa ordem", async () => {
|
|
23
|
+
await abrirPainel();
|
|
24
|
+
const dl = document.querySelector(".umds-attachment-card__campos") as HTMLElement;
|
|
25
|
+
expect(dl.tagName).toBe("DL");
|
|
26
|
+
const grupos = [...dl.children];
|
|
27
|
+
expect(grupos).toHaveLength(2);
|
|
28
|
+
for (const grupo of grupos) {
|
|
29
|
+
expect(grupo.tagName).toBe("DIV");
|
|
30
|
+
expect([...grupo.children].map((f) => f.tagName)).toEqual(["DT", "DD"]);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("o botão de copiar mora dentro do dd, ao lado do valor, e continua com nome", async () => {
|
|
35
|
+
await abrirPainel();
|
|
36
|
+
const dd = document.querySelector(".umds-attachment-card__campo-valor") as HTMLElement;
|
|
37
|
+
expect(dd.tagName).toBe("DD");
|
|
38
|
+
expect(dd.querySelector(".umds-attachment-card__campo-valor-texto")).toHaveTextContent("Marcos Vinícius Tavares");
|
|
39
|
+
const botao = dd.querySelector("button") as HTMLElement;
|
|
40
|
+
expect(botao).toHaveAttribute("aria-label", "Copiar Nome completo");
|
|
41
|
+
// nenhum botão solto direto num div do dl
|
|
42
|
+
for (const grupo of document.querySelectorAll(".umds-attachment-card__campo")) {
|
|
43
|
+
expect(grupo.querySelector(":scope > button")).toBeNull();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("o campo sem copyable não tem botão e o valor segue no dd", async () => {
|
|
48
|
+
await abrirPainel();
|
|
49
|
+
const campos = document.querySelectorAll(".umds-attachment-card__campo");
|
|
50
|
+
expect(campos[1].querySelector("button")).toBeNull();
|
|
51
|
+
expect(campos[1].querySelector("dd")).toHaveTextContent("123.456.789-09");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import AttachmentCard from "../components/AttachmentCard/AttachmentCard";
|
|
4
|
+
|
|
5
|
+
const tamanho = (el: Element | null) => (el as HTMLElement).style.width;
|
|
6
|
+
|
|
7
|
+
describe("AttachmentCard · size", () => {
|
|
8
|
+
it("md (padrão): ícone do preview de 48px e o do rodapé no sm do Icon", () => {
|
|
9
|
+
const { container } = render(<AttachmentCard fileName="Protocolo 104-2026.pdf" />);
|
|
10
|
+
expect(container.firstElementChild).not.toHaveClass("umds-attachment-card--sm");
|
|
11
|
+
expect(tamanho(container.querySelector(".umds-attachment-card__preview .umds-icon"))).toBe("48px");
|
|
12
|
+
const rodape = container.querySelector(".umds-attachment-card__rodape-icone .umds-icon") as HTMLElement;
|
|
13
|
+
expect(rodape).toHaveClass("umds-icon--sm");
|
|
14
|
+
expect(rodape.style.width).toBe("");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("sm: ícone do preview de 40px e o do rodapé de 12px", () => {
|
|
18
|
+
const { container } = render(<AttachmentCard fileName="Protocolo 104-2026.pdf" size="sm" />);
|
|
19
|
+
expect(container.firstElementChild).toHaveClass("umds-attachment-card--sm");
|
|
20
|
+
expect(tamanho(container.querySelector(".umds-attachment-card__preview .umds-icon"))).toBe("40px");
|
|
21
|
+
expect(tamanho(container.querySelector(".umds-attachment-card__rodape-icone .umds-icon"))).toBe("12px");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("o ícone central escolhido continua valendo no sm", () => {
|
|
25
|
+
const { container } = render(<AttachmentCard fileName="a.pdf" size="sm" documentType="CNH" status="positive" statusLabel="Pré-aprovado" />);
|
|
26
|
+
expect(container.querySelector(".umds-attachment-card__tipo-texto")).toHaveTextContent("CNH");
|
|
27
|
+
expect(container.querySelector(".umds-attachment-card__status-texto")).toHaveTextContent("Pré-aprovado");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import Badge from "../components/Badge/Badge";
|
|
4
|
+
|
|
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(
|
|
9
|
+
<Badge attached variant="neutral" appearance="solid">
|
|
10
|
+
Tarefa manual
|
|
11
|
+
</Badge>,
|
|
12
|
+
);
|
|
13
|
+
expect(container.firstElementChild).toHaveClass("umds-badge--attached", "umds-badge--neutral", "umds-badge--solid");
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -62,3 +62,24 @@ describe("Tag compacta, soft, pílula e novidade", () => {
|
|
|
62
62
|
expect(raiz(container)).toHaveClass("umds-tag--new", "umds-tag--soft");
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
|
+
|
|
66
|
+
describe("Badge attachedSide", () => {
|
|
67
|
+
it("right escreve o modificador do canto direito; left e o padrão não", () => {
|
|
68
|
+
const { container } = render(
|
|
69
|
+
<>
|
|
70
|
+
<Badge attached attachedSide="right">A</Badge>
|
|
71
|
+
<Badge attached attachedSide="left">B</Badge>
|
|
72
|
+
<Badge attached>C</Badge>
|
|
73
|
+
</>,
|
|
74
|
+
);
|
|
75
|
+
expect(container.children[0]).toHaveClass("umds-badge--attached", "umds-badge--attached-right");
|
|
76
|
+
expect(container.children[1]).not.toHaveClass("umds-badge--attached-right");
|
|
77
|
+
expect(container.children[2]).not.toHaveClass("umds-badge--attached-right");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("attachedSide sem attached não faz nada", () => {
|
|
81
|
+
const { container } = render(<Badge attachedSide="right">A</Badge>);
|
|
82
|
+
expect(raiz(container)).not.toHaveClass("umds-badge--attached-right");
|
|
83
|
+
expect(raiz(container)).not.toHaveClass("umds-badge--attached");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import Calendar from "../components/Calendar/Calendar";
|
|
4
|
+
|
|
5
|
+
describe("Calendar · cabeçalho", () => {
|
|
6
|
+
it("headerStart e headerEnd (React: props com nó) chegam ao cabeçalho", () => {
|
|
7
|
+
render(<Calendar events={[]} defaultDate="2026-10-08" headerStart={<span>Todos os documentos</span>} headerEnd={<span>Legenda</span>} />);
|
|
8
|
+
expect(screen.getByText("Todos os documentos")).toBeInTheDocument();
|
|
9
|
+
expect(screen.getByText("Legenda")).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import Coachmark from "../components/Coachmark/Coachmark";
|
|
4
|
+
|
|
5
|
+
const PASSOS = [{ target: "#alvo-a", title: "Novo: arrastar", content: "Arraste a linha para trocar a ordem." }];
|
|
6
|
+
|
|
7
|
+
function Pagina(props: any) {
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
<button id="alvo-a">A</button>
|
|
11
|
+
<button id="fora">Fora</button>
|
|
12
|
+
<Coachmark steps={PASSOS} open {...props} />
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("Coachmark · modal={false}", () => {
|
|
18
|
+
it("sem véu nem recorte; o cartão é um diálogo não modal e a raiz deixa o ponteiro passar", () => {
|
|
19
|
+
const { baseElement } = render(<Pagina modal={false} />);
|
|
20
|
+
expect(baseElement.querySelector(".umds-coachmark__veil")).toBeNull();
|
|
21
|
+
expect(baseElement.querySelector(".umds-coachmark__cutout")).toBeNull();
|
|
22
|
+
expect(baseElement.querySelector(".umds-coachmark")).toHaveClass("umds-coachmark--modeless");
|
|
23
|
+
expect(screen.getByRole("dialog", { name: "Novo: arrastar" })).toHaveAttribute("aria-modal", "false");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("o padrão continua modal: véu, recorte e aria-modal verdadeiro", () => {
|
|
27
|
+
const { baseElement } = render(<Pagina />);
|
|
28
|
+
expect(baseElement.querySelector(".umds-coachmark__veil")).not.toBeNull();
|
|
29
|
+
expect(baseElement.querySelector(".umds-coachmark__cutout")).not.toBeNull();
|
|
30
|
+
expect(baseElement.querySelector(".umds-coachmark")).not.toHaveClass("umds-coachmark--modeless");
|
|
31
|
+
expect(screen.getByRole("dialog")).toHaveAttribute("aria-modal", "true");
|
|
32
|
+
const { unmount } = render(<Pagina modal />);
|
|
33
|
+
unmount();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("o foco entra no cartão, mas o Tab não fica preso", async () => {
|
|
37
|
+
render(<Pagina modal={false} />);
|
|
38
|
+
const cartao = screen.getByRole("dialog");
|
|
39
|
+
await waitFor(() => expect(cartao).toHaveFocus());
|
|
40
|
+
const concluir = screen.getByRole("button", { name: "Concluir" });
|
|
41
|
+
concluir.focus();
|
|
42
|
+
const evento = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
|
|
43
|
+
concluir.dispatchEvent(evento);
|
|
44
|
+
expect(evento.defaultPrevented).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("no modal o Tab continua preso (o último volta ao primeiro)", () => {
|
|
48
|
+
render(<Pagina />);
|
|
49
|
+
const concluir = screen.getByRole("button", { name: "Concluir" });
|
|
50
|
+
concluir.focus();
|
|
51
|
+
const evento = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
|
|
52
|
+
concluir.dispatchEvent(evento);
|
|
53
|
+
expect(evento.defaultPrevented).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("a página segue clicável: o clique fora não é capturado e o tour não fecha sozinho", () => {
|
|
57
|
+
const aoFechar = vi.fn();
|
|
58
|
+
render(<Pagina modal={false} onClose={aoFechar} />);
|
|
59
|
+
fireEvent.click(screen.getByRole("button", { name: "Fora" }));
|
|
60
|
+
expect(aoFechar).not.toHaveBeenCalled();
|
|
61
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("o Esc fecha de qualquer lugar da página, uma vez só", () => {
|
|
65
|
+
const aoFechar = vi.fn();
|
|
66
|
+
render(<Pagina modal={false} onClose={aoFechar} />);
|
|
67
|
+
const fora = screen.getByRole("button", { name: "Fora" });
|
|
68
|
+
fora.focus();
|
|
69
|
+
fireEvent.keyDown(fora, { key: "Escape" });
|
|
70
|
+
expect(aoFechar).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(aoFechar).toHaveBeenCalledWith("escape");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("o Esc dentro do cartão também fecha uma vez só", async () => {
|
|
75
|
+
const aoFechar = vi.fn();
|
|
76
|
+
render(<Pagina modal={false} onClose={aoFechar} />);
|
|
77
|
+
await waitFor(() => expect(screen.getByRole("dialog")).toHaveFocus());
|
|
78
|
+
fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" });
|
|
79
|
+
expect(aoFechar).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("depois de fechar, o ouvinte do Esc sai do documento", () => {
|
|
83
|
+
const aoFechar = vi.fn();
|
|
84
|
+
const { rerender } = render(<Pagina modal={false} onClose={aoFechar} />);
|
|
85
|
+
rerender(<Pagina modal={false} open={false} onClose={aoFechar} />);
|
|
86
|
+
fireEvent.keyDown(document, { key: "Escape" });
|
|
87
|
+
expect(aoFechar).not.toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("ao fechar, o foco volta ao elemento de antes, se a pessoa não foi a outro lugar", async () => {
|
|
91
|
+
const { rerender } = render(<Pagina modal={false} open={false} />);
|
|
92
|
+
const gatilho = screen.getByRole("button", { name: "Fora" });
|
|
93
|
+
gatilho.focus();
|
|
94
|
+
expect(gatilho).toHaveFocus();
|
|
95
|
+
rerender(<Pagina modal={false} open />);
|
|
96
|
+
// O foco entra no cartão depois da montagem (um quadro de animação).
|
|
97
|
+
await waitFor(() => expect(screen.getByRole("dialog")).toHaveFocus());
|
|
98
|
+
rerender(<Pagina modal={false} open={false} />);
|
|
99
|
+
await waitFor(() => expect(gatilho).toHaveFocus());
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import ColorPicker from "../components/ColorPicker/ColorPicker";
|
|
4
|
+
|
|
5
|
+
describe("ColorPicker · size xs", () => {
|
|
6
|
+
it("xs escreve a classe do tamanho; sm e md seguem como eram", () => {
|
|
7
|
+
const { container, rerender } = render(<ColorPicker ariaLabel="Cor" size="xs" />);
|
|
8
|
+
const raiz = () => container.firstElementChild as HTMLElement;
|
|
9
|
+
expect(raiz()).toHaveClass("umds-color-picker--xs");
|
|
10
|
+
rerender(<ColorPicker ariaLabel="Cor" size="sm" />);
|
|
11
|
+
expect(raiz()).toHaveClass("umds-color-picker--sm");
|
|
12
|
+
expect(raiz()).not.toHaveClass("umds-color-picker--xs");
|
|
13
|
+
rerender(<ColorPicker ariaLabel="Cor" />);
|
|
14
|
+
expect(raiz()).toHaveClass("umds-color-picker--md");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("no xs a amostra escolhida mantém a marcação e o aria-checked", () => {
|
|
18
|
+
const { container } = render(<ColorPicker ariaLabel="Cor" size="xs" defaultValue="--umds-primitive-red-pure" />);
|
|
19
|
+
expect(screen.getByRole("radio", { name: "Vermelho" })).toHaveAttribute("aria-checked", "true");
|
|
20
|
+
expect(container.querySelectorAll(".umds-color-picker__swatch--selected")).toHaveLength(1);
|
|
21
|
+
});
|
|
22
|
+
});
|