@transferwise/components 0.0.0-experimental-d1715ff → 0.0.0-experimental-3064bdb

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 (44) hide show
  1. package/build/i18n/en.json +2 -0
  2. package/build/i18n/en.json.js +2 -0
  3. package/build/i18n/en.json.js.map +1 -1
  4. package/build/i18n/en.json.mjs +2 -0
  5. package/build/i18n/en.json.mjs.map +1 -1
  6. package/build/main.css +214 -0
  7. package/build/styles/main.css +214 -0
  8. package/build/styles/table/Table.css +214 -0
  9. package/build/types/table/Table.d.ts +23 -0
  10. package/build/types/table/Table.d.ts.map +1 -0
  11. package/build/types/table/Table.messages.d.ts +12 -0
  12. package/build/types/table/Table.messages.d.ts.map +1 -0
  13. package/build/types/table/TableCell.d.ts +37 -0
  14. package/build/types/table/TableCell.d.ts.map +1 -0
  15. package/build/types/table/TableHeader.d.ts +12 -0
  16. package/build/types/table/TableHeader.d.ts.map +1 -0
  17. package/build/types/table/TableRow.d.ts +17 -0
  18. package/build/types/table/TableRow.d.ts.map +1 -0
  19. package/build/types/table/TableStatusText.d.ts +9 -0
  20. package/build/types/table/TableStatusText.d.ts.map +1 -0
  21. package/build/types/table/index.d.ts +6 -0
  22. package/build/types/table/index.d.ts.map +1 -0
  23. package/package.json +5 -5
  24. package/src/i18n/en.json +2 -0
  25. package/src/main.css +214 -0
  26. package/src/main.less +1 -0
  27. package/src/table/Table.css +214 -0
  28. package/src/table/Table.less +253 -0
  29. package/src/table/Table.messages.ts +12 -0
  30. package/src/table/Table.spec.tsx +87 -0
  31. package/src/table/Table.story.tsx +352 -0
  32. package/src/table/Table.tsx +121 -0
  33. package/src/table/TableCell.spec.tsx +298 -0
  34. package/src/table/TableCell.tsx +153 -0
  35. package/src/table/TableHeader.spec.tsx +58 -0
  36. package/src/table/TableHeader.tsx +50 -0
  37. package/src/table/TableRow.spec.tsx +104 -0
  38. package/src/table/TableRow.tsx +62 -0
  39. package/src/table/TableStatusText.spec.tsx +53 -0
  40. package/src/table/TableStatusText.tsx +35 -0
  41. package/src/table/index.ts +11 -0
  42. package/src/test-utils/assets/avatar-rectangle-fox.webp +0 -0
  43. package/src/test-utils/assets/avatar-square-dude.webp +0 -0
  44. package/src/test-utils/assets/tapestry-01.png +0 -0
