@purpurds/table 8.3.0 → 8.4.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 (82) hide show
  1. package/dist/LICENSE.txt +205 -35
  2. package/dist/drag-indicator-circle.d.ts +13 -0
  3. package/dist/drag-indicator-circle.d.ts.map +1 -0
  4. package/dist/draggable-table.d.ts +23 -0
  5. package/dist/draggable-table.d.ts.map +1 -0
  6. package/dist/empty-table.d.ts +14 -0
  7. package/dist/empty-table.d.ts.map +1 -0
  8. package/dist/loading-table-rows.d.ts +13 -0
  9. package/dist/loading-table-rows.d.ts.map +1 -0
  10. package/dist/styles.css +1 -1
  11. package/dist/table-body.d.ts +2 -2
  12. package/dist/table-body.d.ts.map +1 -1
  13. package/dist/table-column-header-cell.d.ts +15 -2
  14. package/dist/table-column-header-cell.d.ts.map +1 -1
  15. package/dist/table-content.d.ts +42 -0
  16. package/dist/table-content.d.ts.map +1 -0
  17. package/dist/table-headers.d.ts +28 -0
  18. package/dist/table-headers.d.ts.map +1 -0
  19. package/dist/table-row-cell-skeleton.d.ts +1 -1
  20. package/dist/table-row-cell-skeleton.d.ts.map +1 -1
  21. package/dist/table-row-cell.d.ts +5 -2
  22. package/dist/table-row-cell.d.ts.map +1 -1
  23. package/dist/table-row.d.ts +2 -2
  24. package/dist/table-row.d.ts.map +1 -1
  25. package/dist/table-settings-drawer.d.ts +44 -11
  26. package/dist/table-settings-drawer.d.ts.map +1 -1
  27. package/dist/table.cjs.js +89 -85
  28. package/dist/table.cjs.js.map +1 -1
  29. package/dist/table.d.ts +3 -3
  30. package/dist/table.d.ts.map +1 -1
  31. package/dist/table.es.js +14397 -10195
  32. package/dist/table.es.js.map +1 -1
  33. package/dist/test-utils/helpers.d.ts +1 -0
  34. package/dist/test-utils/helpers.d.ts.map +1 -1
  35. package/dist/types.d.ts +23 -2
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/use-drag-handle.hook.d.ts +15 -0
  38. package/dist/use-drag-handle.hook.d.ts.map +1 -0
  39. package/dist/use-drag-indicator-position.hook.d.ts +19 -0
  40. package/dist/use-drag-indicator-position.hook.d.ts.map +1 -0
  41. package/dist/use-drop-indicator.hook.d.ts +15 -0
  42. package/dist/use-drop-indicator.hook.d.ts.map +1 -0
  43. package/dist/use-element-visibility.hook.d.ts +4 -0
  44. package/dist/use-element-visibility.hook.d.ts.map +1 -0
  45. package/dist/use-table-scroll.hook.d.ts +6 -0
  46. package/dist/use-table-scroll.hook.d.ts.map +1 -0
  47. package/dist/utils/custom-keyboard-coordinates.d.ts +8 -0
  48. package/dist/utils/custom-keyboard-coordinates.d.ts.map +1 -0
  49. package/package.json +27 -23
  50. package/src/drag-indicator-circle.tsx +36 -0
  51. package/src/draggable-table.test.tsx +381 -0
  52. package/src/draggable-table.tsx +191 -0
  53. package/src/empty-table.tsx +54 -0
  54. package/src/loading-table-rows.tsx +41 -0
  55. package/src/table-body.tsx +1 -3
  56. package/src/table-column-header-cell.tsx +135 -64
  57. package/src/table-content-drag.test.tsx +505 -0
  58. package/src/table-content.tsx +165 -0
  59. package/src/table-dnd-integration.test.tsx +425 -0
  60. package/src/table-drag-and-drop.test.tsx +276 -0
  61. package/src/table-headers.tsx +118 -0
  62. package/src/table-row-cell-skeleton.tsx +1 -1
  63. package/src/table-row-cell.test.tsx +2 -1
  64. package/src/table-row-cell.tsx +42 -31
  65. package/src/table-row.tsx +1 -3
  66. package/src/table-settings-drawer.module.scss +165 -2
  67. package/src/table-settings-drawer.test.tsx +0 -99
  68. package/src/table-settings-drawer.tsx +359 -53
  69. package/src/table.module.scss +191 -30
  70. package/src/table.stories.tsx +60 -4
  71. package/src/table.test.tsx +5 -1
  72. package/src/table.tsx +255 -213
  73. package/src/test-utils/helpers.ts +2 -0
  74. package/src/types.ts +25 -2
  75. package/src/use-drag-handle.hook.tsx +60 -0
  76. package/src/use-drag-handle.test.tsx +380 -0
  77. package/src/use-drag-indicator-position.hook.ts +74 -0
  78. package/src/use-drop-indicator.hook.ts +46 -0
  79. package/src/use-element-visibility.hook.ts +28 -0
  80. package/src/use-table-scroll.hook.tsx +30 -0
  81. package/src/utils/custom-keyboard-coordinates.ts +83 -0
  82. package/vitest.setup.ts +1 -1
