@hexclave/dashboard-ui-components 1.0.2 → 1.0.5

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 (52) hide show
  1. package/dist/components/button.d.ts +4 -4
  2. package/dist/components/pill-toggle.js +2 -2
  3. package/dist/components/pill-toggle.js.map +1 -1
  4. package/dist/dashboard-ui-components.global.js +7447 -9242
  5. package/dist/dashboard-ui-components.global.js.map +4 -4
  6. package/dist/esm/components/button.d.ts +4 -4
  7. package/dist/esm/components/pill-toggle.js +3 -3
  8. package/dist/esm/components/pill-toggle.js.map +1 -1
  9. package/package.json +4 -3
  10. package/src/components/alert.tsx +120 -0
  11. package/src/components/analytics-chart/analytics-chart-pie.tsx +369 -0
  12. package/src/components/analytics-chart/analytics-chart.tsx +1585 -0
  13. package/src/components/analytics-chart/default-analytics-chart-tooltip.tsx +265 -0
  14. package/src/components/analytics-chart/format.ts +101 -0
  15. package/src/components/analytics-chart/index.ts +75 -0
  16. package/src/components/analytics-chart/palette.ts +68 -0
  17. package/src/components/analytics-chart/render-data-series.tsx +169 -0
  18. package/src/components/analytics-chart/state.ts +165 -0
  19. package/src/components/analytics-chart/strings.ts +72 -0
  20. package/src/components/analytics-chart/types.ts +220 -0
  21. package/src/components/badge.tsx +108 -0
  22. package/src/components/button.tsx +104 -0
  23. package/src/components/card.tsx +263 -0
  24. package/src/components/chart-card.tsx +101 -0
  25. package/src/components/chart-container.tsx +155 -0
  26. package/src/components/chart-legend.tsx +65 -0
  27. package/src/components/chart-theme.tsx +52 -0
  28. package/src/components/chart-tooltip.tsx +165 -0
  29. package/src/components/cursor-blast-effect.tsx +334 -0
  30. package/src/components/data-grid/data-grid-sizing.ts +51 -0
  31. package/src/components/data-grid/data-grid-toolbar.tsx +373 -0
  32. package/src/components/data-grid/data-grid.test.tsx +366 -0
  33. package/src/components/data-grid/data-grid.tsx +1220 -0
  34. package/src/components/data-grid/index.ts +64 -0
  35. package/src/components/data-grid/state.ts +235 -0
  36. package/src/components/data-grid/strings.ts +45 -0
  37. package/src/components/data-grid/types.ts +401 -0
  38. package/src/components/data-grid/use-data-source.ts +413 -0
  39. package/src/components/data-grid/use-url-state.test.tsx +88 -0
  40. package/src/components/data-grid/use-url-state.ts +298 -0
  41. package/src/components/dialog.tsx +207 -0
  42. package/src/components/edit-mode.tsx +17 -0
  43. package/src/components/empty-state.tsx +64 -0
  44. package/src/components/input.tsx +85 -0
  45. package/src/components/metric-card.tsx +142 -0
  46. package/src/components/pill-toggle.tsx +159 -0
  47. package/src/components/progress-bar.tsx +82 -0
  48. package/src/components/separator.tsx +36 -0
  49. package/src/components/skeleton.tsx +30 -0
  50. package/src/components/table.tsx +113 -0
  51. package/src/components/tabs.tsx +214 -0
  52. package/src/index.ts +69 -0