@@ -0,0 +1,121 @@
1
+ import { useIntl } from 'react-intl';
2
+ import TableCell from './TableCell';
3
+ import TableHeader, { TableHeaderType } from './TableHeader';
4
+ import TableRow, { TableRowClickableType, TableRowType } from './TableRow';
5
+ import Alert from '../alert';
6
+
7
+ import messages from './Table.messages';
8
+ import Loader from '../loader';
9
+ import { Sentiment, Size } from '../common';
10
+ import StatusIcon from '../statusIcon';
11
+ import clsx from 'clsx';
12
+
13
+ export interface TableProps {
14
+ data: {
15
+ headers?: TableHeaderType[];
16
+ content?: TableRowType[] | TableRowClickableType[];
17
+ onRowClick?: (rowData: TableRowType | TableRowClickableType) => void;
18
+ };
19
+ loading?: boolean;
20
+ className?: string | undefined;
21
+ fullWidth?: boolean;
22
+ error?: {
23
+ message?: string;
24
+ action?: {
25
+ href?: string;
26
+ text?: string;
27
+ };
28
+ };
29
+ onRetry?: () => void;
30
+ }
31
+
32
+ const Table: React.FC<TableProps> = ({ data, loading, className, fullWidth = true, error }) => {
33
+ const { formatMessage } = useIntl();
34
+
35
+ const getTableContent = () => {
36
+ if (loading) {
37
+ return (
38
+ <TableRow>
39
+ <TableCell>
40
+ <Loader data-testid="np-table-loader" />
41
+ </TableCell>
42
+ </TableRow>
43
+ );
44
+ }
45
+
46
+ // Shows the `emptyData` message when there is no data to display
47
+ if (!data?.content?.length) {
48
+ return (
49
+ <TableRow>
50
+ <TableCell colSpan={data?.headers?.length ? data?.headers?.length : undefined}>
51
+ <div className="np-table-empty-data" data-testid="np-table-empty-data">
52
+ <StatusIcon sentiment={Sentiment.WARNING} size={Size.MEDIUM} />
53
+ <div className="np-text-body-large-bold">{formatMessage(messages.emptyData)}</div>
54
+ </div>
55
+ </TableCell>
56
+ </TableRow>
57
+ );
58
+ }
59
+
60
+ return data?.content?.map((rowData, rowIndex) => {
61
+ return (
62
+ <TableRow
63
+ key={'table-row'.concat(rowIndex.toString())}
64
+ rowData={rowData}
65
+ hasSeparator={data?.content?.length ? data.content.length - 1 !== rowIndex : false}
66
+ onRowClick={data?.onRowClick}
67
+ />
68
+ );
69
+ });
70
+ };
71
+
72
+ if (error) {
73
+ return (
74
+ <Alert
75
+ message={error.message}
76
+ type={Sentiment.NEGATIVE}
77
+ action={{
78
+ href: error?.action?.href ?? '/',
79
+ text: error?.action?.text ?? formatMessage(messages.refreshPage),
80
+ }}
81
+ data-testid="np-table-error"
82
+ />
83
+ );
84
+ }
85
+
86
+ return (
87
+ <div
88
+ className={clsx('np-table-container', className, {
89
+ 'np-table-container--loading': loading,
90
+ 'np-table-container--center': !fullWidth,
91
+ 'np-table-container--full-width': fullWidth,
92
+ })}
93
+ data-testid="np-table-container"
94
+ >
95
+ <div className="np-table-inner-container">
96
+ <table className="np-table">
97
+ <thead>
98
+ <tr>
99
+ {(loading ?? (data?.headers && !data?.headers.length)) ? (
100
+ <TableHeader />
101
+ ) : (
102
+ data?.headers?.map((headerItem: TableHeaderType, index) => (
103
+ <TableHeader
104
+ key={headerItem.header?.concat(index.toString())}
105
+ hasActionColumn={
106
+ data?.onRowClick && index === Number(data?.headers?.length) - 1
107
+ }
108
+ {...headerItem}
109
+ />
110
+ ))
111
+ )}
112
+ </tr>
113
+ </thead>
114
+ <tbody>{getTableContent()}</tbody>
115
+ </table>
116
+ </div>
117
+ </div>
118
+ );
119
+ };
120
+
121
+ export default Table;
@@ -0,0 +1,298 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import '@testing-library/jest-dom';
3
+ import TableCell, {
4
+ CurrencyContentType,
5
+ LeadingContentType,
6
+ StatusContentType,
7
+ TextContentType,
8
+ } from './TableCell';
9
+ import { IntlProvider } from 'react-intl';
10
+ import { mockMatchMedia } from '../test-utils';
11
+
12
+ mockMatchMedia();
13
+
14
+ describe('TableCell Component', () => {
15
+ const cellContentMocks = {
16
+ leading: {
17
+ primaryText: 'Leading text',
18
+ secondaryText: 'Description',
19
+ initials: 'AB',
20
+ } satisfies LeadingContentType,
21
+ text: {
22
+ text: 'Cell text',
23
+ } satisfies TextContentType,
24
+ currency: {
25
+ primaryCurrency: {
26
+ value: '12345.67',
27
+ currency: 'eur',
28
+ },
29
+ secondaryCurrency: {
30
+ value: '11000.00',
31
+ currency: 'gbp',
32
+ },
33
+ } satisfies CurrencyContentType,
34
+ status: {
35
+ primaryText: 'Primary text',
36
+ secondaryText: 'Description',
37
+ sentiment: 'negative',
38
+ } satisfies StatusContentType,
39
+ custom: <div>Custom content</div>,
40
+ };
41
+
42
+ const renderWithIntl = (component: React.ReactElement) => {
43
+ return render(<IntlProvider locale="en">{component}</IntlProvider>);
44
+ };
45
+
46
+ it('renders without content correctly', () => {
47
+ renderWithIntl(<TableCell />);
48
+ expect(screen.getByRole('cell')).toBeInTheDocument();
49
+ });
50
+
51
+ it('renders with colSpan correctly', () => {
52
+ renderWithIntl(<TableCell colSpan={2} />);
53
+ expect(screen.getByRole('cell')).toHaveAttribute('colSpan', '2');
54
+ });
55
+
56
+ it('renders with custom className correctly', () => {
57
+ renderWithIntl(<TableCell className="custom-class" />);
58
+ expect(screen.getByRole('cell')).toHaveClass('custom-class');
59
+ });
60
+
61
+ it('renders separator correctly', () => {
62
+ renderWithIntl(<TableCell hasSeparator />);
63
+ expect(screen.getByTestId('np-table-cell-separator')).toBeInTheDocument();
64
+ });
65
+
66
+ it("renders cell with children's content correctly", () => {
67
+ renderWithIntl(<TableCell>{cellContentMocks.custom}</TableCell>);
68
+ expect(screen.getByText('Custom content')).toBeInTheDocument();
69
+ });
70
+
71
+ it('applies alignment class correctly', () => {
72
+ renderWithIntl(<TableCell alignment="right" />);
73
+ expect(screen.getByRole('cell')).toHaveClass('np-table-cell--right');
74
+ });
75
+
76
+ it('renders text content correctly', () => {
77
+ renderWithIntl(<TableCell content={{ ...cellContentMocks.text }} />);
78
+ expect(screen.getByText('Cell text')).toBeInTheDocument();
79
+ });
80
+
81
+ it('renders text with error content correctly', () => {
82
+ renderWithIntl(<TableCell content={{ ...cellContentMocks.text, status: 'error' }} />);
83
+ expect(screen.getByText('Cell text')).toBeInTheDocument();
84
+ expect(screen.getByText('Cell text')).toHaveClass('np-table-content--error');
85
+ expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
86
+ });
87
+
88
+ it('renders text with success content correctly', () => {
89
+ renderWithIntl(<TableCell content={{ ...cellContentMocks.text, status: 'success' }} />);
90
+ expect(screen.getByText('Cell text')).toBeInTheDocument();
91
+ expect(screen.getByText('Cell text')).toHaveClass('np-table-content--success');
92
+ expect(screen.getByTestId('check-icon')).toBeInTheDocument();
93
+ });
94
+
95
+ it('renders leading content with initials correctly', () => {
96
+ renderWithIntl(<TableCell type="leading" content={{ ...cellContentMocks.leading }} />);
97
+ expect(screen.getByText('AB')).toBeInTheDocument();
98
+ expect(screen.getByText('Leading text')).toBeInTheDocument();
99
+ expect(screen.getByText('Description')).toBeInTheDocument();
100
+ });
101
+
102
+ it('renders leading cell without media content when initials are undefined', () => {
103
+ renderWithIntl(
104
+ <TableCell
105
+ type="leading"
106
+ content={{
107
+ ...cellContentMocks.leading,
108
+ initials: undefined,
109
+ }}
110
+ />,
111
+ );
112
+ expect(screen.queryByText('AB')).not.toBeInTheDocument();
113
+ expect(screen.getByText('Leading text')).toBeInTheDocument();
114
+ expect(screen.getByText('Description')).toBeInTheDocument();
115
+ expect(screen.queryByTestId('np-table-content-media')).not.toBeInTheDocument();
116
+ });
117
+
118
+ it('renders leading cell without primary text if it is undefined', () => {
119
+ renderWithIntl(
120
+ <TableCell
121
+ type="leading"
122
+ content={{
123
+ ...cellContentMocks.leading,
124
+ primaryText: undefined,
125
+ }}
126
+ />,
127
+ );
128
+ expect(screen.getByText('AB')).toBeInTheDocument();
129
+ expect(screen.queryByText('Leading text')).not.toBeInTheDocument();
130
+ expect(screen.getByText('Description')).toBeInTheDocument();
131
+ });
132
+
133
+ it('renders leading cell without secondary text if it is undefined', () => {
134
+ renderWithIntl(
135
+ <TableCell
136
+ type="leading"
137
+ content={{
138
+ ...cellContentMocks.leading,
139
+ secondaryText: undefined,
140
+ }}
141
+ />,
142
+ );
143
+ expect(screen.getByText('AB')).toBeInTheDocument();
144
+ expect(screen.getByText('Leading text')).toBeInTheDocument();
145
+ expect(screen.queryByText('Description')).not.toBeInTheDocument();
146
+ });
147
+
148
+ it('renders leading content with error content correctly', () => {
149
+ renderWithIntl(
150
+ <TableCell
151
+ type="leading"
152
+ content={{
153
+ ...cellContentMocks.leading,
154
+ status: 'error',
155
+ }}
156
+ />,
157
+ );
158
+ expect(screen.getByText('AB')).toBeInTheDocument();
159
+ expect(screen.getByText('Leading text')).toHaveClass('np-table-content--error');
160
+ expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
161
+ });
162
+
163
+ it('renders leading content with success content correctly', () => {
164
+ renderWithIntl(
165
+ <TableCell
166
+ type="leading"
167
+ content={{
168
+ ...cellContentMocks.leading,
169
+ status: 'success',
170
+ }}
171
+ />,
172
+ );
173
+ expect(screen.getByText('AB')).toBeInTheDocument();
174
+ expect(screen.getByText('Leading text')).toHaveClass('np-table-content--success');
175
+ expect(screen.getByTestId('check-icon')).toBeInTheDocument();
176
+ });
177
+
178
+ it('renders status content correctly', () => {
179
+ renderWithIntl(<TableCell type="status" content={{ ...cellContentMocks.status }} />);
180
+ expect(screen.getByTestId('status-icon')).toBeInTheDocument();
181
+ expect(screen.getByTestId('status-icon')).toHaveClass('negative');
182
+ expect(screen.getByText('Primary text')).toBeInTheDocument();
183
+ expect(screen.getByText('Description')).toBeInTheDocument();
184
+ });
185
+
186
+ it('renders status type with default sentiment', () => {
187
+ renderWithIntl(
188
+ <TableCell
189
+ type="status"
190
+ content={{ primaryText: 'Primary text', secondaryText: 'Description' }}
191
+ />,
192
+ );
193
+ expect(screen.getByTestId('info-icon')).toBeInTheDocument();
194
+ expect(screen.getByText('Primary text')).toBeInTheDocument();
195
+ expect(screen.getByText('Description')).toBeInTheDocument();
196
+ });
197
+
198
+ it('renders currency content correctly', () => {
199
+ renderWithIntl(<TableCell type="currency" content={{ ...cellContentMocks.currency }} />);
200
+ expect(screen.getByRole('presentation')).toBeInTheDocument();
201
+ expect(screen.getByText('12,345.67 EUR')).toBeInTheDocument();
202
+ expect(screen.getByText('11,000.00 GBP')).toBeInTheDocument();
203
+ });
204
+
205
+ it('renders currency cell without media content when currency value of primaryCurrency is empty', () => {
206
+ renderWithIntl(
207
+ <TableCell
208
+ type="currency"
209
+ content={{
210
+ ...cellContentMocks.currency,
211
+ primaryCurrency: {
212
+ value: cellContentMocks.currency.primaryCurrency.value,
213
+ currency: '',
214
+ },
215
+ }}
216
+ />,
217
+ );
218
+ expect(screen.queryByRole('presentation')).not.toBeInTheDocument();
219
+ expect(screen.getByText('12,345.67')).toBeInTheDocument();
220
+ expect(screen.getByText('11,000.00 GBP')).toBeInTheDocument();
221
+ expect(screen.queryByTestId('np-table-content-media')).not.toBeInTheDocument();
222
+ });
223
+
224
+ it('renders currency cell without primary currency when it is undefined', () => {
225
+ renderWithIntl(
226
+ <TableCell
227
+ type="currency"
228
+ content={{
229
+ ...cellContentMocks.currency,
230
+ primaryCurrency: undefined,
231
+ }}
232
+ />,
233
+ );
234
+ expect(screen.queryByText('12,345.67 EUR')).not.toBeInTheDocument();
235
+ expect(screen.getByText('11,000.00 GBP')).toBeInTheDocument();
236
+ });
237
+
238
+ it('renders currency cell without secondary currency when it is undefined', () => {
239
+ renderWithIntl(
240
+ <TableCell
241
+ type="currency"
242
+ content={{
243
+ ...cellContentMocks.currency,
244
+ secondaryCurrency: undefined,
245
+ }}
246
+ />,
247
+ );
248
+ expect(screen.getByText('12,345.67 EUR')).toBeInTheDocument();
249
+ expect(screen.queryByText('11,000.00 GBP')).not.toBeInTheDocument();
250
+ });
251
+
252
+ it('renders currency cell without secondary currency when its currency is empty', () => {
253
+ renderWithIntl(
254
+ <TableCell
255
+ type="currency"
256
+ content={{
257
+ ...cellContentMocks.currency,
258
+ secondaryCurrency: {
259
+ value: cellContentMocks.currency.secondaryCurrency.value,
260
+ currency: '',
261
+ },
262
+ }}
263
+ />,
264
+ );
265
+ expect(screen.getByText('12,345.67 EUR')).toBeInTheDocument();
266
+ expect(screen.getByText('11,000.00')).toBeInTheDocument();
267
+ });
268
+
269
+ it('renders currency content correctly with error content correctly', () => {
270
+ renderWithIntl(
271
+ <TableCell
272
+ type="currency"
273
+ content={{
274
+ ...cellContentMocks.currency,
275
+ status: 'error',
276
+ }}
277
+ />,
278
+ );
279
+ expect(screen.getByText('12,345.67 EUR')).toBeInTheDocument();
280
+ expect(screen.getByText('12,345.67 EUR')).toHaveClass('np-table-content--error');
281
+ expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
282
+ });
283
+
284
+ it('renders currency content correctly with success content correctly', () => {
285
+ renderWithIntl(
286
+ <TableCell
287
+ type="currency"
288
+ content={{
289
+ ...cellContentMocks.currency,
290
+ status: 'success',
291
+ }}
292
+ />,
293
+ );
294
+ expect(screen.getByText('12,345.67 EUR')).toBeInTheDocument();
295
+ expect(screen.getByText('12,345.67 EUR')).toHaveClass('np-table-content--success');
296
+ expect(screen.getByTestId('check-icon')).toBeInTheDocument();
297
+ });
298
+ });
@@ -0,0 +1,153 @@
1
+ import TableStatusText from './TableStatusText';
2
+ import Avatar, { AvatarType } from '../avatar';
3
+ import StatusIcon from '../statusIcon';
4
+ import { Flag } from '@wise/art';
5
+ import Body from '../body';
6
+ import { formatMoney } from '@transferwise/formatting';
7
+ import { useIntl } from 'react-intl';
8
+ import React from 'react';
9
+ import { clsx } from 'clsx';
10
+
11
+ // `Leading` and `Status` cells' types have 2 text fields: primary and secondary
12
+ interface TextPropsType {
13
+ primaryText?: string;
14
+ secondaryText?: string;
15
+ }
16
+
17
+ // `Leading`, `Text` and `Status` cells' types can have a status indicator with `error` or `success` values
18
+ interface StatusPropsType {
19
+ status?: 'error' | 'success';
20
+ }
21
+
22
+ // TODO: Instead of initials it should be an avatar, change implementation
23
+ export interface LeadingContentType extends TextPropsType, StatusPropsType {
24
+ initials?: string;
25
+ }
26
+
27
+ export interface TextContentType extends StatusPropsType {
28
+ text?: string;
29
+ }
30
+
31
+ interface CurrencyType {
32
+ value: string | number;
33
+ currency: string;
34
+ }
35
+
36
+ export interface CurrencyContentType extends StatusPropsType {
37
+ primaryCurrency?: CurrencyType;
38
+ secondaryCurrency?: CurrencyType;
39
+ }
40
+
41
+ export interface StatusContentType extends TextPropsType {
42
+ sentiment?: 'negative' | 'neutral' | 'positive' | 'warning' | 'pending';
43
+ }
44
+
45
+ export interface TableCellProps {
46
+ type?: 'leading' | 'text' | 'currency' | 'status';
47
+ content?: LeadingContentType & TextContentType & CurrencyContentType & StatusContentType;
48
+ alignment?: 'right' | 'left';
49
+ className?: string;
50
+ colSpan?: number;
51
+ hasSeparator?: boolean;
52
+ children?: React.ReactNode;
53
+ }
54
+
55
+ const TableCell = ({
56
+ type = 'text',
57
+ content,
58
+ alignment,
59
+ className,
60
+ colSpan,
61
+ hasSeparator,
62
+ children,
63
+ }: TableCellProps) => {
64
+ const { locale } = useIntl();
65
+
66
+ const getContentMedia = () => {
67
+ let mediaContent = null;
68
+
69
+ if (type === 'leading' && content?.initials) {
70
+ mediaContent = (
71
+ <Avatar size={40} type={AvatarType.INITIALS}>
72
+ {content?.initials}
73
+ </Avatar>
74
+ );
75
+ }
76
+
77
+ if (type === 'currency' && content?.primaryCurrency?.currency) {
78
+ mediaContent = (
79
+ <Flag code={content?.primaryCurrency?.currency?.toLowerCase()} intrinsicSize={24} />
80
+ );
81
+ }
82
+
83
+ if (type === 'status') {
84
+ mediaContent = <StatusIcon size="md" sentiment={content?.sentiment ?? 'neutral'} />;
85
+ }
86
+
87
+ if (mediaContent) {
88
+ return (
89
+ <div className="np-table-content-media" data-testid="np-table-content-media">
90
+ {mediaContent}
91
+ </div>
92
+ );
93
+ }
94
+ };
95
+
96
+ const formatCurrencyValue = (currency?: CurrencyType) => {
97
+ if (currency) {
98
+ return formatMoney(Number(currency.value), currency.currency, locale, {
99
+ alwaysShowDecimals: true,
100
+ });
101
+ }
102
+
103
+ return '';
104
+ };
105
+
106
+ return (
107
+ <td
108
+ className={clsx('np-table-cell', `np-table-cell--${type}`, className, {
109
+ 'np-table-cell--right': alignment === 'right',
110
+ })}
111
+ colSpan={colSpan}
112
+ >
113
+ {type === 'text' && content?.text && (
114
+ <TableStatusText text={content?.text} status={content?.status} />
115
+ )}
116
+ {['leading', 'currency', 'status'].includes(type) && (
117
+ <div
118
+ className={clsx('np-table-content', {
119
+ 'np-table-content--reversed': type === 'currency',
120
+ })}
121
+ >
122
+ {getContentMedia()}
123
+ <div className="np-table-content-body">
124
+ {(content?.primaryCurrency ?? content?.primaryText) && (
125
+ <TableStatusText
126
+ text={
127
+ type === 'currency'
128
+ ? formatCurrencyValue(content?.primaryCurrency)
129
+ : (content?.primaryText ?? '')
130
+ }
131
+ status={type !== 'status' ? content?.status : undefined}
132
+ typography="large-bold"
133
+ />
134
+ )}
135
+ {(content?.secondaryCurrency ?? content?.secondaryText) && (
136
+ <Body>
137
+ {type === 'currency'
138
+ ? formatCurrencyValue(content?.secondaryCurrency)
139
+ : content?.secondaryText}
140
+ </Body>
141
+ )}
142
+ </div>
143
+ </div>
144
+ )}
145
+ {hasSeparator && (
146
+ <div className="np-table-cell-separator" data-testid="np-table-cell-separator" />
147
+ )}
148
+ {children}
149
+ </td>
150
+ );
151
+ };
152
+
153
+ export default TableCell;
@@ -0,0 +1,58 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import TableHeader, { TableHeaderProps } from './TableHeader';
3
+
4
+ describe('TableHeader Component', () => {
5
+ const renderComponent = (props: Partial<TableHeaderProps> = {}) => {
6
+ const defaultProps = {
7
+ header: '',
8
+ className: '',
9
+ alignment: 'left',
10
+ hasError: false,
11
+ } satisfies TableHeaderProps;
12
+ return render(<TableHeader {...defaultProps} {...props} />);
13
+ };
14
+
15
+ it('should render without crashing', () => {
16
+ const { container } = renderComponent();
17
+ expect(container).toBeInTheDocument();
18
+ });
19
+
20
+ it('should render header text when provided', () => {
21
+ const headerText = 'Test Header';
22
+ renderComponent({ header: headerText });
23
+ expect(screen.getByText(headerText)).toBeInTheDocument();
24
+ });
25
+
26
+ it('should apply custom className', () => {
27
+ const className = 'custom-class';
28
+ renderComponent({ className });
29
+ expect(screen.getByRole('columnheader')).toHaveClass(className);
30
+ });
31
+
32
+ it('should align text to the right when alignment is set to right', () => {
33
+ renderComponent({ alignment: 'right' });
34
+ expect(screen.getByRole('columnheader')).toHaveClass('np-table-header--right');
35
+ });
36
+
37
+ it('should show error class when hasError is passed', () => {
38
+ renderComponent({ hasError: true });
39
+ expect(screen.getByRole('columnheader')).toHaveClass('np-table-header--has-error');
40
+ });
41
+
42
+ it('should render empty header content when header is not provided', () => {
43
+ renderComponent();
44
+ expect(screen.getByTestId('np-table-empty-header').innerHTML).toBe('&nbsp;');
45
+ });
46
+
47
+ it('should render with colSpan of 2 when hasActionColumn is passed', () => {
48
+ renderComponent({ hasActionColumn: true });
49
+ expect(screen.getByRole('columnheader')).toHaveAttribute('colSpan', '2');
50
+ });
51
+
52
+ it('should render TableStatusText with error status when hasError is passed', () => {
53
+ const headerText = 'Test Header';
54
+ renderComponent({ header: headerText, hasError: true });
55
+ expect(screen.getByText(headerText)).toHaveClass('np-table-content--error');
56
+ expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
57
+ });
58
+ });
@@ -0,0 +1,50 @@
1
+ import TableStatusText from './TableStatusText';
2
+ import { clsx } from 'clsx';
3
+
4
+ // TODO: Add `width` prop in next iterations
5
+ export interface TableHeaderType {
6
+ header?: string;
7
+ className?: string;
8
+ alignment?: 'right' | 'left';
9
+ hasError?: boolean;
10
+ }
11
+
12
+ export interface TableHeaderProps extends TableHeaderType {
13
+ hasActionColumn?: boolean;
14
+ }
15
+
16
+ const TableHeader: React.FC<TableHeaderProps> = ({
17
+ header,
18
+ className,
19
+ alignment = 'left',
20
+ hasError = false,
21
+ hasActionColumn = false,
22
+ }: TableHeaderProps) => {
23
+ return (
24
+ <th
25
+ className={clsx('np-table-header', className, {
26
+ 'np-table-header--right': alignment === 'right',
27
+ 'np-table-header--has-error': hasError,
28
+ })}
29
+ colSpan={hasActionColumn ? 2 : undefined}
30
+ >
31
+ {header ? (
32
+ <TableStatusText
33
+ text={header}
34
+ className="np-table-header-content"
35
+ status={hasError ? 'error' : undefined}
36
+ typography="large-bold"
37
+ />
38
+ ) : (
39
+ <div
40
+ className="np-table-header-content np-text-body-large-bold"
41
+ data-testid="np-table-empty-header"
42
+ >
43
+ &nbsp;
44
+ </div>
45
+ )}
46
+ </th>
47
+ );
48
+ };
49
+
50
+ export default TableHeader;