@purpurds/table 8.6.0 → 8.8.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.
@@ -9,137 +9,53 @@ import { EmptyTable } from "./empty-table";
9
9
  import { LoadingTableRows } from "./loading-table-rows";
10
10
  import styles from "./table.module.scss";
11
11
  import { TableBody } from "./table-body";
12
- import { TableHeader } from "./table-header";
13
12
  import { TableRow } from "./table-row";
14
13
  import { TableRowCell } from "./table-row-cell";
15
14
 
16
15
  const cx = c.bind(styles);
17
16
  const rootClassName = "purpur-table";
18
17
 
19
- type BaseTableContentProps<TData extends RowData> = {
18
+ type TableContentProps<TData extends RowData> = {
20
19
  tanstackTable: Table<TData>;
21
20
  tableRows: Row<TData>[];
22
- showColumnFiltersEnabled: boolean;
23
21
  fullWidth: boolean;
24
- renderTableHeaders: () => React.ReactNode;
25
- };
26
-
27
- type NormalTableContentProps<TData extends RowData> = BaseTableContentProps<TData> & {
28
- stickyFirstColumn: boolean;
29
- getStickyColumn: (index: number) => boolean;
30
- isScrolled: boolean;
31
- showBorder: (index: number) => boolean;
32
- enableColumnDrag: boolean;
33
- activeId: UniqueIdentifier | null;
22
+ tableHeader: React.ReactNode;
23
+ loading?: boolean;
24
+ skeletonRows?: number;
25
+ isEmptyTable?: boolean;
26
+ stickyFirstColumn?: boolean;
27
+ getStickyColumn?: (index: number) => boolean;
28
+ isScrolled?: boolean;
29
+ showBorder?: (index: number) => boolean;
30
+ enableColumnDrag?: boolean;
31
+ activeId?: UniqueIdentifier | null;
32
+ getColumnWidths?: () => (string | number)[];
33
+ variant?: "primary" | "secondary";
34
+ emptyTableHeadingTag?: HeadingTagType;
35
+ emptyTableCopy?: { title: string; description: string };
36
+ emptyTableIcon?: React.ReactNode;
34
37
  };
35
38
 
36
- export function NormalTableContent<TData extends RowData>({
39
+ export function TableContent<TData extends RowData>({
40
+ tanstackTable,
37
41
  tableRows,
38
- showColumnFiltersEnabled,
39
42
  fullWidth,
40
- renderTableHeaders,
43
+ tableHeader,
44
+ loading,
45
+ skeletonRows,
46
+ isEmptyTable,
41
47
  stickyFirstColumn,
42
48
  getStickyColumn,
43
49
  isScrolled,
44
50
  showBorder,
45
51
  enableColumnDrag,
46
52
  activeId,
47
- }: NormalTableContentProps<TData>) {
48
- return (
49
- <table
50
- className={cx([
51
- `${rootClassName}__table`,
52
- { [`${rootClassName}__table--full-width`]: fullWidth },
53
- ])}
54
- >
55
- <TableHeader columnFiltersEnabled={showColumnFiltersEnabled}>
56
- {renderTableHeaders()}
57
- </TableHeader>
58
- <TableBody>
59
- {tableRows.map((row, rowIndex) => (
60
- <TableRow key={row.id} isSelected={row.getIsSelected()}>
61
- {row.getVisibleCells().map((cell, cellIndex) => (
62
- <TableRowCell
63
- key={cell.id}
64
- cell={cell}
65
- isLastRow={rowIndex === tableRows.length - 1}
66
- isFirstCell={cellIndex === 0}
67
- isLastCell={cellIndex === row.getVisibleCells().length - 1}
68
- stickyColumn={stickyFirstColumn && getStickyColumn(cellIndex)}
69
- isScrolled={isScrolled}
70
- showBorder={showBorder(cellIndex)}
71
- enableColumnDrag={enableColumnDrag || false}
72
- draggingActive={activeId === cell.column.id}
73
- />
74
- ))}
75
- </TableRow>
76
- ))}
77
- </TableBody>
78
- </table>
79
- );
80
- }
81
-
82
- type LoadingTableContentProps<TData extends RowData> = BaseTableContentProps<TData> & {
83
- skeletonRows: number;
84
- getStickyColumn: (index: number) => boolean;
85
- stickyFirstColumn: boolean;
86
- isScrolled: boolean;
87
- showBorder: (index: number) => boolean;
88
- getColumnWidths: () => (string | number)[];
89
- };
90
-
91
- export function LoadingTableContent<TData extends RowData>({
92
- showColumnFiltersEnabled,
93
- fullWidth,
94
- renderTableHeaders,
95
- skeletonRows,
96
- getStickyColumn,
97
- stickyFirstColumn,
98
- isScrolled,
99
- showBorder,
100
53
  getColumnWidths,
101
- }: LoadingTableContentProps<TData>) {
102
- return (
103
- <table
104
- className={cx([
105
- `${rootClassName}__table`,
106
- { [`${rootClassName}__table--full-width`]: fullWidth },
107
- ])}
108
- >
109
- <TableHeader columnFiltersEnabled={showColumnFiltersEnabled}>
110
- {renderTableHeaders()}
111
- </TableHeader>
112
- <TableBody>
113
- <LoadingTableRows
114
- rowCount={skeletonRows}
115
- getStickyColumn={getStickyColumn}
116
- stickyFirstColumn={stickyFirstColumn}
117
- isScrolled={isScrolled}
118
- cellWidths={getColumnWidths()}
119
- showBorder={showBorder}
120
- />
121
- </TableBody>
122
- </table>
123
- );
124
- }
125
-
126
- type EmptyTableContentProps<TData extends RowData> = BaseTableContentProps<TData> & {
127
- variant: "primary" | "secondary";
128
- emptyTableHeadingTag: HeadingTagType;
129
- emptyTableCopy: { title: string; description: string };
130
- emptyTableIcon?: React.ReactNode;
131
- };
132
-
133
- export function EmptyTableContent<TData extends RowData>({
134
- tanstackTable,
135
- showColumnFiltersEnabled,
136
- fullWidth,
137
- renderTableHeaders,
138
54
  variant,
139
55
  emptyTableHeadingTag,
140
56
  emptyTableCopy,
141
57
  emptyTableIcon,
142
- }: EmptyTableContentProps<TData>) {
58
+ }: TableContentProps<TData>) {
143
59
  return (
144
60
  <table
145
61
  className={cx([
@@ -147,18 +63,46 @@ export function EmptyTableContent<TData extends RowData>({
147
63
  { [`${rootClassName}__table--full-width`]: fullWidth },
148
64
  ])}
149
65
  >
150
- <TableHeader columnFiltersEnabled={showColumnFiltersEnabled}>
151
- {renderTableHeaders()}
152
- </TableHeader>
66
+ {tableHeader}
153
67
  <TableBody>
154
- <EmptyTable
155
- variant={variant}
156
- tag={emptyTableHeadingTag}
157
- title={emptyTableCopy.title}
158
- description={emptyTableCopy.description}
159
- colSpan={tanstackTable.getVisibleLeafColumns().length}
160
- icon={emptyTableIcon}
161
- />
68
+ {loading && skeletonRows ? (
69
+ <LoadingTableRows
70
+ rowCount={skeletonRows}
71
+ getStickyColumn={getStickyColumn!}
72
+ stickyFirstColumn={stickyFirstColumn!}
73
+ isScrolled={isScrolled!}
74
+ cellWidths={getColumnWidths!()}
75
+ showBorder={showBorder!}
76
+ />
77
+ ) : isEmptyTable && emptyTableCopy && emptyTableHeadingTag ? (
78
+ <EmptyTable
79
+ variant={variant!}
80
+ tag={emptyTableHeadingTag}
81
+ title={emptyTableCopy.title}
82
+ description={emptyTableCopy.description}
83
+ colSpan={tanstackTable.getVisibleLeafColumns().length}
84
+ icon={emptyTableIcon}
85
+ />
86
+ ) : (
87
+ tableRows.map((row, rowIndex) => (
88
+ <TableRow key={row.id} isSelected={row.getIsSelected()}>
89
+ {row.getVisibleCells().map((cell, cellIndex) => (
90
+ <TableRowCell
91
+ key={cell.id}
92
+ cell={cell}
93
+ isLastRow={rowIndex === tableRows.length - 1}
94
+ isFirstCell={cellIndex === 0}
95
+ isLastCell={cellIndex === row.getVisibleCells().length - 1}
96
+ stickyColumn={stickyFirstColumn && getStickyColumn!(cellIndex)}
97
+ isScrolled={isScrolled!}
98
+ showBorder={showBorder!(cellIndex)}
99
+ enableColumnDrag={enableColumnDrag || false}
100
+ draggingActive={activeId === cell.column.id}
101
+ />
102
+ ))}
103
+ </TableRow>
104
+ ))
105
+ )}
162
106
  </TableBody>
163
107
  </table>
164
108
  );
@@ -489,7 +489,7 @@ const DraggableColumnItem = ({
489
489
  >
490
490
  <IconDragVertical className={cx(`${rootClassName}__draggable-handle-icon`)} size="sm" />
491
491
  </div>
492
- <span>{label}</span>
492
+ <Paragraph>{label}</Paragraph>
493
493
  <VisuallyHidden id={`drag-instructions-${id}`}>
494
494
  {copy.visibleColumns.ariaLabels.dragHandle.instructions}
495
495
  </VisuallyHidden>
@@ -403,6 +403,14 @@ manages filtering, sorting and pagination internally.
403
403
  stepNumberPrefix: "Go to page",
404
404
  };
405
405
 
406
+ const emptyTableProps = {
407
+ emptyTableCopy: {
408
+ title: args.emptyTableCopy?.title || "No data found",
409
+ description: args.emptyTableCopy?.description || "There are no items to display.",
410
+ },
411
+ emptyTableHeadingTag: args.emptyTableHeadingTag || "h2",
412
+ };
413
+
406
414
  return renderTableContainer(
407
415
  { ...args, isExpanded: tableExpanded },
408
416
  /* @ts-expect-error Props are incompatible. Storybook issue after adding argTypes. */
@@ -446,6 +454,9 @@ manages filtering, sorting and pagination internally.
446
454
  stickyHeaders={args.stickyHeaders}
447
455
  enableColumnDrag={args.enableColumnDrag}
448
456
  columnDragAriaLabelsCopy={commonColumnDragAriaLabels}
457
+ emptyTableCopy={emptyTableProps.emptyTableCopy}
458
+ emptyTableHeadingTag={emptyTableProps.emptyTableHeadingTag}
459
+ emptyTableIcon={<IllustrativeIconTableQuestionDuocolorStatic />}
449
460
  />
450
461
  );
451
462
  },
package/src/table.tsx CHANGED
@@ -38,8 +38,9 @@ export type {
38
38
 
39
39
  import styles from "./table.module.scss";
40
40
  import { TableActionBar } from "./table-action-bar";
41
- import { EmptyTableContent, LoadingTableContent, NormalTableContent } from "./table-content";
41
+ import { TableContent } from "./table-content";
42
42
  import { TableExportDrawer } from "./table-export-drawer";
43
+ import { TableHeader } from "./table-header";
43
44
  import { SortableTableHeaders, StandardTableHeaders } from "./table-headers";
44
45
  import { TableSettingsDrawer, type TableSettingsDrawerCopyProps } from "./table-settings-drawer";
45
46
  import { TableToolbar } from "./table-toolbar";
@@ -140,12 +141,12 @@ export const Table = <TData extends RowData>({
140
141
  const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
141
142
 
142
143
  const classes = cx([
143
- className,
144
144
  rootClassName,
145
145
  {
146
146
  [`${rootClassName}--${variant}`]: variant,
147
147
  [`${rootClassName}--without-toolbar`]: !enableToolbar || !settingsDrawerCopy,
148
148
  },
149
+ className,
149
150
  ]);
150
151
 
151
152
  // Only add row selection columns when enableRowSelection is true
@@ -204,7 +205,7 @@ export const Table = <TData extends RowData>({
204
205
  ...restProps,
205
206
  });
206
207
 
207
- const rowCount = useMemo(() => tanstackTable.getRowCount(), [tanstackTable]);
208
+ const rowCount = tanstackTable.getRowCount();
208
209
 
209
210
  useEffect(() => {
210
211
  if (onRowsCountChange) {
@@ -315,141 +316,101 @@ export const Table = <TData extends RowData>({
315
316
  const tableRows = tanstackTable.getRowModel().rows;
316
317
  const emptyTable = tableRows.length === 0 && Boolean(emptyTableCopy);
317
318
 
318
- const memoizedGetColumnWidths = React.useCallback(() => {
319
- return tanstackTable.getAllColumns().map((column) => column.getSize() || "100%");
320
- }, [tanstackTable]);
321
-
322
- // Render table headers with or without drag functionality
323
- const renderTableHeaders = React.useCallback(() => {
324
- return tanstackTable.getHeaderGroups().map((headerGroup) => {
325
- if (enableColumnDrag && state?.columnOrder) {
326
- return (
327
- <SortableTableHeaders
328
- key={headerGroup.id}
329
- headerGroup={headerGroup}
330
- tanstackTable={tanstackTable}
331
- tableHasFilters={showColumnFiltersEnabled}
332
- emptyTable={emptyTable}
333
- stickyFirstColumn={stickyFirstColumn}
334
- stickyHeaders={stickyHeaders}
335
- isScrolled={isScrolled}
336
- getStickyColumn={getStickyColumn}
337
- showBorder={showBorder}
338
- enableSorting={props.enableSorting || false}
339
- sortingAriaLabels={sortingAriaLabels}
340
- columnOrder={state.columnOrder}
341
- activeId={activeId}
342
- enableColumnDrag={enableColumnDrag}
343
- columnDragAriaLabelsCopy={columnDragAriaLabelsCopy}
344
- />
345
- );
346
- }
347
-
348
- return (
349
- <StandardTableHeaders
350
- key={headerGroup.id}
351
- headerGroup={headerGroup}
352
- tanstackTable={tanstackTable}
353
- tableHasFilters={showColumnFiltersEnabled}
354
- emptyTable={emptyTable}
355
- stickyFirstColumn={stickyFirstColumn}
356
- stickyHeaders={stickyHeaders}
357
- isScrolled={isScrolled}
358
- getStickyColumn={getStickyColumn}
359
- showBorder={showBorder}
360
- enableSorting={props.enableSorting || false}
361
- sortingAriaLabels={sortingAriaLabels}
362
- />
363
- );
364
- });
365
- }, [
366
- activeId,
367
- columnDragAriaLabelsCopy,
368
- emptyTable,
369
- enableColumnDrag,
370
- getStickyColumn,
371
- isScrolled,
372
- props.enableSorting,
373
- showBorder,
374
- showColumnFiltersEnabled,
375
- sortingAriaLabels,
376
- state?.columnOrder,
377
- stickyFirstColumn,
378
- stickyHeaders,
379
- tanstackTable,
380
- ]);
381
-
382
- const renderTableContent = React.useCallback(() => {
383
- if (loading && skeletonRows) {
384
- return (
385
- <LoadingTableContent
386
- tanstackTable={tanstackTable}
387
- tableRows={tableRows}
388
- showColumnFiltersEnabled={showColumnFiltersEnabled}
389
- fullWidth={fullWidth}
390
- renderTableHeaders={renderTableHeaders}
391
- skeletonRows={skeletonRows}
392
- getStickyColumn={getStickyColumn}
393
- stickyFirstColumn={stickyFirstColumn}
394
- isScrolled={isScrolled}
395
- showBorder={showBorder}
396
- getColumnWidths={memoizedGetColumnWidths}
397
- />
398
- );
399
- }
400
-
401
- const isEmptyTable = tableRows.length === 0 && Boolean(emptyTableCopy);
402
- if (isEmptyTable && emptyTableCopy && emptyTableHeadingTag) {
403
- return (
404
- <EmptyTableContent
405
- tanstackTable={tanstackTable}
406
- tableRows={tableRows}
407
- showColumnFiltersEnabled={showColumnFiltersEnabled}
408
- fullWidth={fullWidth}
409
- renderTableHeaders={renderTableHeaders}
410
- variant={variant}
411
- emptyTableHeadingTag={emptyTableHeadingTag}
412
- emptyTableCopy={emptyTableCopy}
413
- emptyTableIcon={emptyTableIcon}
414
- />
415
- );
416
- }
319
+ // Extract header groups to avoid complex expression in dependency array
320
+ const headerGroups = tanstackTable.getHeaderGroups();
321
+
322
+ // Memoized table header to prevent re-renders and maintain filter focus
323
+ const tableHeader = React.useMemo(
324
+ () => (
325
+ <TableHeader columnFiltersEnabled={showColumnFiltersEnabled}>
326
+ {headerGroups.map((headerGroup) => {
327
+ if (enableColumnDrag && state?.columnOrder) {
328
+ return (
329
+ <SortableTableHeaders
330
+ key={headerGroup.id}
331
+ headerGroup={headerGroup}
332
+ tanstackTable={tanstackTable}
333
+ tableHasFilters={showColumnFiltersEnabled}
334
+ emptyTable={emptyTable}
335
+ stickyFirstColumn={stickyFirstColumn}
336
+ stickyHeaders={stickyHeaders}
337
+ isScrolled={isScrolled}
338
+ getStickyColumn={getStickyColumn}
339
+ showBorder={showBorder}
340
+ enableSorting={props.enableSorting || false}
341
+ sortingAriaLabels={sortingAriaLabels}
342
+ columnOrder={state.columnOrder}
343
+ activeId={activeId}
344
+ enableColumnDrag={enableColumnDrag}
345
+ columnDragAriaLabelsCopy={columnDragAriaLabelsCopy}
346
+ />
347
+ );
348
+ }
349
+
350
+ return (
351
+ <StandardTableHeaders
352
+ key={headerGroup.id}
353
+ headerGroup={headerGroup}
354
+ tanstackTable={tanstackTable}
355
+ tableHasFilters={showColumnFiltersEnabled}
356
+ emptyTable={emptyTable}
357
+ stickyFirstColumn={stickyFirstColumn}
358
+ stickyHeaders={stickyHeaders}
359
+ isScrolled={isScrolled}
360
+ getStickyColumn={getStickyColumn}
361
+ showBorder={showBorder}
362
+ enableSorting={props.enableSorting || false}
363
+ sortingAriaLabels={sortingAriaLabels}
364
+ />
365
+ );
366
+ })}
367
+ </TableHeader>
368
+ ),
369
+ [
370
+ showColumnFiltersEnabled,
371
+ headerGroups,
372
+ enableColumnDrag,
373
+ state?.columnOrder,
374
+ props.enableSorting,
375
+ sortingAriaLabels,
376
+ columnDragAriaLabelsCopy,
377
+ tanstackTable,
378
+ emptyTable,
379
+ stickyFirstColumn,
380
+ stickyHeaders,
381
+ isScrolled,
382
+ getStickyColumn,
383
+ showBorder,
384
+ activeId,
385
+ ]
386
+ );
417
387
 
418
- return (
419
- <NormalTableContent
420
- tanstackTable={tanstackTable}
421
- tableRows={tableRows}
422
- showColumnFiltersEnabled={showColumnFiltersEnabled}
423
- fullWidth={fullWidth}
424
- renderTableHeaders={renderTableHeaders}
425
- stickyFirstColumn={stickyFirstColumn}
426
- getStickyColumn={getStickyColumn}
427
- isScrolled={isScrolled}
428
- showBorder={showBorder}
429
- enableColumnDrag={enableColumnDrag || false}
430
- activeId={activeId}
431
- />
432
- );
433
- }, [
434
- activeId,
435
- emptyTableCopy,
436
- emptyTableHeadingTag,
437
- emptyTableIcon,
438
- enableColumnDrag,
439
- fullWidth,
440
- getStickyColumn,
441
- isScrolled,
442
- loading,
443
- memoizedGetColumnWidths,
444
- renderTableHeaders,
445
- showBorder,
446
- showColumnFiltersEnabled,
447
- skeletonRows,
448
- stickyFirstColumn,
449
- tableRows,
450
- tanstackTable,
451
- variant,
452
- ]);
388
+ const isEmptyTable = tableRows.length === 0 && Boolean(emptyTableCopy);
389
+
390
+ const tableContent = (
391
+ <TableContent
392
+ tanstackTable={tanstackTable}
393
+ tableRows={tableRows}
394
+ fullWidth={fullWidth}
395
+ tableHeader={tableHeader}
396
+ loading={loading}
397
+ skeletonRows={skeletonRows}
398
+ isEmptyTable={isEmptyTable}
399
+ stickyFirstColumn={stickyFirstColumn}
400
+ getStickyColumn={getStickyColumn}
401
+ isScrolled={isScrolled}
402
+ showBorder={showBorder}
403
+ enableColumnDrag={enableColumnDrag}
404
+ activeId={activeId}
405
+ getColumnWidths={() =>
406
+ tanstackTable.getVisibleLeafColumns().map((column) => column.getSize() || "100%")
407
+ }
408
+ variant={variant}
409
+ emptyTableHeadingTag={emptyTableHeadingTag}
410
+ emptyTableCopy={emptyTableCopy}
411
+ emptyTableIcon={emptyTableIcon}
412
+ />
413
+ );
453
414
 
454
415
  return (
455
416
  <div id={`${uid}-table`} className={classes}>
@@ -491,10 +452,10 @@ export const Table = <TData extends RowData>({
491
452
  columnDragAriaLabelsCopy={columnDragAriaLabelsCopy}
492
453
  rootClassName={rootClassName}
493
454
  >
494
- {renderTableContent()}
455
+ {tableContent}
495
456
  </DraggableTable>
496
457
  ) : (
497
- renderTableContent()
458
+ tableContent
498
459
  )}
499
460
  </div>
500
461
  {paginationComponent}
@@ -1,4 +1,4 @@
1
- import React, { useState } from "react";
1
+ import React from "react";
2
2
  import { IconDragHorizontal } from "@purpurds/icon/drag-horizontal";
3
3
  import c from "classnames/bind";
4
4
 
@@ -7,52 +7,33 @@ import styles from "./table.module.scss";
7
7
  const cx = c.bind(styles);
8
8
  const rootClassName = "purpur-table-column-header-cell";
9
9
 
10
- export function useDragHandle() {
11
- const [mouseDownActive, setMouseDownActive] = useState(false);
12
-
13
- const handleMouseDown = () => {
14
- setMouseDownActive(true);
15
- window.addEventListener("mouseup", handleMouseUp, { once: true });
16
- };
17
-
18
- const handleMouseUp = () => {
19
- setMouseDownActive(false);
20
- };
21
-
22
- return { mouseDownActive, handleMouseDown };
23
- }
24
-
25
10
  export type TableColumnDragHandleProps = {
26
- onMouseDown: () => void;
27
11
  overlayActive?: boolean;
28
12
  isFirstColumn: boolean;
29
13
  isLastColumn: boolean;
30
14
  columnDragAriaLabel: string;
15
+ // Add any additional props from listeners
16
+ [key: string]: unknown;
31
17
  };
32
18
 
33
19
  export function TableColumnDragHandle({
34
- onMouseDown,
35
20
  overlayActive,
36
21
  isFirstColumn,
37
22
  isLastColumn,
38
23
  columnDragAriaLabel,
24
+ ...dragListeners // Capture the listeners from useSortable
39
25
  }: TableColumnDragHandleProps) {
40
26
  return (
41
27
  <div
28
+ role="button"
29
+ tabIndex={0}
42
30
  className={cx(`${rootClassName}__drag-handle`, {
43
31
  [`${rootClassName}__border-radius-first-cell`]: isFirstColumn && !overlayActive,
44
32
  [`${rootClassName}__border-radius-last-cell`]: isLastColumn && !overlayActive,
45
33
  [`${rootClassName}__drag-handle--active`]: overlayActive,
46
34
  })}
47
- role="button"
48
- tabIndex={0}
49
35
  aria-label={columnDragAriaLabel}
50
- onMouseDown={onMouseDown}
51
- onKeyDown={(e) => {
52
- if (e.key === "Enter" || e.key === " ") {
53
- onMouseDown();
54
- }
55
- }}
36
+ {...dragListeners}
56
37
  >
57
38
  <IconDragHorizontal className={cx(`${rootClassName}__drag-handle-icon`)} size="sm" />
58
39
  </div>