@1doc/1ds-react 2.11.1 → 2.12.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @1doc/1ds-react
2
2
 
3
+ ## 2.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d03c66d: Timeline: estado vazio dentro do componente, como o List. `isEmpty`, `emptyTitle`, `emptyDescription`, `emptyIcon`, `emptySize`, `emptyActionLabel` com `onEmptyAction` e o slot `empty` (no React, a prop `empty`) trocam só a trilha por um `EmptyState` com `role="status"`, sem desmontar o que está em volta. Sem `isEmpty`, `items` vazio mostra o vazio quando há `emptyTitle` ou o slot; sem nenhum dos dois, a trilha vazia segue como antes. No Vue, `isEmpty` ausente agora chega como `undefined` (e não `false`), para distinguir "não passou" de "passou false".
8
+
3
9
  ## 2.11.1
4
10
 
5
11
  ### Patch Changes
@@ -214,3 +214,11 @@
214
214
  .umds-timeline--sm.umds-timeline--card .umds-timeline__body {
215
215
  padding: var(--umds-primitive-spacing-mini); /* 12px */
216
216
  }
217
+
218
+ /* ── Estado vazio ──
219
+ O miolo da trilha troca de conteúdo; quem rodeia não muda. O EmptyState já
220
+ tem o próprio respiro e alinhamento, o wrapper só dá o lugar. */
221
+ .umds-timeline-empty {
222
+ display: block;
223
+ font-family: var(--umds-semantic-font-family-base);
224
+ }
@@ -35,20 +35,56 @@ export interface TimelineItem {
35
35
  * Extraída do histórico de versões do processo e da timeline de despachos da
36
36
  * 1Doc.
37
37
  *
38
+ * Sem eventos, a trilha troca por um estado vazio dentro do próprio componente:
39
+ * um `EmptyState` (`emptyTitle`, `emptyDescription`, `emptyIcon`,
40
+ * `emptyActionLabel` com `onEmptyAction`) ou, para um conteúdo próprio, o slot
41
+ * `empty` (no React, a prop `empty` recebe o nó). Só o miolo troca: o título do
42
+ * cartão, os filtros e o que mais estiver ao redor continuam montados, e o
43
+ * vazio é anunciado ao leitor de tela (`role="status"`). Passe `isEmpty` quando
44
+ * a decisão for sua (`:is-empty="!eventos.length"`); sem ele, `items` vazio já
45
+ * mostra o vazio, desde que haja `emptyTitle` ou o slot. Sem nenhum dos dois,
46
+ * `items` vazio rende a trilha sem eventos, como sempre.
47
+ *
38
48
  * @property {TimelineItem[]} items - Eventos, na ordem em que devem aparecer.
39
49
  * @property {boolean} card - Envolve o conteúdo de cada evento num card branco bordered.
40
50
  * @property {string} size - Tamanho do marcador e do texto: 'sm' | 'md'.
41
51
  * @property {string} ariaLabel - Nome acessível da lista. Padrão: "Linha do tempo".
52
+ * @property {boolean} isEmpty - Decide o estado vazio: `true` mostra o vazio no lugar da trilha, `false` mantém a trilha. Sem ele, o vazio aparece quando `items` está vazio e há `emptyTitle` ou o slot `empty`.
53
+ * @property {string} emptyTitle - Título do estado vazio (padrão: "Nenhum evento registrado")
54
+ * @property {string} emptyDescription - Texto de apoio do estado vazio
55
+ * @property {IconProp} emptyIcon - Ícone do estado vazio (objeto do Font Awesome)
56
+ * @property {string} emptySize - Tamanho do estado vazio: 'sm' (padrão, compacto) | 'md' | 'lg'
57
+ * @property {string} emptyActionLabel - Rótulo do botão do estado vazio (junto de onEmptyAction)
58
+ * @property {function} onEmptyAction - Callback do botão do estado vazio
59
+ * @property {any} empty - Slot `empty`: conteúdo próprio no lugar do EmptyState (React node ou Vue slot)
42
60
  * @property {string} className - Classes CSS adicionais.
43
- * @quandoUsar Mostrar o que aconteceu e quando — auditoria, histórico de versões, despachos de um processo.
44
- * @quandoNaoUsar Para um processo com etapas futuras e progresso, use `Stepper`. Para uma lista sem eixo de tempo, use `List` ou `Card`.
61
+ * @slot empty - Conteúdo do estado vazio (substitui o EmptyState padrão).
62
+ * @quandoUsar Mostrar o que aconteceu e quando — auditoria, histórico de versões, despachos de um processo — inclusive quando ainda não aconteceu nada: o "nenhum despacho registrado" fica no componente, sem `v-if` por fora.
63
+ * @quandoNaoUsar Para um processo com etapas futuras e progresso, use `Stepper`. Para uma lista sem eixo de tempo, use `List` ou `Card`. Para um vazio que ocupa a tela inteira (sem trilha), use `EmptyState` direto.
45
64
  */
46
65
  export interface TimelineProps {
47
66
  items: TimelineItem[];
48
67
  card?: boolean;
49
68
  size?: "sm" | "md";
50
69
  ariaLabel?: string;
70
+ /** Decide o estado vazio. Sem ele, vale `items` vazio com `emptyTitle` ou slot. */
71
+ isEmpty?: boolean;
72
+ /** Título do estado vazio. @default 'Nenhum evento registrado' */
73
+ emptyTitle?: string;
74
+ /** Texto de apoio do estado vazio. */
75
+ emptyDescription?: string;
76
+ /** Ícone do estado vazio: o objeto importado do Font Awesome. */
77
+ emptyIcon?: IconProp;
78
+ /** Tamanho do estado vazio. @default 'sm' */
79
+ emptySize?: "sm" | "md" | "lg";
80
+ /** Rótulo do botão do estado vazio (exibido junto de `onEmptyAction`). */
81
+ emptyActionLabel?: string;
82
+ /** Chamado no botão do estado vazio. */
83
+ onEmptyAction?: () => void;
84
+ /** Conteúdo próprio do estado vazio. No Vue, slot `empty`. */
85
+ empty?: any;
51
86
  className?: string;
52
87
  }
88
+ import type { IconProp } from "../Icon/Icon";
53
89
  declare function Timeline(props: TimelineProps): import("react/jsx-runtime").JSX.Element;
54
90
  export default Timeline;
@@ -1,8 +1,36 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import './Timeline.css';
3
3
  import Text from "../Text/Text";
4
4
  import Icon from "../Icon/Icon";
5
+ import EmptyState from "../EmptyState/EmptyState";
5
6
  function Timeline(props) {
7
+ function showEmpty() {
8
+ if (props.isEmpty !== undefined && props.isEmpty !== null) {
9
+ return !!props.isEmpty;
10
+ }
11
+ const semEventos = !props.items || props.items.length === 0;
12
+ return semEventos && !!(props.emptyTitle || props.empty);
13
+ }
14
+ function showItems() {
15
+ return !showEmpty();
16
+ }
17
+ function classesDoVazio() {
18
+ return ["umds-timeline-empty", props.className].filter(Boolean).join(" ");
19
+ }
20
+ function emptyTitleText() {
21
+ return props.emptyTitle || "Nenhum evento registrado";
22
+ }
23
+ function emptyActionProps() {
24
+ if (!props.onEmptyAction)
25
+ return {};
26
+ return {
27
+ actionLabel: props.emptyActionLabel,
28
+ onAction: props.onEmptyAction,
29
+ };
30
+ }
31
+ function emptySizeValue() {
32
+ return props.emptySize || "sm";
33
+ }
6
34
  function classesDaLista() {
7
35
  return [
8
36
  "umds-timeline",
@@ -36,6 +64,6 @@ function Timeline(props) {
36
64
  function temCabecalho(item) {
37
65
  return !!(item.title || item.timestamp || item.actor);
38
66
  }
39
- return (_jsx("ol", { role: "list", "aria-label": props.ariaLabel || "Linha do tempo", className: classesDaLista(), children: props.items?.map((item, index) => (_jsxs("li", { "aria-current": item.current ? "true" : undefined, className: classesDoItem(item), children: [_jsxs("div", { "aria-hidden": "true", className: "umds-timeline__rail", children: [_jsx("span", { className: classesDoMarcador(item), children: item.icon ? _jsx(Icon, { size: "sm", icon: item.icon }) : null }), !ehUltimo(index) ? (_jsx("span", { className: "umds-timeline__connector" })) : null] }), _jsxs("div", { className: "umds-timeline__body", children: [temCabecalho(item) ? (_jsxs("div", { className: "umds-timeline__header", children: [item.title ? (_jsx(Text, { as: "span", variant: "body2", weight: "semibold", children: item.title })) : null, item.timestamp ? (_jsx(Text, { as: "span", variant: "caption", color: "secondary", children: item.timestamp })) : null, item.actor ? (_jsx(Text, { as: "span", variant: "caption", color: "secondary", children: item.actor })) : null] })) : null, item.description ? (_jsx(Text, { as: "p", variant: "body2", color: "secondary", children: item.description })) : null] })] }, index))) }));
67
+ return (_jsxs(_Fragment, { children: [showEmpty() ? (_jsx("div", { role: "status", className: classesDoVazio(), children: _jsx(_Fragment, { children: props.empty || (_jsx(EmptyState, { icon: props.emptyIcon, title: emptyTitleText(), description: props.emptyDescription, size: emptySizeValue(), ...emptyActionProps() })) }) })) : null, showItems() ? (_jsx("ol", { role: "list", "aria-label": props.ariaLabel || "Linha do tempo", className: classesDaLista(), children: props.items?.map((item, index) => (_jsxs("li", { "aria-current": item.current ? "true" : undefined, className: classesDoItem(item), children: [_jsxs("div", { "aria-hidden": "true", className: "umds-timeline__rail", children: [_jsx("span", { className: classesDoMarcador(item), children: item.icon ? _jsx(Icon, { size: "sm", icon: item.icon }) : null }), !ehUltimo(index) ? (_jsx("span", { className: "umds-timeline__connector" })) : null] }), _jsxs("div", { className: "umds-timeline__body", children: [temCabecalho(item) ? (_jsxs("div", { className: "umds-timeline__header", children: [item.title ? (_jsx(Text, { as: "span", variant: "body2", weight: "semibold", children: item.title })) : null, item.timestamp ? (_jsx(Text, { as: "span", variant: "caption", color: "secondary", children: item.timestamp })) : null, item.actor ? (_jsx(Text, { as: "span", variant: "caption", color: "secondary", children: item.actor })) : null] })) : null, item.description ? (_jsx(Text, { as: "p", variant: "body2", color: "secondary", children: item.description })) : null] })] }, index))) })) : null] }));
40
68
  }
41
69
  export default Timeline;
@@ -1,6 +1,7 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { describe, expect, it } from "vitest";
3
- import { render, screen } from "@testing-library/react";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { render, screen, waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
4
5
  import { faCheck } from "@fortawesome/free-solid-svg-icons";
5
6
  import Timeline from "../components/Timeline/Timeline";
6
7
  /**
@@ -71,3 +72,72 @@ describe("Timeline · conteúdo", () => {
71
72
  expect(screen.getByRole("list", { name: "Despachos do processo" })).toBeInTheDocument();
72
73
  });
73
74
  });
75
+ describe("Timeline · estado vazio", () => {
76
+ it("isEmpty troca a trilha pelo vazio, com role status e o título padrão", () => {
77
+ const { container } = render(_jsx(Timeline, { items: EVENTOS, isEmpty: true }));
78
+ expect(screen.getByRole("status")).toBeInTheDocument();
79
+ expect(screen.getByText("Nenhum evento registrado")).toBeInTheDocument();
80
+ expect(screen.queryByRole("list")).toBeNull();
81
+ expect(screen.queryAllByRole("listitem")).toHaveLength(0);
82
+ expect(container.querySelector(".umds-timeline__marker")).toBeNull();
83
+ });
84
+ it("título, descrição e ícone do vazio vêm das props", () => {
85
+ const { container } = render(_jsx(Timeline, { items: [], isEmpty: true, emptyTitle: "Nenhum despacho registrado", emptyDescription: "O hist\u00F3rico aparece aqui.", emptyIcon: faCheck }));
86
+ expect(screen.getByText("Nenhum despacho registrado")).toBeInTheDocument();
87
+ expect(screen.getByText("O histórico aparece aqui.")).toBeInTheDocument();
88
+ expect(container.querySelector(".umds-empty-state svg")).toBeInTheDocument();
89
+ });
90
+ it("sem isEmpty, items vazio com emptyTitle mostra o vazio", () => {
91
+ render(_jsx(Timeline, { items: [], emptyTitle: "Nada por aqui" }));
92
+ expect(screen.getByRole("status")).toBeInTheDocument();
93
+ expect(screen.getByText("Nada por aqui")).toBeInTheDocument();
94
+ expect(screen.queryByRole("list")).toBeNull();
95
+ });
96
+ it("sem isEmpty, items vazio com a prop empty (slot) mostra o conteúdo próprio", () => {
97
+ render(_jsx(Timeline, { items: [], empty: _jsx("span", { "data-testid": "proprio", children: "Sem despachos" }) }));
98
+ expect(screen.getByRole("status")).toContainElement(screen.getByTestId("proprio"));
99
+ // o EmptyState padrão não entra: o conteúdo próprio o substitui
100
+ expect(screen.queryByText("Nenhum evento registrado")).toBeNull();
101
+ });
102
+ it("sem isEmpty e sem emptyTitle nem slot, items vazio segue sendo a trilha vazia", () => {
103
+ render(_jsx(Timeline, { items: [] }));
104
+ expect(screen.queryByRole("status")).toBeNull();
105
+ expect(screen.getByRole("list")).toBeInTheDocument();
106
+ });
107
+ it("isEmpty false manda: items vazio com emptyTitle mantém a trilha", () => {
108
+ render(_jsx(Timeline, { items: [], isEmpty: false, emptyTitle: "Nada por aqui" }));
109
+ expect(screen.queryByRole("status")).toBeNull();
110
+ expect(screen.getByRole("list")).toBeInTheDocument();
111
+ });
112
+ it("items com eventos e emptyTitle, sem isEmpty, mostra a trilha", () => {
113
+ render(_jsx(Timeline, { items: EVENTOS, emptyTitle: "Nada por aqui" }));
114
+ expect(screen.queryByRole("status")).toBeNull();
115
+ expect(screen.getAllByRole("listitem")).toHaveLength(3);
116
+ });
117
+ it("o botão do vazio só aparece com rótulo e callback, e chama o callback", async () => {
118
+ const usuario = userEvent.setup();
119
+ const aoAgir = vi.fn();
120
+ const { rerender } = render(_jsx(Timeline, { items: [], isEmpty: true, emptyActionLabel: "Limpar filtro" }));
121
+ expect(screen.queryByRole("button")).toBeNull();
122
+ rerender(_jsx(Timeline, { items: [], isEmpty: true, emptyActionLabel: "Limpar filtro", onEmptyAction: aoAgir }));
123
+ await usuario.click(screen.getByRole("button", { name: "Limpar filtro" }));
124
+ expect(aoAgir).toHaveBeenCalledTimes(1);
125
+ });
126
+ it("o entorno não desmonta: só o miolo troca ao esvaziar e ao voltar", async () => {
127
+ const { rerender } = render(_jsxs("section", { children: [_jsx("h3", { "data-testid": "titulo", children: "Hist\u00F3rico" }), _jsx(Timeline, { items: EVENTOS, isEmpty: false, emptyTitle: "Sem eventos" })] }));
128
+ const titulo = screen.getByTestId("titulo");
129
+ expect(screen.getAllByRole("listitem")).toHaveLength(3);
130
+ rerender(_jsxs("section", { children: [_jsx("h3", { "data-testid": "titulo", children: "Hist\u00F3rico" }), _jsx(Timeline, { items: [], isEmpty: true, emptyTitle: "Sem eventos" })] }));
131
+ await waitFor(() => expect(screen.getByRole("status")).toBeInTheDocument());
132
+ expect(screen.getByTestId("titulo")).toBe(titulo);
133
+ expect(screen.queryByRole("list")).toBeNull();
134
+ rerender(_jsxs("section", { children: [_jsx("h3", { "data-testid": "titulo", children: "Hist\u00F3rico" }), _jsx(Timeline, { items: EVENTOS, isEmpty: false, emptyTitle: "Sem eventos" })] }));
135
+ await waitFor(() => expect(screen.getAllByRole("listitem")).toHaveLength(3));
136
+ expect(screen.getByTestId("titulo")).toBe(titulo);
137
+ expect(screen.queryByRole("status")).toBeNull();
138
+ });
139
+ it("className vai ao wrapper do vazio", () => {
140
+ render(_jsx(Timeline, { items: [], isEmpty: true, className: "meu-vazio" }));
141
+ expect(screen.getByRole("status")).toHaveClass("meu-vazio");
142
+ });
143
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1doc/1ds-react",
3
- "version": "2.11.1",
3
+ "version": "2.12.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.11.1",
48
+ "@1doc/1ds-core": "2.12.0",
49
49
  "@1doc/1ds-tokens": "0.7.0"
50
50
  },
51
51
  "publishConfig": {
@@ -1,5 +1,6 @@
1
- import { describe, expect, it } from "vitest";
2
- import { render, screen } from "@testing-library/react";
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { render, screen, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
3
4
  import { faCheck } from "@fortawesome/free-solid-svg-icons";
4
5
  import Timeline from "../components/Timeline/Timeline";
5
6
 
@@ -98,3 +99,120 @@ describe("Timeline · conteúdo", () => {
98
99
  ).toBeInTheDocument();
99
100
  });
100
101
  });
102
+
103
+ describe("Timeline · estado vazio", () => {
104
+ it("isEmpty troca a trilha pelo vazio, com role status e o título padrão", () => {
105
+ const { container } = render(<Timeline items={EVENTOS} isEmpty />);
106
+ expect(screen.getByRole("status")).toBeInTheDocument();
107
+ expect(screen.getByText("Nenhum evento registrado")).toBeInTheDocument();
108
+ expect(screen.queryByRole("list")).toBeNull();
109
+ expect(screen.queryAllByRole("listitem")).toHaveLength(0);
110
+ expect(container.querySelector(".umds-timeline__marker")).toBeNull();
111
+ });
112
+
113
+ it("título, descrição e ícone do vazio vêm das props", () => {
114
+ const { container } = render(
115
+ <Timeline
116
+ items={[]}
117
+ isEmpty
118
+ emptyTitle="Nenhum despacho registrado"
119
+ emptyDescription="O histórico aparece aqui."
120
+ emptyIcon={faCheck}
121
+ />,
122
+ );
123
+ expect(screen.getByText("Nenhum despacho registrado")).toBeInTheDocument();
124
+ expect(screen.getByText("O histórico aparece aqui.")).toBeInTheDocument();
125
+ expect(container.querySelector(".umds-empty-state svg")).toBeInTheDocument();
126
+ });
127
+
128
+ it("sem isEmpty, items vazio com emptyTitle mostra o vazio", () => {
129
+ render(<Timeline items={[]} emptyTitle="Nada por aqui" />);
130
+ expect(screen.getByRole("status")).toBeInTheDocument();
131
+ expect(screen.getByText("Nada por aqui")).toBeInTheDocument();
132
+ expect(screen.queryByRole("list")).toBeNull();
133
+ });
134
+
135
+ it("sem isEmpty, items vazio com a prop empty (slot) mostra o conteúdo próprio", () => {
136
+ render(
137
+ <Timeline items={[]} empty={<span data-testid="proprio">Sem despachos</span>} />,
138
+ );
139
+ expect(screen.getByRole("status")).toContainElement(
140
+ screen.getByTestId("proprio"),
141
+ );
142
+ // o EmptyState padrão não entra: o conteúdo próprio o substitui
143
+ expect(screen.queryByText("Nenhum evento registrado")).toBeNull();
144
+ });
145
+
146
+ it("sem isEmpty e sem emptyTitle nem slot, items vazio segue sendo a trilha vazia", () => {
147
+ render(<Timeline items={[]} />);
148
+ expect(screen.queryByRole("status")).toBeNull();
149
+ expect(screen.getByRole("list")).toBeInTheDocument();
150
+ });
151
+
152
+ it("isEmpty false manda: items vazio com emptyTitle mantém a trilha", () => {
153
+ render(<Timeline items={[]} isEmpty={false} emptyTitle="Nada por aqui" />);
154
+ expect(screen.queryByRole("status")).toBeNull();
155
+ expect(screen.getByRole("list")).toBeInTheDocument();
156
+ });
157
+
158
+ it("items com eventos e emptyTitle, sem isEmpty, mostra a trilha", () => {
159
+ render(<Timeline items={EVENTOS} emptyTitle="Nada por aqui" />);
160
+ expect(screen.queryByRole("status")).toBeNull();
161
+ expect(screen.getAllByRole("listitem")).toHaveLength(3);
162
+ });
163
+
164
+ it("o botão do vazio só aparece com rótulo e callback, e chama o callback", async () => {
165
+ const usuario = userEvent.setup();
166
+ const aoAgir = vi.fn();
167
+ const { rerender } = render(
168
+ <Timeline items={[]} isEmpty emptyActionLabel="Limpar filtro" />,
169
+ );
170
+ expect(screen.queryByRole("button")).toBeNull();
171
+ rerender(
172
+ <Timeline
173
+ items={[]}
174
+ isEmpty
175
+ emptyActionLabel="Limpar filtro"
176
+ onEmptyAction={aoAgir}
177
+ />,
178
+ );
179
+ await usuario.click(screen.getByRole("button", { name: "Limpar filtro" }));
180
+ expect(aoAgir).toHaveBeenCalledTimes(1);
181
+ });
182
+
183
+ it("o entorno não desmonta: só o miolo troca ao esvaziar e ao voltar", async () => {
184
+ const { rerender } = render(
185
+ <section>
186
+ <h3 data-testid="titulo">Histórico</h3>
187
+ <Timeline items={EVENTOS} isEmpty={false} emptyTitle="Sem eventos" />
188
+ </section>,
189
+ );
190
+ const titulo = screen.getByTestId("titulo");
191
+ expect(screen.getAllByRole("listitem")).toHaveLength(3);
192
+
193
+ rerender(
194
+ <section>
195
+ <h3 data-testid="titulo">Histórico</h3>
196
+ <Timeline items={[]} isEmpty emptyTitle="Sem eventos" />
197
+ </section>,
198
+ );
199
+ await waitFor(() => expect(screen.getByRole("status")).toBeInTheDocument());
200
+ expect(screen.getByTestId("titulo")).toBe(titulo);
201
+ expect(screen.queryByRole("list")).toBeNull();
202
+
203
+ rerender(
204
+ <section>
205
+ <h3 data-testid="titulo">Histórico</h3>
206
+ <Timeline items={EVENTOS} isEmpty={false} emptyTitle="Sem eventos" />
207
+ </section>,
208
+ );
209
+ await waitFor(() => expect(screen.getAllByRole("listitem")).toHaveLength(3));
210
+ expect(screen.getByTestId("titulo")).toBe(titulo);
211
+ expect(screen.queryByRole("status")).toBeNull();
212
+ });
213
+
214
+ it("className vai ao wrapper do vazio", () => {
215
+ render(<Timeline items={[]} isEmpty className="meu-vazio" />);
216
+ expect(screen.getByRole("status")).toHaveClass("meu-vazio");
217
+ });
218
+ });