@@ -0,0 +1,366 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { cleanup, fireEvent, render, waitFor } from "@testing-library/react";
4
+ import { useState } from "react";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import {
7
+ createDefaultDataGridState,
8
+ DataGrid,
9
+ isDataGridInteractiveRowClickTarget,
10
+ type DataGridColumnDef,
11
+ type DataGridProps,
12
+ } from "./index";
13
+
14
+ type Row = {
15
+ id: string,
16
+ name: string,
17
+ };
18
+
19
+ const columns: DataGridColumnDef<Row>[] = [
20
+ {
21
+ id: "name",
22
+ header: "Name",
23
+ accessor: (row) => row.name,
24
+ width: 160,
25
+ minWidth: 80,
26
+ sortable: true,
27
+ type: "string",
28
+ },
29
+ ];
30
+
31
+ const wideColumns: DataGridColumnDef<Row>[] = [
32
+ {
33
+ id: "name",
34
+ header: "Name",
35
+ accessor: (row) => row.name,
36
+ width: 320,
37
+ minWidth: 80,
38
+ type: "string",
39
+ },
40
+ {
41
+ id: "email",
42
+ header: "Email",
43
+ accessor: (row) => `${row.name.toLowerCase().replaceAll(" ", ".")}@example.com`,
44
+ width: 420,
45
+ minWidth: 80,
46
+ type: "string",
47
+ },
48
+ ];
49
+
50
+ type ObserverRecord = {
51
+ options?: IntersectionObserverInit,
52
+ };
53
+
54
+ let intersectionObserverRecords: ObserverRecord[] = [];
55
+
56
+ class MockIntersectionObserver implements IntersectionObserver {
57
+ readonly root: Element | Document | null;
58
+ readonly rootMargin: string;
59
+ readonly thresholds: ReadonlyArray<number>;
60
+ private readonly callback: IntersectionObserverCallback;
61
+ private readonly record: ObserverRecord;
62
+
63
+ constructor(
64
+ callback: IntersectionObserverCallback,
65
+ options?: IntersectionObserverInit,
66
+ ) {
67
+ this.callback = callback;
68
+ this.root = options?.root ?? null;
69
+ this.rootMargin = options?.rootMargin ?? "";
70
+ this.thresholds = Array.isArray(options?.threshold)
71
+ ? options.threshold
72
+ : [options?.threshold ?? 0];
73
+ this.record = { options };
74
+ intersectionObserverRecords.push(this.record);
75
+ }
76
+
77
+ disconnect() {}
78
+ observe() {}
79
+ takeRecords(): IntersectionObserverEntry[] {
80
+ return [];
81
+ }
82
+ unobserve() {}
83
+
84
+ trigger(entry: Partial<IntersectionObserverEntry> = {}) {
85
+ this.callback(
86
+ [
87
+ {
88
+ boundingClientRect: {} as DOMRectReadOnly,
89
+ intersectionRatio: 1,
90
+ intersectionRect: {} as DOMRectReadOnly,
91
+ isIntersecting: true,
92
+ rootBounds: null,
93
+ target: document.createElement("div"),
94
+ time: 0,
95
+ ...entry,
96
+ },
97
+ ],
98
+ this,
99
+ );
100
+ }
101
+ }
102
+
103
+ class MockResizeObserver implements ResizeObserver {
104
+ disconnect() {}
105
+ observe() {}
106
+ unobserve() {}
107
+ }
108
+
109
+ function DataGridHarness(props: { fillHeight?: boolean }) {
110
+ const [state, setState] = useState(() => createDefaultDataGridState(columns));
111
+
112
+ return (
113
+ <div style={{ height: 400 }}>
114
+ <DataGrid<Row>
115
+ columns={columns}
116
+ rows={[{ id: "row-1", name: "Row 1" }]}
117
+ getRowId={(row) => row.id}
118
+ state={state}
119
+ onChange={setState}
120
+ paginationMode="infinite"
121
+ hasMore
122
+ fillHeight={props.fillHeight}
123
+ />
124
+ </div>
125
+ );
126
+ }
127
+
128
+ function InteractiveDataGridHarness(props: {
129
+ onSortChange?: DataGridProps<Row>["onSortChange"],
130
+ onSelectionChange?: DataGridProps<Row>["onSelectionChange"],
131
+ }) {
132
+ const [state, setState] = useState(() => createDefaultDataGridState(columns));
133
+
134
+ return (
135
+ <DataGrid<Row>
136
+ columns={columns}
137
+ rows={[{ id: "row-1", name: "Row 1" }]}
138
+ getRowId={(row) => row.id}
139
+ state={state}
140
+ onChange={setState}
141
+ selectionMode="multiple"
142
+ onSortChange={props.onSortChange}
143
+ onSelectionChange={props.onSelectionChange}
144
+ />
145
+ );
146
+ }
147
+
148
+ function WideDataGridHarness() {
149
+ const [state, setState] = useState(() => createDefaultDataGridState(wideColumns));
150
+
151
+ return (
152
+ <div style={{ width: 320 }}>
153
+ <DataGrid<Row>
154
+ columns={wideColumns}
155
+ rows={[{ id: "row-1", name: "Row 1" }]}
156
+ getRowId={(row) => row.id}
157
+ state={state}
158
+ onChange={setState}
159
+ />
160
+ </div>
161
+ );
162
+ }
163
+
164
+ describe("DataGrid infinite scroll observer", () => {
165
+ beforeEach(() => {
166
+ intersectionObserverRecords = [];
167
+
168
+ vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
169
+ vi.stubGlobal("ResizeObserver", MockResizeObserver);
170
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
171
+ function getBoundingClientRect() {
172
+ return {
173
+ x: 0,
174
+ y: 0,
175
+ width: 320,
176
+ height: 44,
177
+ top: 0,
178
+ left: 0,
179
+ right: 320,
180
+ bottom: 44,
181
+ toJSON() {
182
+ return this;
183
+ },
184
+ } as DOMRect;
185
+ },
186
+ );
187
+ Object.defineProperty(HTMLElement.prototype, "clientHeight", {
188
+ configurable: true,
189
+ get() {
190
+ return 400;
191
+ },
192
+ });
193
+ Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
194
+ configurable: true,
195
+ get() {
196
+ return 400;
197
+ },
198
+ });
199
+ });
200
+
201
+ afterEach(() => {
202
+ cleanup();
203
+ vi.restoreAllMocks();
204
+ vi.unstubAllGlobals();
205
+ });
206
+
207
+ it("observes against the grid body when the grid owns vertical scrolling", async () => {
208
+ const { container } = render(<DataGridHarness fillHeight />);
209
+
210
+ await waitFor(() => {
211
+ expect(intersectionObserverRecords.length).toBeGreaterThan(0);
212
+ });
213
+
214
+ const grid = container.querySelector('[role="grid"]');
215
+ expect(grid).not.toBeNull();
216
+ const scrollContainer = grid?.children.item(1);
217
+
218
+ expect(intersectionObserverRecords.at(-1)?.options?.root).toBe(scrollContainer);
219
+ });
220
+
221
+ it("falls back to the viewport when the page owns vertical scrolling", async () => {
222
+ render(<DataGridHarness fillHeight={false} />);
223
+
224
+ await waitFor(() => {
225
+ expect(intersectionObserverRecords.length).toBeGreaterThan(0);
226
+ });
227
+
228
+ expect(intersectionObserverRecords.at(-1)?.options?.root ?? null).toBeNull();
229
+ });
230
+ });
231
+
232
+ describe("DataGrid controlled callbacks", () => {
233
+ beforeEach(() => {
234
+ vi.stubGlobal("ResizeObserver", MockResizeObserver);
235
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
236
+ function getBoundingClientRect() {
237
+ return {
238
+ x: 0,
239
+ y: 0,
240
+ width: 320,
241
+ height: 44,
242
+ top: 0,
243
+ left: 0,
244
+ right: 320,
245
+ bottom: 44,
246
+ toJSON() {
247
+ return this;
248
+ },
249
+ } as DOMRect;
250
+ },
251
+ );
252
+ Object.defineProperty(HTMLElement.prototype, "clientHeight", {
253
+ configurable: true,
254
+ get() {
255
+ return 400;
256
+ },
257
+ });
258
+ Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
259
+ configurable: true,
260
+ get() {
261
+ return 400;
262
+ },
263
+ });
264
+ });
265
+
266
+ afterEach(() => {
267
+ cleanup();
268
+ vi.restoreAllMocks();
269
+ vi.unstubAllGlobals();
270
+ });
271
+
272
+ it("fires onSortChange from the current controlled sort state", () => {
273
+ const onSortChange = vi.fn();
274
+ const { getByRole } = render(<InteractiveDataGridHarness onSortChange={onSortChange} />);
275
+
276
+ fireEvent.click(getByRole("columnheader", { name: /name/i }));
277
+
278
+ expect(onSortChange).toHaveBeenCalledTimes(1);
279
+ expect(onSortChange).toHaveBeenCalledWith([{ columnId: "name", direction: "asc" }]);
280
+ });
281
+
282
+ it("fires onSelectionChange when selecting all rows", () => {
283
+ const onSelectionChange = vi.fn();
284
+ const { getByRole } = render(<InteractiveDataGridHarness onSelectionChange={onSelectionChange} />);
285
+
286
+ fireEvent.click(getByRole("checkbox", { name: /select all rows/i }));
287
+
288
+ expect(onSelectionChange).toHaveBeenCalledTimes(1);
289
+ const [selectedIds, selectedRows] = onSelectionChange.mock.calls[0];
290
+ expect([...selectedIds]).toEqual(["row-1"]);
291
+ expect(selectedRows).toEqual([{ id: "row-1", name: "Row 1" }]);
292
+ });
293
+
294
+ it("identifies nested interactive controls as row-click blockers", () => {
295
+ const cell = document.createElement("div");
296
+ const button = document.createElement("button");
297
+ const label = document.createElement("span");
298
+ label.textContent = "Open menu";
299
+ button.append(label);
300
+ cell.append(button);
301
+
302
+ expect(isDataGridInteractiveRowClickTarget(label.firstChild)).toBe(true);
303
+ expect(isDataGridInteractiveRowClickTarget(cell)).toBe(false);
304
+ });
305
+ });
306
+
307
+ describe("DataGrid horizontal scrolling", () => {
308
+ beforeEach(() => {
309
+ vi.stubGlobal("ResizeObserver", MockResizeObserver);
310
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
311
+ function getBoundingClientRect() {
312
+ return {
313
+ x: 0,
314
+ y: 0,
315
+ width: 320,
316
+ height: 44,
317
+ top: 0,
318
+ left: 0,
319
+ right: 320,
320
+ bottom: 44,
321
+ toJSON() {
322
+ return this;
323
+ },
324
+ } as DOMRect;
325
+ },
326
+ );
327
+ Object.defineProperty(HTMLElement.prototype, "clientHeight", {
328
+ configurable: true,
329
+ get() {
330
+ return 400;
331
+ },
332
+ });
333
+ Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
334
+ configurable: true,
335
+ get() {
336
+ return 400;
337
+ },
338
+ });
339
+ });
340
+
341
+ afterEach(() => {
342
+ cleanup();
343
+ vi.restoreAllMocks();
344
+ vi.unstubAllGlobals();
345
+ });
346
+
347
+ it("sizes the sticky clipping layer to the full row width", () => {
348
+ const { container } = render(<WideDataGridHarness />);
349
+
350
+ const rowsClip = container.querySelector("[data-data-grid-rows-clip]");
351
+
352
+ expect(rowsClip).toBeInstanceOf(HTMLElement);
353
+ expect((rowsClip as HTMLElement).style.minWidth).toBe("740px");
354
+ });
355
+
356
+ it("lets the columns popover escape the sticky toolbar bounds", () => {
357
+ const { container, getByTitle } = render(<WideDataGridHarness />);
358
+
359
+ fireEvent.click(getByTitle("Columns"));
360
+
361
+ const stickyChrome = container.querySelector('[role="grid"]')?.firstElementChild;
362
+ expect(stickyChrome).toBeInstanceOf(HTMLElement);
363
+ expect((stickyChrome as HTMLElement).className).toContain("overflow-visible");
364
+ expect(container.textContent).toContain("Email");
365
+ });
366
+ });