@purpurds/table 0.0.1

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 (142) hide show
  1. package/dist/LICENSE.txt +213 -0
  2. package/dist/cell-types/badge-cell.d.ts +8 -0
  3. package/dist/cell-types/badge-cell.d.ts.map +1 -0
  4. package/dist/cell-types/body-text-cell.d.ts +8 -0
  5. package/dist/cell-types/body-text-cell.d.ts.map +1 -0
  6. package/dist/cell-types/button-cell.d.ts +8 -0
  7. package/dist/cell-types/button-cell.d.ts.map +1 -0
  8. package/dist/cell-types/button-group-cell.d.ts +16 -0
  9. package/dist/cell-types/button-group-cell.d.ts.map +1 -0
  10. package/dist/cell-types/cta-link-cell.d.ts +8 -0
  11. package/dist/cell-types/cta-link-cell.d.ts.map +1 -0
  12. package/dist/cell-types/date-cell.d.ts +8 -0
  13. package/dist/cell-types/date-cell.d.ts.map +1 -0
  14. package/dist/cell-types/empty-cell.d.ts +4 -0
  15. package/dist/cell-types/empty-cell.d.ts.map +1 -0
  16. package/dist/cell-types/error-message-cell.d.ts +8 -0
  17. package/dist/cell-types/error-message-cell.d.ts.map +1 -0
  18. package/dist/cell-types/icon-text-cell.d.ts +8 -0
  19. package/dist/cell-types/icon-text-cell.d.ts.map +1 -0
  20. package/dist/cell-types/lead-text-cell.d.ts +8 -0
  21. package/dist/cell-types/lead-text-cell.d.ts.map +1 -0
  22. package/dist/cell-types/link-cell.d.ts +8 -0
  23. package/dist/cell-types/link-cell.d.ts.map +1 -0
  24. package/dist/cell-types/number-cell.d.ts +8 -0
  25. package/dist/cell-types/number-cell.d.ts.map +1 -0
  26. package/dist/cell-types/row-selection-cell.d.ts +8 -0
  27. package/dist/cell-types/row-selection-cell.d.ts.map +1 -0
  28. package/dist/cell-types/row-toggle-cell.d.ts +8 -0
  29. package/dist/cell-types/row-toggle-cell.d.ts.map +1 -0
  30. package/dist/cell-types/toggle-cell.d.ts +8 -0
  31. package/dist/cell-types/toggle-cell.d.ts.map +1 -0
  32. package/dist/cell-types/warning-message-cell.d.ts +8 -0
  33. package/dist/cell-types/warning-message-cell.d.ts.map +1 -0
  34. package/dist/metadata.js +17 -0
  35. package/dist/story-utils/column-def.d.ts +5 -0
  36. package/dist/story-utils/column-def.d.ts.map +1 -0
  37. package/dist/story-utils/table-data.d.ts +35 -0
  38. package/dist/story-utils/table-data.d.ts.map +1 -0
  39. package/dist/story-utils/use-fetch-table-data-hook.d.ts +11 -0
  40. package/dist/story-utils/use-fetch-table-data-hook.d.ts.map +1 -0
  41. package/dist/styles.css +1 -0
  42. package/dist/table-action-bar.d.ts +26 -0
  43. package/dist/table-action-bar.d.ts.map +1 -0
  44. package/dist/table-body.d.ts +10 -0
  45. package/dist/table-body.d.ts.map +1 -0
  46. package/dist/table-column-header-cell.d.ts +28 -0
  47. package/dist/table-column-header-cell.d.ts.map +1 -0
  48. package/dist/table-export-drawer.d.ts +17 -0
  49. package/dist/table-export-drawer.d.ts.map +1 -0
  50. package/dist/table-header.d.ts +11 -0
  51. package/dist/table-header.d.ts.map +1 -0
  52. package/dist/table-row-cell-skeleton.d.ts +14 -0
  53. package/dist/table-row-cell-skeleton.d.ts.map +1 -0
  54. package/dist/table-row-cell.d.ts +25 -0
  55. package/dist/table-row-cell.d.ts.map +1 -0
  56. package/dist/table-row.d.ts +11 -0
  57. package/dist/table-row.d.ts.map +1 -0
  58. package/dist/table-settings-drawer.d.ts +41 -0
  59. package/dist/table-settings-drawer.d.ts.map +1 -0
  60. package/dist/table-toolbar.d.ts +37 -0
  61. package/dist/table-toolbar.d.ts.map +1 -0
  62. package/dist/table.cjs.js +259 -0
  63. package/dist/table.cjs.js.map +1 -0
  64. package/dist/table.d.ts +20 -0
  65. package/dist/table.d.ts.map +1 -0
  66. package/dist/table.es.js +13585 -0
  67. package/dist/table.es.js.map +1 -0
  68. package/dist/test-utils/column-def.d.ts +6 -0
  69. package/dist/test-utils/column-def.d.ts.map +1 -0
  70. package/dist/test-utils/helpers.d.ts +138 -0
  71. package/dist/test-utils/helpers.d.ts.map +1 -0
  72. package/dist/test-utils/table-data.d.ts +33 -0
  73. package/dist/test-utils/table-data.d.ts.map +1 -0
  74. package/dist/types.d.ts +420 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/use-screen-size.hook.d.ts +7 -0
  77. package/dist/use-screen-size.hook.d.ts.map +1 -0
  78. package/dist/use-truncated-hook.d.ts +10 -0
  79. package/dist/use-truncated-hook.d.ts.map +1 -0
  80. package/dist/utils/custom-functions.d.ts +9 -0
  81. package/dist/utils/custom-functions.d.ts.map +1 -0
  82. package/dist/utils/unit-conversions.d.ts +19 -0
  83. package/dist/utils/unit-conversions.d.ts.map +1 -0
  84. package/dist/utils/unit-conversions.spec.d.ts +2 -0
  85. package/dist/utils/unit-conversions.spec.d.ts.map +1 -0
  86. package/eslint.config.mjs +2 -0
  87. package/package.json +82 -0
  88. package/src/cell-types/badge-cell.tsx +25 -0
  89. package/src/cell-types/body-text-cell.tsx +54 -0
  90. package/src/cell-types/button-cell.tsx +26 -0
  91. package/src/cell-types/button-group-cell.tsx +54 -0
  92. package/src/cell-types/cta-link-cell.tsx +25 -0
  93. package/src/cell-types/date-cell.tsx +33 -0
  94. package/src/cell-types/empty-cell.tsx +6 -0
  95. package/src/cell-types/error-message-cell.tsx +30 -0
  96. package/src/cell-types/icon-text-cell.tsx +30 -0
  97. package/src/cell-types/lead-text-cell.tsx +19 -0
  98. package/src/cell-types/link-cell.tsx +58 -0
  99. package/src/cell-types/number-cell.tsx +27 -0
  100. package/src/cell-types/row-selection-cell.tsx +22 -0
  101. package/src/cell-types/row-toggle-cell.tsx +23 -0
  102. package/src/cell-types/toggle-cell.tsx +19 -0
  103. package/src/cell-types/warning-message-cell.tsx +30 -0
  104. package/src/global.d.ts +4 -0
  105. package/src/story-utils/column-def.ts +148 -0
  106. package/src/story-utils/table-data.tsx +262 -0
  107. package/src/story-utils/use-fetch-table-data-hook.tsx +30 -0
  108. package/src/table-action-bar.module.scss +106 -0
  109. package/src/table-action-bar.test.tsx +111 -0
  110. package/src/table-action-bar.tsx +104 -0
  111. package/src/table-body.tsx +25 -0
  112. package/src/table-column-header-cell.tsx +305 -0
  113. package/src/table-export-drawer.module.scss +9 -0
  114. package/src/table-export-drawer.test.tsx +75 -0
  115. package/src/table-export-drawer.tsx +59 -0
  116. package/src/table-header.tsx +35 -0
  117. package/src/table-kitchen-sink.test.tsx +1196 -0
  118. package/src/table-row-cell-skeleton.tsx +61 -0
  119. package/src/table-row-cell.test.tsx +360 -0
  120. package/src/table-row-cell.tsx +188 -0
  121. package/src/table-row.tsx +30 -0
  122. package/src/table-settings-drawer.module.scss +25 -0
  123. package/src/table-settings-drawer.test.tsx +350 -0
  124. package/src/table-settings-drawer.tsx +254 -0
  125. package/src/table-toolbar.module.scss +17 -0
  126. package/src/table-toolbar.test.tsx +95 -0
  127. package/src/table-toolbar.tsx +136 -0
  128. package/src/table.module.scss +367 -0
  129. package/src/table.stories.tsx +1246 -0
  130. package/src/table.story.css +11 -0
  131. package/src/table.test.tsx +318 -0
  132. package/src/table.tsx +501 -0
  133. package/src/test-utils/column-def.ts +152 -0
  134. package/src/test-utils/helpers.ts +234 -0
  135. package/src/test-utils/table-data.tsx +318 -0
  136. package/src/types.ts +496 -0
  137. package/src/use-screen-size.hook.ts +23 -0
  138. package/src/use-truncated-hook.tsx +74 -0
  139. package/src/utils/custom-functions.ts +52 -0
  140. package/src/utils/unit-conversions.spec.ts +92 -0
  141. package/src/utils/unit-conversions.ts +30 -0
  142. package/vitest.setup.ts +60 -0
