@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.
- package/dist/LICENSE.txt +213 -0
- package/dist/cell-types/badge-cell.d.ts +8 -0
- package/dist/cell-types/badge-cell.d.ts.map +1 -0
- package/dist/cell-types/body-text-cell.d.ts +8 -0
- package/dist/cell-types/body-text-cell.d.ts.map +1 -0
- package/dist/cell-types/button-cell.d.ts +8 -0
- package/dist/cell-types/button-cell.d.ts.map +1 -0
- package/dist/cell-types/button-group-cell.d.ts +16 -0
- package/dist/cell-types/button-group-cell.d.ts.map +1 -0
- package/dist/cell-types/cta-link-cell.d.ts +8 -0
- package/dist/cell-types/cta-link-cell.d.ts.map +1 -0
- package/dist/cell-types/date-cell.d.ts +8 -0
- package/dist/cell-types/date-cell.d.ts.map +1 -0
- package/dist/cell-types/empty-cell.d.ts +4 -0
- package/dist/cell-types/empty-cell.d.ts.map +1 -0
- package/dist/cell-types/error-message-cell.d.ts +8 -0
- package/dist/cell-types/error-message-cell.d.ts.map +1 -0
- package/dist/cell-types/icon-text-cell.d.ts +8 -0
- package/dist/cell-types/icon-text-cell.d.ts.map +1 -0
- package/dist/cell-types/lead-text-cell.d.ts +8 -0
- package/dist/cell-types/lead-text-cell.d.ts.map +1 -0
- package/dist/cell-types/link-cell.d.ts +8 -0
- package/dist/cell-types/link-cell.d.ts.map +1 -0
- package/dist/cell-types/number-cell.d.ts +8 -0
- package/dist/cell-types/number-cell.d.ts.map +1 -0
- package/dist/cell-types/row-selection-cell.d.ts +8 -0
- package/dist/cell-types/row-selection-cell.d.ts.map +1 -0
- package/dist/cell-types/row-toggle-cell.d.ts +8 -0
- package/dist/cell-types/row-toggle-cell.d.ts.map +1 -0
- package/dist/cell-types/toggle-cell.d.ts +8 -0
- package/dist/cell-types/toggle-cell.d.ts.map +1 -0
- package/dist/cell-types/warning-message-cell.d.ts +8 -0
- package/dist/cell-types/warning-message-cell.d.ts.map +1 -0
- package/dist/metadata.js +17 -0
- package/dist/story-utils/column-def.d.ts +5 -0
- package/dist/story-utils/column-def.d.ts.map +1 -0
- package/dist/story-utils/table-data.d.ts +35 -0
- package/dist/story-utils/table-data.d.ts.map +1 -0
- package/dist/story-utils/use-fetch-table-data-hook.d.ts +11 -0
- package/dist/story-utils/use-fetch-table-data-hook.d.ts.map +1 -0
- package/dist/styles.css +1 -0
- package/dist/table-action-bar.d.ts +26 -0
- package/dist/table-action-bar.d.ts.map +1 -0
- package/dist/table-body.d.ts +10 -0
- package/dist/table-body.d.ts.map +1 -0
- package/dist/table-column-header-cell.d.ts +28 -0
- package/dist/table-column-header-cell.d.ts.map +1 -0
- package/dist/table-export-drawer.d.ts +17 -0
- package/dist/table-export-drawer.d.ts.map +1 -0
- package/dist/table-header.d.ts +11 -0
- package/dist/table-header.d.ts.map +1 -0
- package/dist/table-row-cell-skeleton.d.ts +14 -0
- package/dist/table-row-cell-skeleton.d.ts.map +1 -0
- package/dist/table-row-cell.d.ts +25 -0
- package/dist/table-row-cell.d.ts.map +1 -0
- package/dist/table-row.d.ts +11 -0
- package/dist/table-row.d.ts.map +1 -0
- package/dist/table-settings-drawer.d.ts +41 -0
- package/dist/table-settings-drawer.d.ts.map +1 -0
- package/dist/table-toolbar.d.ts +37 -0
- package/dist/table-toolbar.d.ts.map +1 -0
- package/dist/table.cjs.js +259 -0
- package/dist/table.cjs.js.map +1 -0
- package/dist/table.d.ts +20 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.es.js +13585 -0
- package/dist/table.es.js.map +1 -0
- package/dist/test-utils/column-def.d.ts +6 -0
- package/dist/test-utils/column-def.d.ts.map +1 -0
- package/dist/test-utils/helpers.d.ts +138 -0
- package/dist/test-utils/helpers.d.ts.map +1 -0
- package/dist/test-utils/table-data.d.ts +33 -0
- package/dist/test-utils/table-data.d.ts.map +1 -0
- package/dist/types.d.ts +420 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/use-screen-size.hook.d.ts +7 -0
- package/dist/use-screen-size.hook.d.ts.map +1 -0
- package/dist/use-truncated-hook.d.ts +10 -0
- package/dist/use-truncated-hook.d.ts.map +1 -0
- package/dist/utils/custom-functions.d.ts +9 -0
- package/dist/utils/custom-functions.d.ts.map +1 -0
- package/dist/utils/unit-conversions.d.ts +19 -0
- package/dist/utils/unit-conversions.d.ts.map +1 -0
- package/dist/utils/unit-conversions.spec.d.ts +2 -0
- package/dist/utils/unit-conversions.spec.d.ts.map +1 -0
- package/eslint.config.mjs +2 -0
- package/package.json +82 -0
- package/src/cell-types/badge-cell.tsx +25 -0
- package/src/cell-types/body-text-cell.tsx +54 -0
- package/src/cell-types/button-cell.tsx +26 -0
- package/src/cell-types/button-group-cell.tsx +54 -0
- package/src/cell-types/cta-link-cell.tsx +25 -0
- package/src/cell-types/date-cell.tsx +33 -0
- package/src/cell-types/empty-cell.tsx +6 -0
- package/src/cell-types/error-message-cell.tsx +30 -0
- package/src/cell-types/icon-text-cell.tsx +30 -0
- package/src/cell-types/lead-text-cell.tsx +19 -0
- package/src/cell-types/link-cell.tsx +58 -0
- package/src/cell-types/number-cell.tsx +27 -0
- package/src/cell-types/row-selection-cell.tsx +22 -0
- package/src/cell-types/row-toggle-cell.tsx +23 -0
- package/src/cell-types/toggle-cell.tsx +19 -0
- package/src/cell-types/warning-message-cell.tsx +30 -0
- package/src/global.d.ts +4 -0
- package/src/story-utils/column-def.ts +148 -0
- package/src/story-utils/table-data.tsx +262 -0
- package/src/story-utils/use-fetch-table-data-hook.tsx +30 -0
- package/src/table-action-bar.module.scss +106 -0
- package/src/table-action-bar.test.tsx +111 -0
- package/src/table-action-bar.tsx +104 -0
- package/src/table-body.tsx +25 -0
- package/src/table-column-header-cell.tsx +305 -0
- package/src/table-export-drawer.module.scss +9 -0
- package/src/table-export-drawer.test.tsx +75 -0
- package/src/table-export-drawer.tsx +59 -0
- package/src/table-header.tsx +35 -0
- package/src/table-kitchen-sink.test.tsx +1196 -0
- package/src/table-row-cell-skeleton.tsx +61 -0
- package/src/table-row-cell.test.tsx +360 -0
- package/src/table-row-cell.tsx +188 -0
- package/src/table-row.tsx +30 -0
- package/src/table-settings-drawer.module.scss +25 -0
- package/src/table-settings-drawer.test.tsx +350 -0
- package/src/table-settings-drawer.tsx +254 -0
- package/src/table-toolbar.module.scss +17 -0
- package/src/table-toolbar.test.tsx +95 -0
- package/src/table-toolbar.tsx +136 -0
- package/src/table.module.scss +367 -0
- package/src/table.stories.tsx +1246 -0
- package/src/table.story.css +11 -0
- package/src/table.test.tsx +318 -0
- package/src/table.tsx +501 -0
- package/src/test-utils/column-def.ts +152 -0
- package/src/test-utils/helpers.ts +234 -0
- package/src/test-utils/table-data.tsx +318 -0
- package/src/types.ts +496 -0
- package/src/use-screen-size.hook.ts +23 -0
- package/src/use-truncated-hook.tsx +74 -0
- package/src/utils/custom-functions.ts +52 -0
- package/src/utils/unit-conversions.spec.ts +92 -0
- package/src/utils/unit-conversions.ts +30 -0
- 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,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
|
+
};
|