@@ -0,0 +1,381 @@
1
+ import React from "react";
2
+ import { render, screen, waitFor } from "@testing-library/react";
3
+ import { axe } from "vitest-axe";
4
+
5
+ import { DraggableTable } from "./draggable-table";
6
+
7
+ const mockColumnDragAriaLabels = {
8
+ grab: "Drag to reorder column",
9
+ grabbing: "Dragging column",
10
+ };
11
+
12
+ const mockHandleDragEnd = vi.fn();
13
+ const mockSetActiveId = vi.fn();
14
+
15
+ // Mock tanstack table - using minimal interface for testing
16
+ const mockTanstackTable = {
17
+ getHeaderGroups: vi.fn(() => [
18
+ {
19
+ id: "header-group-1",
20
+ headers: [
21
+ {
22
+ id: "id",
23
+ column: { id: "id" },
24
+ getContext: () => ({ column: { id: "id" } }),
25
+ },
26
+ {
27
+ id: "name",
28
+ column: { id: "name" },
29
+ getContext: () => ({ column: { id: "name" } }),
30
+ },
31
+ ],
32
+ },
33
+ ]),
34
+ } as unknown as Parameters<typeof DraggableTable>[0]["tanstackTable"];
35
+
36
+ describe("DraggableTable Component", () => {
37
+ const defaultProps = {
38
+ activeId: null,
39
+ setActiveId: mockSetActiveId,
40
+ handleDragEnd: mockHandleDragEnd,
41
+ tableRows: [],
42
+ tanstackTable: mockTanstackTable,
43
+ variant: "primary",
44
+ fullWidth: false,
45
+ showColumnFiltersEnabled: false,
46
+ columnDragAriaLabelsCopy: mockColumnDragAriaLabels,
47
+ rootClassName: "purpur-table",
48
+ };
49
+
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ });
53
+
54
+ describe("Rendering", () => {
55
+ it("should render DndContext wrapper", () => {
56
+ render(
57
+ <DraggableTable {...defaultProps}>
58
+ <div data-testid="table-content">Table Content</div>
59
+ </DraggableTable>
60
+ );
61
+
62
+ // Check for presence of DnD context
63
+ expect(screen.getByTestId("table-content")).toBeInTheDocument();
64
+ });
65
+
66
+ it("should render children components", () => {
67
+ render(
68
+ <DraggableTable {...defaultProps}>
69
+ <div data-testid="child-component">Child Component</div>
70
+ </DraggableTable>
71
+ );
72
+
73
+ expect(screen.getByTestId("child-component")).toBeInTheDocument();
74
+ });
75
+
76
+ it("should be accessible", async () => {
77
+ const { container } = render(
78
+ <DraggableTable {...defaultProps}>
79
+ <table>
80
+ <thead>
81
+ <tr>
82
+ <th>Header</th>
83
+ </tr>
84
+ </thead>
85
+ <tbody>
86
+ <tr>
87
+ <td>Cell</td>
88
+ </tr>
89
+ </tbody>
90
+ </table>
91
+ </DraggableTable>
92
+ );
93
+
94
+ const results = await axe(container);
95
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
96
+ // @ts-ignore
97
+ expect(results).toHaveNoViolations();
98
+ });
99
+ });
100
+
101
+ describe("Drag Overlay", () => {
102
+ it("should not show drag overlay when activeId is null", () => {
103
+ render(
104
+ <DraggableTable {...defaultProps} activeId={null}>
105
+ <div>Table Content</div>
106
+ </DraggableTable>
107
+ );
108
+
109
+ // Drag overlay should not be visible
110
+ const overlay = screen.queryByText(/drag-overlay/);
111
+ expect(overlay).not.toBeInTheDocument();
112
+ });
113
+
114
+ it("should not show drag overlay for row-selection activeId", () => {
115
+ render(
116
+ <DraggableTable {...defaultProps} activeId="row-selection">
117
+ <div>Table Content</div>
118
+ </DraggableTable>
119
+ );
120
+
121
+ // Drag overlay should not be visible for special row selection
122
+ const overlay = screen.queryByText(/drag-overlay/);
123
+ expect(overlay).not.toBeInTheDocument();
124
+ });
125
+
126
+ it("should not show drag overlay for row-toggle activeId", () => {
127
+ render(
128
+ <DraggableTable {...defaultProps} activeId="row-toggle">
129
+ <div>Table Content</div>
130
+ </DraggableTable>
131
+ );
132
+
133
+ // Drag overlay should not be visible for special row toggle
134
+ const overlay = screen.queryByText(/drag-overlay/);
135
+ expect(overlay).not.toBeInTheDocument();
136
+ });
137
+
138
+ it("should show drag overlay with proper structure when activeId is set", async () => {
139
+ const { container } = render(
140
+ <DraggableTable {...defaultProps} activeId="id">
141
+ <div>Table Content</div>
142
+ </DraggableTable>
143
+ );
144
+
145
+ // In test environment, @dnd-kit DragOverlay doesn't render the same way
146
+ // But we can verify the DndContext creates its accessibility structures
147
+ await waitFor(() => {
148
+ const dndDescribedBy = container.querySelector('[id^="DndDescribedBy"]');
149
+ const dndLiveRegion = container.querySelector('[id^="DndLiveRegion"]');
150
+ expect(dndDescribedBy).toBeInTheDocument();
151
+ expect(dndLiveRegion).toBeInTheDocument();
152
+ });
153
+ });
154
+
155
+ it("should apply correct CSS classes to drag overlay", async () => {
156
+ const activeId = "name";
157
+ const variant = "secondary";
158
+ const { container } = render(
159
+ <DraggableTable {...defaultProps} activeId={activeId} variant={variant}>
160
+ <div>Table Content</div>
161
+ </DraggableTable>
162
+ );
163
+
164
+ // In test environment, verify DndContext renders with accessibility features
165
+ await waitFor(() => {
166
+ const dndDescribedBy = container.querySelector('[id^="DndDescribedBy"]');
167
+ const dndLiveRegion = container.querySelector('[id^="DndLiveRegion"]');
168
+ expect(dndDescribedBy).toBeInTheDocument();
169
+ expect(dndLiveRegion).toBeInTheDocument();
170
+ });
171
+ });
172
+
173
+ it("should render table structure in drag overlay", async () => {
174
+ const activeId = "name";
175
+ const { container } = render(
176
+ <DraggableTable {...defaultProps} activeId={activeId} fullWidth={true}>
177
+ <div>Table Content</div>
178
+ </DraggableTable>
179
+ );
180
+
181
+ // In test environment, verify DndContext renders with accessibility features
182
+ await waitFor(() => {
183
+ const dndDescribedBy = container.querySelector('[id^="DndDescribedBy"]');
184
+ const dndLiveRegion = container.querySelector('[id^="DndLiveRegion"]');
185
+ expect(dndDescribedBy).toBeInTheDocument();
186
+ expect(dndLiveRegion).toBeInTheDocument();
187
+ });
188
+ });
189
+ });
190
+
191
+ describe("DnD Context Configuration", () => {
192
+ it("should configure sensors properly", () => {
193
+ // This is more of an integration test to ensure the component renders without errors
194
+ expect(() => {
195
+ render(
196
+ <DraggableTable {...defaultProps}>
197
+ <div>Table Content</div>
198
+ </DraggableTable>
199
+ );
200
+ }).not.toThrow();
201
+ });
202
+
203
+ it("should handle drag start event", () => {
204
+ render(
205
+ <DraggableTable {...defaultProps}>
206
+ <div>Table Content</div>
207
+ </DraggableTable>
208
+ );
209
+
210
+ // Since we can't easily trigger DnD events in tests, we verify the setup doesn't throw
211
+ expect(mockSetActiveId).not.toHaveBeenCalled();
212
+ });
213
+
214
+ it("should handle drag end event", () => {
215
+ render(
216
+ <DraggableTable {...defaultProps}>
217
+ <div>Table Content</div>
218
+ </DraggableTable>
219
+ );
220
+
221
+ expect(mockHandleDragEnd).not.toHaveBeenCalled();
222
+ });
223
+
224
+ it("should handle drag cancel event", () => {
225
+ render(
226
+ <DraggableTable {...defaultProps}>
227
+ <div>Table Content</div>
228
+ </DraggableTable>
229
+ );
230
+
231
+ // Drag cancel should reset activeId to null
232
+ expect(mockSetActiveId).not.toHaveBeenCalled();
233
+ });
234
+ });
235
+
236
+ describe("Props Handling", () => {
237
+ it("should handle different variants", async () => {
238
+ const variants = ["primary", "secondary"];
239
+
240
+ for (const variant of variants) {
241
+ const { container, unmount } = render(
242
+ <DraggableTable {...defaultProps} variant={variant} activeId="id">
243
+ <div>Table Content</div>
244
+ </DraggableTable>
245
+ );
246
+
247
+ await waitFor(() => {
248
+ const dndDescribedBy = container.querySelector('[id^="DndDescribedBy"]');
249
+ const dndLiveRegion = container.querySelector('[id^="DndLiveRegion"]');
250
+ expect(dndDescribedBy).toBeInTheDocument();
251
+ expect(dndLiveRegion).toBeInTheDocument();
252
+ });
253
+
254
+ unmount();
255
+ }
256
+ });
257
+
258
+ it("should handle fullWidth prop", async () => {
259
+ const { container } = render(
260
+ <DraggableTable {...defaultProps} fullWidth={true} activeId="id">
261
+ <div>Table Content</div>
262
+ </DraggableTable>
263
+ );
264
+
265
+ await waitFor(() => {
266
+ const dndDescribedBy = container.querySelector('[id^="DndDescribedBy"]');
267
+ const dndLiveRegion = container.querySelector('[id^="DndLiveRegion"]');
268
+ expect(dndDescribedBy).toBeInTheDocument();
269
+ expect(dndLiveRegion).toBeInTheDocument();
270
+ });
271
+ });
272
+
273
+ it("should handle showColumnFiltersEnabled prop", () => {
274
+ // This affects the table structure in the overlay
275
+ expect(() => {
276
+ render(
277
+ <DraggableTable {...defaultProps} showColumnFiltersEnabled={true} activeId="id">
278
+ <div>Table Content</div>
279
+ </DraggableTable>
280
+ );
281
+ }).not.toThrow();
282
+ });
283
+
284
+ it("should handle different rootClassName", async () => {
285
+ const customRootClassName = "custom-table";
286
+ const { container } = render(
287
+ <DraggableTable {...defaultProps} rootClassName={customRootClassName} activeId="id">
288
+ <div>Table Content</div>
289
+ </DraggableTable>
290
+ );
291
+
292
+ await waitFor(() => {
293
+ const dndDescribedBy = container.querySelector('[id^="DndDescribedBy"]');
294
+ const dndLiveRegion = container.querySelector('[id^="DndLiveRegion"]');
295
+ expect(dndDescribedBy).toBeInTheDocument();
296
+ expect(dndLiveRegion).toBeInTheDocument();
297
+ });
298
+ });
299
+ });
300
+
301
+ describe("Table Rows Handling", () => {
302
+ it("should handle empty table rows", () => {
303
+ expect(() => {
304
+ render(
305
+ <DraggableTable {...defaultProps} tableRows={[]} activeId="test">
306
+ <div>Table Content</div>
307
+ </DraggableTable>
308
+ );
309
+ }).not.toThrow();
310
+ });
311
+
312
+ it("should handle table rows with data", () => {
313
+ const mockRows = [
314
+ {
315
+ id: "row1",
316
+ getVisibleCells: () => [
317
+ {
318
+ id: "cell1",
319
+ column: { id: "name" },
320
+ },
321
+ ],
322
+ },
323
+ ] as unknown as Parameters<typeof DraggableTable>[0]["tableRows"];
324
+
325
+ expect(() => {
326
+ render(
327
+ <DraggableTable {...defaultProps} tableRows={mockRows} activeId="name">
328
+ <div>Table Content</div>
329
+ </DraggableTable>
330
+ );
331
+ }).not.toThrow();
332
+ });
333
+ });
334
+
335
+ describe("Accessibility", () => {
336
+ it("should maintain accessibility when dragging", async () => {
337
+ const { container } = render(
338
+ <DraggableTable {...defaultProps} activeId="test">
339
+ <table role="table">
340
+ <thead>
341
+ <tr>
342
+ <th>Header</th>
343
+ </tr>
344
+ </thead>
345
+ <tbody>
346
+ <tr>
347
+ <td>Cell</td>
348
+ </tr>
349
+ </tbody>
350
+ </table>
351
+ </DraggableTable>
352
+ );
353
+
354
+ const results = await axe(container);
355
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
356
+ // @ts-ignore
357
+ expect(results).toHaveNoViolations();
358
+ });
359
+
360
+ it("should preserve aria labels in overlay", async () => {
361
+ const { container } = render(
362
+ <DraggableTable {...defaultProps} activeId="id">
363
+ <div>Table Content</div>
364
+ </DraggableTable>
365
+ );
366
+
367
+ // Verify DndContext creates accessibility features
368
+ await waitFor(() => {
369
+ const dndDescribedBy = container.querySelector('[id^="DndDescribedBy"]');
370
+ const dndLiveRegion = container.querySelector('[id^="DndLiveRegion"]');
371
+ expect(dndDescribedBy).toBeInTheDocument();
372
+ expect(dndLiveRegion).toBeInTheDocument();
373
+
374
+ // Verify the live region has correct accessibility attributes
375
+ expect(dndLiveRegion).toHaveAttribute("role", "status");
376
+ expect(dndLiveRegion).toHaveAttribute("aria-live", "assertive");
377
+ expect(dndLiveRegion).toHaveAttribute("aria-atomic", "true");
378
+ });
379
+ });
380
+ });
381
+ });
@@ -0,0 +1,191 @@
1
+ import React from "react";
2
+ import {
3
+ closestCenter,
4
+ DndContext,
5
+ type DragEndEvent,
6
+ DragOverlay,
7
+ type DragStartEvent,
8
+ KeyboardSensor,
9
+ MeasuringStrategy,
10
+ PointerSensor,
11
+ TouchSensor,
12
+ type UniqueIdentifier,
13
+ useSensor,
14
+ useSensors,
15
+ } from "@dnd-kit/core";
16
+ import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
17
+ import {
18
+ purpurMotionDuration150,
19
+ purpurMotionEasingEaseInOut,
20
+ } from "@purpurds/tokens/motion/variables";
21
+ import { type Row, type RowData, type Table } from "@tanstack/react-table";
22
+ import c from "classnames/bind";
23
+
24
+ import styles from "./table.module.scss";
25
+ import { TableBody } from "./table-body";
26
+ import { TableColumnHeaderCell } from "./table-column-header-cell";
27
+ import { TableHeader } from "./table-header";
28
+ import { TableRow } from "./table-row";
29
+ import { TableRowCell } from "./table-row-cell";
30
+ import { enhancedColumnKeyboardCoordinates } from "./utils/custom-keyboard-coordinates";
31
+
32
+ const cx = c.bind(styles);
33
+
34
+ export interface ColumnDragAriaLabelsCopy {
35
+ grab: string;
36
+ grabbing: string;
37
+ }
38
+
39
+ export interface DraggableTableProps<TData extends RowData = RowData> {
40
+ children: React.ReactNode;
41
+ activeId: UniqueIdentifier | null;
42
+ setActiveId: React.Dispatch<React.SetStateAction<UniqueIdentifier | null>>;
43
+ handleDragEnd: (event: DragEndEvent) => void;
44
+ tableRows: Row<TData>[];
45
+ tanstackTable: Table<TData>;
46
+ variant: string;
47
+ fullWidth: boolean;
48
+ showColumnFiltersEnabled: boolean;
49
+ columnDragAriaLabelsCopy: ColumnDragAriaLabelsCopy;
50
+ rootClassName: string;
51
+ }
52
+
53
+ export const DraggableTable = <TData extends RowData = RowData>({
54
+ children,
55
+ activeId,
56
+ setActiveId,
57
+ handleDragEnd,
58
+ tableRows,
59
+ tanstackTable,
60
+ variant,
61
+ fullWidth,
62
+ showColumnFiltersEnabled,
63
+ columnDragAriaLabelsCopy,
64
+ rootClassName,
65
+ }: DraggableTableProps<TData>): React.ReactElement => {
66
+ const sensors = useSensors(
67
+ useSensor(PointerSensor, {
68
+ activationConstraint: {
69
+ distance: 8,
70
+ },
71
+ }),
72
+ useSensor(KeyboardSensor, {
73
+ coordinateGetter: enhancedColumnKeyboardCoordinates,
74
+ }),
75
+ useSensor(TouchSensor, {})
76
+ );
77
+
78
+ const handleDragStart = (event: DragStartEvent) => {
79
+ const { active } = event;
80
+ setActiveId(active.id);
81
+ };
82
+
83
+ const handleDragCancel = () => {
84
+ setActiveId(null);
85
+ };
86
+
87
+ return (
88
+ <DndContext
89
+ sensors={sensors}
90
+ collisionDetection={closestCenter}
91
+ onDragStart={handleDragStart}
92
+ onDragEnd={handleDragEnd}
93
+ onDragCancel={handleDragCancel}
94
+ modifiers={[restrictToHorizontalAxis]}
95
+ measuring={{
96
+ droppable: {
97
+ strategy: MeasuringStrategy.Always,
98
+ },
99
+ }}
100
+ >
101
+ {children}
102
+
103
+ <DragOverlay
104
+ dropAnimation={{
105
+ duration: Number(purpurMotionDuration150),
106
+ easing: purpurMotionEasingEaseInOut,
107
+ }}
108
+ adjustScale={false}
109
+ >
110
+ {activeId && activeId !== "row-selection" && activeId !== "row-toggle" ? (
111
+ <div
112
+ className={cx([`purpur-table__drag-overlay`, `purpur-table__drag-overlay--${variant}`])}
113
+ aria-hidden="true"
114
+ >
115
+ <table
116
+ className={cx([
117
+ `${rootClassName}__table`,
118
+ { [`${rootClassName}__table--full-width`]: fullWidth },
119
+ ])}
120
+ role="presentation"
121
+ aria-hidden="true"
122
+ >
123
+ <TableHeader columnFiltersEnabled={showColumnFiltersEnabled}>
124
+ {tanstackTable.getHeaderGroups().map((headerGroup) => (
125
+ <TableRow key={headerGroup.id}>
126
+ {headerGroup.headers
127
+ .filter((h) => h.id === activeId)
128
+ .map((header) => (
129
+ <TableColumnHeaderCell
130
+ key={header.id}
131
+ header={header}
132
+ tanstackTable={tanstackTable}
133
+ tableHasFilters={showColumnFiltersEnabled}
134
+ stickyColumn={false}
135
+ stickyHeaders={false}
136
+ isScrolled={false}
137
+ showBorder={false}
138
+ enableSorting={false}
139
+ overlayActive={true}
140
+ enableColumnDrag={true}
141
+ columnDragAriaLabel={columnDragAriaLabelsCopy.grab}
142
+ />
143
+ ))}
144
+ </TableRow>
145
+ ))}
146
+ </TableHeader>
147
+ <TableBody>
148
+ {tableRows.map((row) => (
149
+ <TableRow key={row.id} isSelected={false}>
150
+ {row
151
+ .getVisibleCells()
152
+ .filter((cell) => cell.column.id === activeId)
153
+ .map((cell) => (
154
+ <TableRowCell
155
+ key={cell.id}
156
+ cell={cell}
157
+ isLastRow={row.id === tableRows[tableRows.length - 1].id}
158
+ isFirstCell={true}
159
+ isLastCell={true}
160
+ stickyColumn={false}
161
+ isScrolled={false}
162
+ showBorder={false}
163
+ enableColumnDrag={true}
164
+ />
165
+ ))}
166
+ </TableRow>
167
+ ))}
168
+ </TableBody>
169
+ </table>
170
+ {/* Live region for drag status updates (outside aria-hidden scope) */}
171
+ <span
172
+ aria-live="polite"
173
+ style={{
174
+ position: "absolute",
175
+ width: 1,
176
+ height: 1,
177
+ margin: -1,
178
+ padding: 0,
179
+ border: 0,
180
+ clip: "rect(0 0 0 0)",
181
+ overflow: "hidden",
182
+ }}
183
+ >
184
+ {activeId ? columnDragAriaLabelsCopy.grabbing : ""}
185
+ </span>
186
+ </div>
187
+ ) : null}
188
+ </DragOverlay>
189
+ </DndContext>
190
+ );
191
+ };
@@ -0,0 +1,54 @@
1
+ import React from "react";
2
+ import { Heading, type HeadingTagType } from "@purpurds/heading";
3
+ import { Paragraph } from "@purpurds/paragraph";
4
+ import c from "classnames/bind";
5
+
6
+ import styles from "./table.module.scss";
7
+ import { TableRow } from "./table-row";
8
+ import { TableRowCell } from "./table-row-cell";
9
+
10
+ const cx = c.bind(styles);
11
+ const rootClassName = "purpur-table";
12
+
13
+ type EmptyTableProps = {
14
+ variant: "primary" | "secondary";
15
+ tag: HeadingTagType;
16
+ title: string;
17
+ description: string;
18
+ colSpan: number;
19
+ icon: React.ReactNode;
20
+ };
21
+
22
+ export const EmptyTable = ({
23
+ variant,
24
+ tag,
25
+ title,
26
+ description,
27
+ colSpan,
28
+ icon,
29
+ }: EmptyTableProps) => (
30
+ <TableRow>
31
+ <TableRowCell
32
+ colSpan={colSpan}
33
+ isLastRow={true}
34
+ isFirstCell={true}
35
+ isLastCell={true}
36
+ enableColumnDrag={false}
37
+ >
38
+ <div
39
+ className={cx([
40
+ `${rootClassName}__empty-section`,
41
+ `${rootClassName}__empty-section--${variant}`,
42
+ ])}
43
+ >
44
+ {icon && <div className={cx(`${rootClassName}__empty-section__icon`)}>{icon}</div>}
45
+ <div className={cx(`${rootClassName}__empty-section__texts`)}>
46
+ <Heading data-testid="purpur-table-empty-table-title" variant="title-100" tag={tag}>
47
+ {title}
48
+ </Heading>
49
+ <Paragraph data-testid="purpur-table-empty-table-description">{description}</Paragraph>
50
+ </div>
51
+ </div>
52
+ </TableRowCell>
53
+ </TableRow>
54
+ );
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+
3
+ import { TableRow } from "./table-row";
4
+ import { TableRowCellSkeleton } from "./table-row-cell-skeleton";
5
+
6
+ type LoadingTableRowsProps = {
7
+ rowCount: number;
8
+ isScrolled: boolean;
9
+ stickyFirstColumn: boolean;
10
+ getStickyColumn: (index: number) => boolean;
11
+ cellWidths: (string | number)[];
12
+ showBorder: (index: number) => boolean;
13
+ };
14
+
15
+ export const LoadingTableRows = ({
16
+ rowCount,
17
+ getStickyColumn,
18
+ stickyFirstColumn,
19
+ isScrolled,
20
+ cellWidths,
21
+ showBorder,
22
+ }: LoadingTableRowsProps) => (
23
+ <>
24
+ {Array.from({ length: rowCount }, (_value, index) => index).map((row, rowIndex) => (
25
+ <TableRow key={`skeleton-row-${row}`}>
26
+ {cellWidths.map((cellWidth, cellIndex) => (
27
+ <TableRowCellSkeleton
28
+ key={`skeleton-cell-${cellIndex}`}
29
+ isLastRow={rowIndex === rowCount - 1}
30
+ isFirstCell={cellIndex === 0}
31
+ isLastCell={cellIndex === cellWidths.length - 1}
32
+ stickyColumn={stickyFirstColumn && getStickyColumn(cellIndex)}
33
+ isScrolled={isScrolled}
34
+ showBorder={showBorder(cellIndex)}
35
+ cellWidth={cellWidth}
36
+ />
37
+ ))}
38
+ </TableRow>
39
+ ))}
40
+ </>
41
+ );
@@ -12,7 +12,7 @@ type TableBodyProps = {
12
12
 
13
13
  const rootClassName = "purpur-table-body";
14
14
 
15
- const TableBody = ({ children, className, ...props }: TableBodyProps) => {
15
+ export const TableBody = ({ children, className, ...props }: TableBodyProps) => {
16
16
  const classes = cx(className, rootClassName);
17
17
 
18
18
  return (
@@ -21,5 +21,3 @@ const TableBody = ({ children, className, ...props }: TableBodyProps) => {
21
21
  </tbody>
22
22
  );
23
23
  };
24
-
25
- export default TableBody;