@@ -0,0 +1,104 @@
1
+ import React from "react";
2
+ import { Button } from "@purpurds/button";
3
+ import { Paragraph } from "@purpurds/paragraph";
4
+ import { Toggle } from "@purpurds/toggle";
5
+ import c from "classnames/bind";
6
+
7
+ import styles from "./table-action-bar.module.scss";
8
+ import { useScreenSize } from "./use-screen-size.hook";
9
+
10
+ export type TableActionBarCopyProps = {
11
+ buttons: {
12
+ cancel: string;
13
+ primary: string;
14
+ secondary?: string;
15
+ toggleSelected: string;
16
+ };
17
+ selectedRowsCount: {
18
+ of: string;
19
+ selected: string;
20
+ };
21
+ };
22
+
23
+ export type TableActionBarProps = {
24
+ rowCount: number;
25
+ selectedRowsCount: number;
26
+ copy: TableActionBarCopyProps;
27
+ isVisible: boolean;
28
+ onCancelSelection: () => void;
29
+ onToggleSelected: () => void;
30
+ onPrimaryButtonClick: () => void;
31
+ onSecondaryButtonClick?: () => void;
32
+ };
33
+
34
+ const rootClassName = "purpur-table-action-bar";
35
+ const cx = c.bind(styles);
36
+ const rootTestId = "purpur-table-action-bar";
37
+
38
+ export const TableActionBar = ({
39
+ rowCount,
40
+ selectedRowsCount,
41
+ copy,
42
+ isVisible,
43
+ onCancelSelection,
44
+ onToggleSelected,
45
+ onPrimaryButtonClick,
46
+ onSecondaryButtonClick,
47
+ }: TableActionBarProps) => {
48
+ const { isLgOrSmaller } = useScreenSize();
49
+
50
+ return (
51
+ <div
52
+ data-testid={rootTestId}
53
+ className={cx(rootClassName, { [`${rootClassName}--is-visible`]: isVisible })}
54
+ >
55
+ <div className={cx(styles[`${rootClassName}__selected-wrapper`])}>
56
+ <Paragraph data-testid={`${rootTestId}-selected-text`}>
57
+ <span>{selectedRowsCount}</span> {copy.selectedRowsCount.of} {rowCount}{" "}
58
+ {copy.selectedRowsCount.selected}
59
+ </Paragraph>
60
+
61
+ <Toggle
62
+ data-testid={`${rootTestId}-toggle-selected`}
63
+ id="toggle-selected"
64
+ label={copy.buttons.toggleSelected}
65
+ onChange={onToggleSelected}
66
+ />
67
+ </div>
68
+
69
+ <div className={cx(styles[`${rootClassName}__button-group`])}>
70
+ <Button
71
+ data-testid={`${rootTestId}-cancel-button`}
72
+ variant="tertiary-purple"
73
+ fullWidth={isLgOrSmaller}
74
+ onClick={onCancelSelection}
75
+ >
76
+ {copy.buttons.cancel}
77
+ </Button>
78
+
79
+ <div className={cx(styles[`${rootClassName}__primary-secondary-buttons`])}>
80
+ {onSecondaryButtonClick && copy.buttons.secondary && (
81
+ <Button
82
+ data-testid={`${rootTestId}-secondary-button`}
83
+ variant="secondary"
84
+ fullWidth={isLgOrSmaller}
85
+ onClick={onSecondaryButtonClick}
86
+ >
87
+ {copy.buttons.secondary}
88
+ </Button>
89
+ )}
90
+ {onPrimaryButtonClick && copy.buttons.primary && (
91
+ <Button
92
+ data-testid={`${rootTestId}-primary-button`}
93
+ variant="primary"
94
+ fullWidth={isLgOrSmaller}
95
+ onClick={onPrimaryButtonClick}
96
+ >
97
+ {copy.buttons.primary}
98
+ </Button>
99
+ )}
100
+ </div>
101
+ </div>
102
+ </div>
103
+ );
104
+ };
@@ -0,0 +1,25 @@
1
+ import React, { ReactNode } from "react";
2
+ import c from "classnames/bind";
3
+
4
+ import styles from "./table.module.scss";
5
+ const cx = c.bind(styles);
6
+
7
+ type TableBodyProps = {
8
+ ["data-testid"]?: string;
9
+ children: ReactNode;
10
+ className?: string;
11
+ };
12
+
13
+ const rootClassName = "purpur-table-body";
14
+
15
+ const TableBody = ({ children, className, ...props }: TableBodyProps) => {
16
+ const classes = cx([className, rootClassName]);
17
+
18
+ return (
19
+ <tbody className={classes} {...props}>
20
+ {children}
21
+ </tbody>
22
+ );
23
+ };
24
+
25
+ export default TableBody;
@@ -0,0 +1,305 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { Button } from "@purpurds/button";
3
+ import { Checkbox, CheckedState } from "@purpurds/checkbox";
4
+ import { IconArrowDown } from "@purpurds/icon/arrow-down";
5
+ import { IconArrowUp } from "@purpurds/icon/arrow-up";
6
+ import { IconSorter } from "@purpurds/icon/sorter";
7
+ import { Paragraph } from "@purpurds/paragraph";
8
+ import { Select, SelectProps } from "@purpurds/select";
9
+ import { TextField } from "@purpurds/text-field";
10
+ import { VisuallyHidden } from "@purpurds/visually-hidden";
11
+ import { flexRender, Header, RowData, SortDirection, Table } from "@tanstack/react-table";
12
+ import c from "classnames/bind";
13
+
14
+ import styles from "./table.module.scss";
15
+ import { pxToRemString } from "./utils/unit-conversions";
16
+ const cx = c.bind(styles);
17
+
18
+ export type SortingEnabledProps = {
19
+ enableSorting: true;
20
+ sortingAriaLabels: SortingAriaLabels;
21
+ };
22
+
23
+ export type SortingDisabledProps = {
24
+ enableSorting?: false;
25
+ sortingAriaLabels?: never;
26
+ };
27
+
28
+ export type TableColumnHeaderCellProps<TData> = {
29
+ className?: string;
30
+ stickyColumn: boolean;
31
+ stickyHeaders: boolean;
32
+ header: Header<TData, unknown>;
33
+ tanstackTable: Table<TData>;
34
+ isScrolled?: boolean;
35
+ showBorder?: boolean;
36
+ } & (SortingEnabledProps | SortingDisabledProps);
37
+
38
+ export type SortingAriaLabels = {
39
+ desc: string;
40
+ asc: string;
41
+ default: string;
42
+ };
43
+
44
+ export type FilterVariants = "string" | "select";
45
+
46
+ const rootClassName = "purpur-table-column-header-cell";
47
+
48
+ export const TableColumnHeaderCell = <TData extends RowData>({
49
+ className,
50
+ enableSorting,
51
+ header,
52
+ sortingAriaLabels,
53
+ stickyColumn,
54
+ stickyHeaders,
55
+ tanstackTable,
56
+ isScrolled,
57
+ showBorder,
58
+ }: TableColumnHeaderCellProps<TData>) => {
59
+ const canSort = enableSorting && header.column.getCanSort();
60
+ const hasFilter = header.column.getCanFilter();
61
+ const isCheckBox = header.column.columnDef.meta?.cellType === "rowSelection";
62
+ const isRadiobutton = header.column.columnDef.meta?.cellType === "rowToggle";
63
+ const [isVisible, setIsVisible] = useState(false);
64
+ const elementRef = useRef<HTMLTableCellElement>(null);
65
+
66
+ useEffect(() => {
67
+ const currentElement = elementRef.current;
68
+ if (!currentElement) {
69
+ return;
70
+ }
71
+
72
+ const observer = new IntersectionObserver(
73
+ ([entry]) => {
74
+ setIsVisible(entry.isIntersecting);
75
+ },
76
+ { threshold: 1 }
77
+ );
78
+
79
+ observer.observe(currentElement);
80
+
81
+ return () => {
82
+ if (currentElement) {
83
+ observer.unobserve(currentElement);
84
+ }
85
+ };
86
+ }, []);
87
+
88
+ const getSortingDirection = (sortingDirection: SortDirection | false) => {
89
+ switch (sortingDirection) {
90
+ case "desc":
91
+ return "descending";
92
+ case "asc":
93
+ return "ascending";
94
+ case false:
95
+ default:
96
+ return "none";
97
+ }
98
+ };
99
+
100
+ const widthInRemString = pxToRemString(header.getSize());
101
+
102
+ const isFirstColumn = header.column.getIsFirstColumn();
103
+ const isLastColumn = header.column.getIsLastColumn();
104
+
105
+ // If its a string we use it as aria-label else its a custom markup and consumer has to make sure the aria label is set correctly.
106
+ const ariaLabel =
107
+ typeof header.column.columnDef.header === "string"
108
+ ? `${header.column.columnDef.header}`
109
+ : undefined;
110
+
111
+ return (
112
+ <th
113
+ ref={elementRef}
114
+ className={cx([
115
+ className,
116
+ rootClassName,
117
+ {
118
+ [`${rootClassName}__checkbox`]: isCheckBox,
119
+ [`${rootClassName}__sticky-column`]: stickyColumn,
120
+ [`${rootClassName}__sticky-header`]: stickyHeaders,
121
+ [`${rootClassName}__sticky-column__with-sticky-border`]: isScrolled && showBorder,
122
+ [`${rootClassName}__first-header-visible`]: isFirstColumn && isVisible,
123
+ [`${rootClassName}__last-header-visible`]: isLastColumn && isVisible,
124
+ [`${rootClassName}__border-radius-first-cell`]: isFirstColumn,
125
+ [`${rootClassName}__border-radius-last-cell`]: isLastColumn,
126
+ },
127
+ ])}
128
+ style={{
129
+ // If column resizing is enabled Don't use column.getSize() on every header and every data cell. Instead, calculate all column widths once upfront, memoized! https://tanstack.com/table/latest/docs/guide/column-sizing#column-sizing-guide
130
+ maxWidth: widthInRemString,
131
+ minWidth: widthInRemString,
132
+ }}
133
+ scope="col"
134
+ aria-sort={getSortingDirection(header.column.getIsSorted())}
135
+ aria-label={ariaLabel}
136
+ >
137
+ <div className={cx(`${rootClassName}__content`)}>
138
+ <div className={cx(`${rootClassName}__title`)}>
139
+ <HeaderContent
140
+ header={header}
141
+ tanstackTable={tanstackTable}
142
+ canSort={canSort}
143
+ isCheckBox={isCheckBox}
144
+ isRadiobutton={isRadiobutton}
145
+ sortingAriaLabels={sortingAriaLabels}
146
+ />
147
+ </div>
148
+
149
+ {hasFilter && !isCheckBox && (
150
+ <div className={cx(`${rootClassName}__filter-wrapper`)}>
151
+ <Filter header={header} />
152
+ </div>
153
+ )}
154
+ </div>
155
+ </th>
156
+ );
157
+ };
158
+
159
+ type HeaderContentProps<TData> = {
160
+ header: Header<TData, unknown>;
161
+ tanstackTable: Table<TData>;
162
+ canSort: boolean | undefined;
163
+ isCheckBox: boolean;
164
+ isRadiobutton: boolean;
165
+ sortingAriaLabels?: SortingAriaLabels;
166
+ };
167
+
168
+ const HeaderContent = <TData extends RowData>({
169
+ header,
170
+ tanstackTable,
171
+ canSort,
172
+ isCheckBox,
173
+ isRadiobutton,
174
+ sortingAriaLabels,
175
+ }: HeaderContentProps<TData>) => {
176
+ if (isCheckBox) {
177
+ return (
178
+ <HeaderCheckBox
179
+ id={header.id}
180
+ tanstackTable={tanstackTable}
181
+ ariaLabel={header.column.columnDef.meta?.rowSelectionAriaLabels?.header ?? ""}
182
+ />
183
+ );
184
+ }
185
+
186
+ if (isRadiobutton) {
187
+ const ariaLabel = header.column.columnDef.meta?.rowSelectionAriaLabels?.header ?? "";
188
+ return <VisuallyHidden>{ariaLabel}</VisuallyHidden>;
189
+ }
190
+
191
+ if (canSort && sortingAriaLabels) {
192
+ return (
193
+ <Button
194
+ variant="text"
195
+ iconOnly={false}
196
+ size="sm"
197
+ type="button"
198
+ aria-label={getSortingAriaLabel(header.column.getIsSorted(), sortingAriaLabels)}
199
+ onClick={header.column.getToggleSortingHandler()}
200
+ className={cx(`${rootClassName}__sortable-title`)}
201
+ >
202
+ <HeaderParagraph header={header} />
203
+ {getIcon(header.column.getIsSorted())}
204
+ </Button>
205
+ );
206
+ }
207
+
208
+ return <HeaderParagraph header={header} />;
209
+ };
210
+
211
+ type HeaderCheckboxProps<TData> = {
212
+ id: string;
213
+ ariaLabel: string;
214
+ tanstackTable: Table<TData>;
215
+ };
216
+
217
+ const HeaderCheckBox = <TData extends RowData>({
218
+ id,
219
+ ariaLabel,
220
+ tanstackTable,
221
+ }: HeaderCheckboxProps<TData>) => {
222
+ const checked: CheckedState = tanstackTable.getIsSomeRowsSelected()
223
+ ? "indeterminate"
224
+ : tanstackTable.getIsAllPageRowsSelected();
225
+
226
+ return (
227
+ <Checkbox
228
+ id={id}
229
+ onChange={() => tanstackTable.toggleAllPageRowsSelected()}
230
+ checked={checked}
231
+ aria-label={ariaLabel}
232
+ />
233
+ );
234
+ };
235
+
236
+ const HeaderParagraph = <TData extends RowData>({ header }: { header: Header<TData, unknown> }) => (
237
+ <Paragraph data-testid="purpur-table-column-header-cell-title" variant="paragraph-100-bold">
238
+ {flexRender(header.column.columnDef.header, header.getContext())}
239
+ </Paragraph>
240
+ );
241
+
242
+ const getIcon = (sortingDirection: SortDirection | false) => {
243
+ switch (sortingDirection) {
244
+ case "desc":
245
+ return <IconArrowDown size="xs" />;
246
+ case "asc":
247
+ return <IconArrowUp size="xs" />;
248
+ case false:
249
+ default:
250
+ return <IconSorter size="xs" />;
251
+ }
252
+ };
253
+
254
+ const getSortingAriaLabel = (
255
+ sortingDirection: SortDirection | false,
256
+ sortingAriaLabels: SortingAriaLabels
257
+ ) => {
258
+ switch (sortingDirection) {
259
+ case "desc":
260
+ return sortingAriaLabels.desc;
261
+ case "asc":
262
+ return sortingAriaLabels.asc;
263
+ case false:
264
+ default:
265
+ return sortingAriaLabels.default;
266
+ }
267
+ };
268
+
269
+ const Filter = <TData extends RowData>({ header }: { header: Header<TData, unknown> }) => {
270
+ const { filterVariant, filterKey, filterPlaceholder, filterAriaLabel } =
271
+ header.column.columnDef.meta ?? {};
272
+
273
+ if (filterVariant === "string") {
274
+ const columnFilterValue = header.column.getFilterValue() as string;
275
+ return (
276
+ <TextField
277
+ onChange={(event) => header.column.setFilterValue(event.target.value)}
278
+ type="text"
279
+ value={columnFilterValue ?? ""}
280
+ placeholder={filterPlaceholder}
281
+ aria-label={filterAriaLabel}
282
+ />
283
+ );
284
+ }
285
+
286
+ if (filterVariant === "select") {
287
+ const columnFilterValue = header.column.getFilterValue() as {
288
+ filterKey: string;
289
+ value: string;
290
+ };
291
+ const selectProps = header.column.columnDef.meta?.selectProps as SelectProps;
292
+ return (
293
+ <Select
294
+ {...selectProps}
295
+ onChange={(event) => {
296
+ const filter = filterKey ? { filterKey, value: event.target.value } : event.target.value;
297
+ header.column.setFilterValue(filter);
298
+ }}
299
+ value={columnFilterValue?.value ?? ""}
300
+ />
301
+ );
302
+ }
303
+
304
+ return null;
305
+ };
@@ -0,0 +1,9 @@
1
+ .purpur-table-export-drawer {
2
+ &__content {
3
+ display: flex;
4
+ flex-direction: column;
5
+ align-items: flex-start;
6
+ gap: var(--purpur-spacing-100);
7
+ align-self: stretch;
8
+ }
9
+ }
@@ -0,0 +1,75 @@
1
+ import React from "react";
2
+ import { render, screen, within } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { axe } from "vitest-axe";
5
+
6
+ import { TableExportDrawer } from "./table-export-drawer";
7
+ import { copy, Selectors } from "./test-utils/helpers";
8
+
9
+ const onExportDataMock = vi.fn();
10
+ const setDrawerIsOpenMock = vi.fn();
11
+
12
+ describe("Data table - Export drawer", () => {
13
+ let container: HTMLElement;
14
+ beforeEach(() => {
15
+ container = render(
16
+ <TableExportDrawer
17
+ copy={copy.exportDrawer}
18
+ exportFormats={["csv", "xlxs"]}
19
+ isOpen={true}
20
+ onExportData={onExportDataMock}
21
+ setDrawerIsOpen={setDrawerIsOpenMock}
22
+ />
23
+ ).container;
24
+ });
25
+
26
+ it("should have a drawer title", () => {
27
+ const drawerTitle = screen.getByTestId(Selectors.EXPORT_DRAWER.TITLE);
28
+ expect(drawerTitle).toHaveTextContent("Export");
29
+ });
30
+
31
+ it("should have a body text", () => {
32
+ const bodyText = screen.getByTestId(Selectors.EXPORT_DRAWER.BODY_TEXT);
33
+ expect(bodyText).toHaveTextContent("Choose the format you want to export the table in.");
34
+ });
35
+
36
+ it("should have two export buttons", () => {
37
+ const exportButtons = screen.getAllByTestId(Selectors.EXPORT_DRAWER.EXPORT_BUTTON);
38
+ expect(exportButtons).toHaveLength(2);
39
+
40
+ expect(exportButtons[0]).toHaveTextContent("Export as csv");
41
+ expect(exportButtons[1]).toHaveTextContent("Export as xlxs");
42
+ });
43
+
44
+ it("should have a close button", () => {
45
+ const closeButton = within(screen.getByTestId(Selectors.EXPORT_DRAWER.HEADER_ROW)).getByRole(
46
+ "button"
47
+ );
48
+ expect(closeButton).toHaveAttribute("aria-label", "Close drawer");
49
+ });
50
+
51
+ it("should send an 'onClose' event when closeButton clicked", async () => {
52
+ const closeButton = within(screen.getByTestId(Selectors.EXPORT_DRAWER.HEADER_ROW)).getByRole(
53
+ "button"
54
+ );
55
+
56
+ await userEvent.click(closeButton);
57
+
58
+ expect(setDrawerIsOpenMock).toHaveBeenCalled();
59
+ });
60
+
61
+ it("should send an 'onExportData' event when export button clicked", async () => {
62
+ const exportButtons = screen.getAllByTestId(Selectors.EXPORT_DRAWER.EXPORT_BUTTON);
63
+
64
+ await userEvent.click(exportButtons[0]);
65
+
66
+ expect(onExportDataMock).toHaveBeenCalledWith("csv");
67
+ });
68
+
69
+ it("should be accessible", async () => {
70
+ const results = await axe(container);
71
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
72
+ // @ts-ignore
73
+ expect(results).toHaveNoViolations();
74
+ });
75
+ });
@@ -0,0 +1,59 @@
1
+ import React from "react";
2
+ import { Button } from "@purpurds/button";
3
+ import { Drawer } from "@purpurds/drawer";
4
+ import c from "classnames/bind";
5
+
6
+ import styles from "./table-export-drawer.module.scss";
7
+
8
+ export type TableExportDrawerCopyProps = {
9
+ link: string;
10
+ title: string;
11
+ bodyText: string;
12
+ closeButtonText: string;
13
+ };
14
+
15
+ export type TableExportDrawerProps = {
16
+ isOpen: boolean;
17
+ exportFormats: string[];
18
+ copy: TableExportDrawerCopyProps;
19
+ onExportData: (format: string) => void;
20
+ setDrawerIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
21
+ };
22
+
23
+ const rootClassName = "purpur-table-export-drawer";
24
+ const cx = c.bind(styles);
25
+ const rootTestId = "purpur-table-export-drawer";
26
+
27
+ export const TableExportDrawer = ({
28
+ exportFormats,
29
+ isOpen,
30
+ copy,
31
+ onExportData,
32
+ setDrawerIsOpen,
33
+ }: TableExportDrawerProps) => (
34
+ <div id="purpur-table-export-drawer">
35
+ <Drawer data-testid={rootTestId} open={isOpen} onOpenChange={setDrawerIsOpen}>
36
+ <Drawer.Content
37
+ data-testid={`${rootTestId}-content`}
38
+ zIndex={6}
39
+ title={copy.title}
40
+ bodyText={copy.bodyText}
41
+ closeButtonText={copy.closeButtonText}
42
+ >
43
+ <div className={cx(`${rootClassName}__content`)}>
44
+ {exportFormats.map((format) => (
45
+ <Button
46
+ key={format}
47
+ data-testid={`${rootTestId}-export-button`}
48
+ variant="secondary"
49
+ onClick={() => onExportData(format)}
50
+ fullWidth
51
+ >
52
+ {`${copy.link} ${format}`}
53
+ </Button>
54
+ ))}
55
+ </div>
56
+ </Drawer.Content>
57
+ </Drawer>
58
+ </div>
59
+ );
@@ -0,0 +1,35 @@
1
+ import React, { ReactNode } from "react";
2
+ import c from "classnames/bind";
3
+
4
+ import styles from "./table.module.scss";
5
+ const cx = c.bind(styles);
6
+
7
+ type TableHeaderProps = {
8
+ ["data-testid"]?: string;
9
+ children: ReactNode;
10
+ className?: string;
11
+ columnFiltersEnabled: boolean;
12
+ };
13
+
14
+ const rootClassName = "purpur-table-header";
15
+
16
+ export const TableHeader = ({
17
+ children,
18
+ className,
19
+ columnFiltersEnabled,
20
+ ...props
21
+ }: TableHeaderProps) => {
22
+ const classes = cx([
23
+ className,
24
+ rootClassName,
25
+ {
26
+ [`${rootClassName}--has-filters`]: columnFiltersEnabled,
27
+ },
28
+ ]);
29
+
30
+ return (
31
+ <thead className={classes} {...props}>
32
+ {children}
33
+ </thead>
34
+ );
35
+ };