@transferwise/components 46.81.0 → 46.82.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 (93) hide show
  1. package/build/alert/Alert.js +2 -9
  2. package/build/alert/Alert.js.map +1 -1
  3. package/build/alert/Alert.mjs +2 -9
  4. package/build/alert/Alert.mjs.map +1 -1
  5. package/build/i18n/en.json +5 -0
  6. package/build/i18n/en.json.js +5 -0
  7. package/build/i18n/en.json.js.map +1 -1
  8. package/build/i18n/en.json.mjs +5 -0
  9. package/build/i18n/en.json.mjs.map +1 -1
  10. package/build/logo/Logo.js +11 -131
  11. package/build/logo/Logo.js.map +1 -1
  12. package/build/logo/Logo.mjs +1 -121
  13. package/build/logo/Logo.mjs.map +1 -1
  14. package/build/logo/logo-assets.js +134 -0
  15. package/build/logo/logo-assets.js.map +1 -0
  16. package/build/logo/logo-assets.mjs +125 -0
  17. package/build/logo/logo-assets.mjs.map +1 -0
  18. package/build/main.css +274 -0
  19. package/build/money/Money.js +5 -2
  20. package/build/money/Money.js.map +1 -1
  21. package/build/money/Money.mjs +5 -2
  22. package/build/money/Money.mjs.map +1 -1
  23. package/build/styles/main.css +274 -0
  24. package/build/styles/table/Table.css +274 -0
  25. package/build/types/alert/Alert.d.ts +1 -5
  26. package/build/types/alert/Alert.d.ts.map +1 -1
  27. package/build/types/logo/Logo.d.ts.map +1 -1
  28. package/build/types/logo/logo-assets.d.ts +10 -0
  29. package/build/types/logo/logo-assets.d.ts.map +1 -0
  30. package/build/types/money/Money.d.ts +2 -1
  31. package/build/types/money/Money.d.ts.map +1 -1
  32. package/build/types/table/Table.d.ts +23 -0
  33. package/build/types/table/Table.d.ts.map +1 -0
  34. package/build/types/table/Table.messages.d.ts +24 -0
  35. package/build/types/table/Table.messages.d.ts.map +1 -0
  36. package/build/types/table/TableCell.d.ts +40 -0
  37. package/build/types/table/TableCell.d.ts.map +1 -0
  38. package/build/types/table/TableHeader.d.ts +13 -0
  39. package/build/types/table/TableHeader.d.ts.map +1 -0
  40. package/build/types/table/TableRow.d.ts +17 -0
  41. package/build/types/table/TableRow.d.ts.map +1 -0
  42. package/build/types/table/TableStatusText.d.ts +10 -0
  43. package/build/types/table/TableStatusText.d.ts.map +1 -0
  44. package/build/types/table/index.d.ts +6 -0
  45. package/build/types/table/index.d.ts.map +1 -0
  46. package/build/types/test-utils/index.d.ts +10 -0
  47. package/build/types/test-utils/index.d.ts.map +1 -1
  48. package/package.json +3 -4
  49. package/src/alert/Alert.spec.story.tsx +0 -82
  50. package/src/alert/Alert.spec.tsx +0 -30
  51. package/src/alert/Alert.tsx +51 -67
  52. package/src/i18n/en.json +5 -0
  53. package/src/logo/Logo.tsx +10 -8
  54. package/src/logo/__snapshots__/Logo.spec.tsx.snap +16 -16
  55. package/src/logo/logo-assets.tsx +137 -0
  56. package/src/main.css +274 -0
  57. package/src/main.less +1 -0
  58. package/src/money/Money.tsx +9 -2
  59. package/src/table/Table.css +274 -0
  60. package/src/table/Table.less +334 -0
  61. package/src/table/Table.messages.ts +24 -0
  62. package/src/table/Table.spec.tsx +82 -0
  63. package/src/table/Table.story.tsx +356 -0
  64. package/src/table/Table.tsx +167 -0
  65. package/src/table/TableCell.spec.tsx +298 -0
  66. package/src/table/TableCell.tsx +149 -0
  67. package/src/table/TableHeader.spec.tsx +50 -0
  68. package/src/table/TableHeader.tsx +74 -0
  69. package/src/table/TableRow.spec.tsx +112 -0
  70. package/src/table/TableRow.tsx +70 -0
  71. package/src/table/TableStatusText.spec.tsx +53 -0
  72. package/src/table/TableStatusText.tsx +40 -0
  73. package/src/table/index.ts +11 -0
  74. package/build/logo/svg/flag-inverse.svg +0 -1
  75. package/build/logo/svg/flag-platform-white.svg +0 -1
  76. package/build/logo/svg/flag-platform.svg +0 -1
  77. package/build/logo/svg/flag.svg +0 -1
  78. package/build/logo/svg/logo-business-inverse.svg +0 -1
  79. package/build/logo/svg/logo-business.svg +0 -1
  80. package/build/logo/svg/logo-inverse.svg +0 -1
  81. package/build/logo/svg/logo-platform-white.svg +0 -1
  82. package/build/logo/svg/logo-platform.svg +0 -1
  83. package/build/logo/svg/logo.svg +0 -1
  84. package/src/logo/svg/flag-inverse.svg +0 -1
  85. package/src/logo/svg/flag-platform-white.svg +0 -1
  86. package/src/logo/svg/flag-platform.svg +0 -1
  87. package/src/logo/svg/flag.svg +0 -1
  88. package/src/logo/svg/logo-business-inverse.svg +0 -1
  89. package/src/logo/svg/logo-business.svg +0 -1
  90. package/src/logo/svg/logo-inverse.svg +0 -1
  91. package/src/logo/svg/logo-platform-white.svg +0 -1
  92. package/src/logo/svg/logo-platform.svg +0 -1
  93. package/src/logo/svg/logo.svg +0 -1
@@ -0,0 +1,298 @@
1
+ import { render, screen, mockMatchMedia } from '../test-utils';
2
+ import '@testing-library/jest-dom';
3
+ import TableCell, {
4
+ TableCellCurrency,
5
+ TableCellLeading,
6
+ TableCellStatus,
7
+ TableCellText,
8
+ } from './TableCell';
9
+
10
+ mockMatchMedia();
11
+
12
+ describe('TableCell Component', () => {
13
+ const cellContentMocks = {
14
+ leading: {
15
+ type: 'leading',
16
+ primaryText: 'Alice Johnson',
17
+ secondaryText: 'Frontend Developer',
18
+ avatar: {
19
+ profileName: 'Alice Johnson',
20
+ },
21
+ } satisfies TableCellLeading,
22
+ text: {
23
+ type: 'text',
24
+ text: 'JP, Tokyo',
25
+ } satisfies TableCellText,
26
+ currency: {
27
+ type: 'currency',
28
+ primaryCurrency: {
29
+ amount: 56789.01,
30
+ currency: 'jpy',
31
+ },
32
+ secondaryCurrency: {
33
+ amount: 51000.0,
34
+ currency: 'gbp',
35
+ },
36
+ } satisfies TableCellCurrency,
37
+ status: {
38
+ type: 'status',
39
+ primaryText: 'Overdue',
40
+ secondaryText: '6 days ago',
41
+ sentiment: 'pending',
42
+ } satisfies TableCellStatus,
43
+ custom: <div>Custom content</div>,
44
+ };
45
+
46
+ it('renders without content', () => {
47
+ render(<TableCell />);
48
+ expect(screen.getByRole('cell')).toBeInTheDocument();
49
+ });
50
+
51
+ it('applies `right` alignment class', () => {
52
+ render(<TableCell alignment="right" />);
53
+ expect(screen.getByRole('cell')).toHaveClass('np-table-cell--right');
54
+ });
55
+
56
+ it('applies reversed class for cell with image and `right` alignment', () => {
57
+ render(<TableCell cell={cellContentMocks.currency} alignment="right" />);
58
+ expect(screen.getByTestId('np-table-content')).toHaveClass('np-table-content--reversed');
59
+ });
60
+
61
+ it('applies custom class', () => {
62
+ render(<TableCell className="custom-class" />);
63
+ expect(screen.getByRole('cell')).toHaveClass('custom-class');
64
+ });
65
+
66
+ it('renders with spanned columns', () => {
67
+ render(<TableCell colSpan={2} />);
68
+ expect(screen.getByRole('cell')).toHaveAttribute('colSpan', '2');
69
+ });
70
+
71
+ it('renders with `children` as a content', () => {
72
+ render(<TableCell>{cellContentMocks.custom}</TableCell>);
73
+ expect(screen.getByText('Custom content')).toBeInTheDocument();
74
+ });
75
+
76
+ it('renders `text` cell type', () => {
77
+ render(<TableCell cell={{ ...cellContentMocks.text }} />);
78
+ expect(screen.getByText('JP, Tokyo')).toBeInTheDocument();
79
+ });
80
+
81
+ it('renders `text` cell type with error', () => {
82
+ render(<TableCell cell={{ ...cellContentMocks.text, status: 'error' }} />);
83
+ expect(screen.getByText('JP, Tokyo')).toHaveClass('np-table-content--error');
84
+ expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
85
+ });
86
+
87
+ it('renders `text` cell type with success', () => {
88
+ render(<TableCell cell={{ ...cellContentMocks.text, status: 'success' }} />);
89
+ expect(screen.getByText('JP, Tokyo')).toHaveClass('np-table-content--success');
90
+ expect(screen.getByTestId('check-icon')).toBeInTheDocument();
91
+ });
92
+
93
+ it('renders `leading` cell type with initials avatar when `profileName` is provided', () => {
94
+ render(<TableCell cell={{ ...cellContentMocks.leading }} />);
95
+ expect(screen.getByText('AJ')).toBeInTheDocument();
96
+ expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
97
+ expect(screen.getByText('Frontend Developer')).toBeInTheDocument();
98
+ });
99
+
100
+ it('renders `leading` cell type without media content when `profileName` is not provided', () => {
101
+ render(
102
+ <TableCell
103
+ cell={{
104
+ ...cellContentMocks.leading,
105
+ avatar: { profileName: undefined },
106
+ }}
107
+ />,
108
+ );
109
+ expect(screen.queryByText('AJ')).not.toBeInTheDocument();
110
+ expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
111
+ expect(screen.getByText('Frontend Developer')).toBeInTheDocument();
112
+ expect(screen.queryByTestId('np-table-content-media')).not.toBeInTheDocument();
113
+ });
114
+
115
+ it('renders `leading` cell without primary text if is not provided', () => {
116
+ render(
117
+ <TableCell
118
+ cell={{
119
+ ...cellContentMocks.leading,
120
+ primaryText: undefined,
121
+ }}
122
+ />,
123
+ );
124
+ expect(screen.getByText('AJ')).toBeInTheDocument();
125
+ expect(screen.queryByText('Alice Johnson')).not.toBeInTheDocument();
126
+ expect(screen.getByText('Frontend Developer')).toBeInTheDocument();
127
+ });
128
+
129
+ it('renders `leading` cell without secondary text if it is not provided', () => {
130
+ render(
131
+ <TableCell
132
+ cell={{
133
+ ...cellContentMocks.leading,
134
+ secondaryText: undefined,
135
+ }}
136
+ />,
137
+ );
138
+ expect(screen.getByText('AJ')).toBeInTheDocument();
139
+ expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
140
+ expect(screen.queryByText('Frontend Developer')).not.toBeInTheDocument();
141
+ });
142
+
143
+ it('applies an `error` class, renders an `alert-icon` for `leading` cell type with `error` status', () => {
144
+ render(
145
+ <TableCell
146
+ cell={{
147
+ ...cellContentMocks.leading,
148
+ status: 'error',
149
+ }}
150
+ />,
151
+ );
152
+ expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
153
+ expect(screen.getByText('Alice Johnson')).toHaveClass('np-table-content--error');
154
+ expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
155
+ });
156
+
157
+ it('applies a `success` class, renders an `check-icon` for `leading` cell type with `success` status', () => {
158
+ render(
159
+ <TableCell
160
+ cell={{
161
+ ...cellContentMocks.leading,
162
+ status: 'success',
163
+ }}
164
+ />,
165
+ );
166
+ expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
167
+ expect(screen.getByText('Alice Johnson')).toHaveClass('np-table-content--success');
168
+ expect(screen.getByTestId('check-icon')).toBeInTheDocument();
169
+ });
170
+
171
+ it('renders `currency` cell type', () => {
172
+ render(<TableCell cell={{ ...cellContentMocks.currency }} alignment="right" />);
173
+ expect(screen.getByTestId('np-table-content-media')).toBeInTheDocument();
174
+ expect(screen.getByText('56,789 JPY')).toBeInTheDocument();
175
+ expect(screen.getByText('51,000.00 GBP')).toBeInTheDocument();
176
+ });
177
+
178
+ it('renders `currency` cell without media content when currency amount of `primaryCurrency` is not provided', () => {
179
+ render(
180
+ <TableCell
181
+ cell={{
182
+ ...cellContentMocks.currency,
183
+ primaryCurrency: {
184
+ amount: cellContentMocks.currency.primaryCurrency.amount,
185
+ currency: '',
186
+ },
187
+ }}
188
+ alignment="right"
189
+ />,
190
+ );
191
+ expect(screen.queryByRole('presentation')).not.toBeInTheDocument();
192
+ expect(screen.getByText('56,789.01')).toBeInTheDocument();
193
+ expect(screen.getByText('51,000.00 GBP')).toBeInTheDocument();
194
+ expect(screen.queryByTestId('np-table-content-media')).not.toBeInTheDocument();
195
+ });
196
+
197
+ it('renders `currency` cell without primary currency when it is not provided', () => {
198
+ render(
199
+ <TableCell
200
+ cell={{
201
+ ...cellContentMocks.currency,
202
+ primaryCurrency: undefined,
203
+ }}
204
+ alignment="right"
205
+ />,
206
+ );
207
+ expect(screen.queryByText('56,789 JPY')).not.toBeInTheDocument();
208
+ expect(screen.getByText('51,000.00 GBP')).toBeInTheDocument();
209
+ });
210
+
211
+ it('renders `currency` cell without secondary currency when it is not provided', () => {
212
+ render(
213
+ <TableCell
214
+ cell={{
215
+ ...cellContentMocks.currency,
216
+ secondaryCurrency: undefined,
217
+ }}
218
+ alignment="right"
219
+ />,
220
+ );
221
+ expect(screen.getByText('56,789 JPY')).toBeInTheDocument();
222
+ expect(screen.queryByText('51,000.00 GBP')).not.toBeInTheDocument();
223
+ });
224
+
225
+ it('renders `currency` cell with error content', () => {
226
+ render(
227
+ <TableCell
228
+ cell={{
229
+ ...cellContentMocks.currency,
230
+ status: 'error',
231
+ }}
232
+ alignment="right"
233
+ />,
234
+ );
235
+ expect(screen.getByText('56,789 JPY')).toBeInTheDocument();
236
+ expect(screen.getByText('56,789 JPY')).toHaveClass('np-table-content--error');
237
+ expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
238
+ });
239
+
240
+ it('renders `currency` cell with `success` content', () => {
241
+ render(
242
+ <TableCell
243
+ cell={{
244
+ ...cellContentMocks.currency,
245
+ status: 'success',
246
+ }}
247
+ alignment="right"
248
+ />,
249
+ );
250
+ expect(screen.getByText('56,789 JPY')).toBeInTheDocument();
251
+ expect(screen.getByText('56,789 JPY')).toHaveClass('np-table-content--success');
252
+ expect(screen.getByTestId('check-icon')).toBeInTheDocument();
253
+ });
254
+
255
+ it('renders a `status` cell type content', () => {
256
+ render(<TableCell cell={{ ...cellContentMocks.status }} />);
257
+ expect(screen.getByTestId('status-icon')).toBeInTheDocument();
258
+ expect(screen.getByTestId('status-icon')).toHaveClass('pending');
259
+ expect(screen.getByText('Overdue')).toBeInTheDocument();
260
+ expect(screen.getByText('6 days ago')).toBeInTheDocument();
261
+ });
262
+
263
+ it('renders an `info-icon` for `status` cell type if `sentiment` is not provided', () => {
264
+ render(
265
+ <TableCell
266
+ cell={{
267
+ ...cellContentMocks.status,
268
+ sentiment: undefined,
269
+ }}
270
+ />,
271
+ );
272
+ expect(screen.getByTestId('info-icon')).toBeInTheDocument();
273
+ });
274
+
275
+ it('renders `status` cell type without `primary` text if it is not provided', () => {
276
+ render(
277
+ <TableCell
278
+ cell={{
279
+ ...cellContentMocks.status,
280
+ primaryText: undefined,
281
+ }}
282
+ />,
283
+ );
284
+ expect(screen.queryByText('Overdue')).not.toBeInTheDocument();
285
+ });
286
+
287
+ it('renders `status` cell type without `secondary` text if it is not provided', () => {
288
+ render(
289
+ <TableCell
290
+ cell={{
291
+ ...cellContentMocks.status,
292
+ secondaryText: undefined,
293
+ }}
294
+ />,
295
+ );
296
+ expect(screen.queryByText('6 days ago')).not.toBeInTheDocument();
297
+ });
298
+ });
@@ -0,0 +1,149 @@
1
+ import TableStatusText from './TableStatusText';
2
+ import StatusIcon from '../statusIcon';
3
+ import { Flag } from '@wise/art';
4
+ import React from 'react';
5
+ import { clsx } from 'clsx';
6
+ import Body from '../body';
7
+ import Money, { MoneyProps } from '../money';
8
+ import AvatarView from '../avatarView';
9
+
10
+ interface TableCellTypeProp {
11
+ type: 'leading' | 'text' | 'currency' | 'status';
12
+ }
13
+
14
+ // `Leading` and `Status` cell types have 2 text fields: `primaryText` and `secondaryText`
15
+ interface TableCellTextProps {
16
+ primaryText?: string;
17
+ secondaryText?: string;
18
+ }
19
+
20
+ // `Leading`, `Text` and `Currency` cells' types can have a status indicator with `error` or `success` values
21
+ interface TableCellStatusProp {
22
+ status?: 'error' | 'success';
23
+ }
24
+
25
+ export interface TableCellLeading
26
+ extends TableCellTypeProp,
27
+ TableCellTextProps,
28
+ TableCellStatusProp {
29
+ avatar?: {
30
+ src?: string;
31
+ profileName?: string | null;
32
+ };
33
+ }
34
+
35
+ export interface TableCellText extends TableCellTypeProp, TableCellStatusProp {
36
+ text?: string;
37
+ }
38
+
39
+ export interface TableCellCurrency extends TableCellTypeProp, TableCellStatusProp {
40
+ primaryCurrency?: MoneyProps;
41
+ secondaryCurrency?: MoneyProps;
42
+ }
43
+
44
+ export interface TableCellStatus extends TableCellTypeProp, TableCellTextProps {
45
+ sentiment?: 'negative' | 'neutral' | 'positive' | 'warning' | 'pending';
46
+ }
47
+
48
+ export interface TableCellType {
49
+ cell?: TableCellLeading & TableCellText & TableCellCurrency & TableCellStatus;
50
+ alignment?: 'left' | 'right';
51
+ }
52
+
53
+ // These properties should be exported only on the lib level to prevent visual issues because of incorrect usage.
54
+ export interface TableCellProps extends TableCellType {
55
+ className?: string;
56
+ colSpan?: number;
57
+ children?: React.ReactNode;
58
+ }
59
+
60
+ const TableCell = ({ cell, alignment = 'left', className, colSpan, children }: TableCellProps) => {
61
+ const getContentMedia = () => {
62
+ let mediaContent = null;
63
+
64
+ if (cell?.type === 'leading' && (cell?.avatar?.src || cell?.avatar?.profileName)) {
65
+ mediaContent = (
66
+ <AvatarView profileName={cell?.avatar?.profileName} size={40} imgSrc={cell?.avatar?.src} />
67
+ );
68
+ }
69
+
70
+ if (cell?.type === 'currency' && cell?.primaryCurrency?.currency) {
71
+ mediaContent = (
72
+ <Flag code={cell?.primaryCurrency?.currency?.toLowerCase()} intrinsicSize={24} />
73
+ );
74
+ }
75
+
76
+ if (cell?.type === 'status') {
77
+ mediaContent = <StatusIcon size={24} sentiment={cell?.sentiment ?? 'neutral'} />;
78
+ }
79
+
80
+ if (mediaContent) {
81
+ return (
82
+ <div
83
+ aria-hidden="true"
84
+ className="np-table-content-media"
85
+ data-testid="np-table-content-media"
86
+ >
87
+ {mediaContent}
88
+ </div>
89
+ );
90
+ }
91
+ };
92
+
93
+ const formatCurrencyValue = (currency?: MoneyProps) => {
94
+ if (currency) {
95
+ return <Money amount={currency.amount} currency={currency.currency} alwaysShowDecimals />;
96
+ }
97
+
98
+ return '';
99
+ };
100
+
101
+ return (
102
+ <td
103
+ className={clsx(
104
+ 'np-table-cell',
105
+ cell?.type ? `np-table-cell--${cell?.type}` : '',
106
+ `np-table-cell--${alignment}`,
107
+ className,
108
+ )}
109
+ colSpan={colSpan}
110
+ >
111
+ {cell?.type === 'text' && cell?.text && (
112
+ <TableStatusText text={cell?.text} status={cell?.status} />
113
+ )}
114
+ {cell?.type && ['leading', 'currency', 'status'].includes(cell?.type) && (
115
+ <div
116
+ className={clsx('np-table-content', {
117
+ 'np-table-content--reversed': alignment === 'right',
118
+ })}
119
+ data-testid="np-table-content"
120
+ >
121
+ {getContentMedia()}
122
+ <div className="np-table-content-body">
123
+ {(cell?.primaryCurrency ?? cell?.primaryText) && (
124
+ <TableStatusText
125
+ text={
126
+ cell?.type === 'currency'
127
+ ? formatCurrencyValue(cell?.primaryCurrency)
128
+ : (cell?.primaryText ?? '')
129
+ }
130
+ status={cell?.type !== 'status' ? cell?.status : undefined}
131
+ typography="default-bold"
132
+ />
133
+ )}
134
+ {(cell?.secondaryCurrency ?? cell?.secondaryText) && (
135
+ <Body>
136
+ {cell?.type === 'currency'
137
+ ? formatCurrencyValue(cell?.secondaryCurrency)
138
+ : cell?.secondaryText}
139
+ </Body>
140
+ )}
141
+ </div>
142
+ </div>
143
+ )}
144
+ {children}
145
+ </td>
146
+ );
147
+ };
148
+
149
+ export default TableCell;
@@ -0,0 +1,50 @@
1
+ import { render, screen } from '../test-utils';
2
+ import TableHeader, { TableHeaderProps } from './TableHeader';
3
+
4
+ describe('TableHeader Component', () => {
5
+ const renderComponent = (props: Partial<TableHeaderProps> = {}) => {
6
+ const defaultProps = {
7
+ header: '',
8
+ } satisfies TableHeaderProps;
9
+ return render(<TableHeader {...defaultProps} {...props} />);
10
+ };
11
+
12
+ it('should render header', () => {
13
+ const { container } = renderComponent();
14
+ expect(container).toBeInTheDocument();
15
+ });
16
+
17
+ it('should render header text when provided', () => {
18
+ const headerText = 'Test Header';
19
+ renderComponent({ header: headerText });
20
+ expect(screen.getByText(headerText)).toBeInTheDocument();
21
+ });
22
+
23
+ it('should apply custom className', () => {
24
+ const className = 'custom-class';
25
+ renderComponent({ className });
26
+ expect(screen.getByRole('columnheader')).toHaveClass(className);
27
+ });
28
+
29
+ it('should align text to the right when alignment is set to right', () => {
30
+ renderComponent({ alignment: 'right' });
31
+ expect(screen.getByRole('columnheader')).toHaveClass('np-table-header--right');
32
+ });
33
+
34
+ it("should show error class when status equals 'error'", () => {
35
+ renderComponent({ status: 'error' });
36
+ expect(screen.getByRole('columnheader')).toHaveClass('np-table-header--error');
37
+ });
38
+
39
+ it('should render empty header content when header is not provided', () => {
40
+ renderComponent();
41
+ expect(screen.getByTestId('np-table-empty-header').innerHTML).toBe('&nbsp;');
42
+ });
43
+
44
+ it('should render header with error status when status is `error`', () => {
45
+ const headerText = 'Test Header';
46
+ renderComponent({ header: headerText, status: 'error' });
47
+ expect(screen.getByText(headerText)).toHaveClass('np-table-content--error');
48
+ expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
49
+ });
50
+ });
@@ -0,0 +1,74 @@
1
+ import { useIntl } from 'react-intl';
2
+ import messages from './Table.messages';
3
+ import TableStatusText from './TableStatusText';
4
+ import { clsx } from 'clsx';
5
+
6
+ export interface TableHeaderType {
7
+ header?: string;
8
+ className?: string;
9
+ alignment?: 'left' | 'right';
10
+ status?: 'error';
11
+ width?: string;
12
+ }
13
+
14
+ export interface TableHeaderProps extends TableHeaderType {
15
+ isActionHeader?: boolean;
16
+ }
17
+
18
+ const TableHeader = ({
19
+ header,
20
+ className,
21
+ alignment = 'left',
22
+ status,
23
+ width,
24
+ isActionHeader = false,
25
+ }: TableHeaderProps) => {
26
+ const { formatMessage } = useIntl();
27
+
28
+ const getHeaderContent = () => {
29
+ if (isActionHeader) {
30
+ // `Action` header doesn't have visual text content, but it has the header, which is visible for screen readers only
31
+ return (
32
+ <TableStatusText
33
+ text={formatMessage(messages.actionHeader)}
34
+ className={`np-table-header-content${isActionHeader ? ' sr-only' : ''}`}
35
+ />
36
+ );
37
+ }
38
+
39
+ if (header) {
40
+ return (
41
+ <TableStatusText
42
+ text={header}
43
+ className={`np-table-header-content${isActionHeader ? ' sr-only' : ''}`}
44
+ status={status}
45
+ typography="default-bold"
46
+ />
47
+ );
48
+ }
49
+
50
+ // If headers are empty, we still should render empty headers to keep visual consistency
51
+ return (
52
+ <div
53
+ className="np-table-header-content np-text-body-default-bold"
54
+ data-testid="np-table-empty-header"
55
+ >
56
+ &nbsp;
57
+ </div>
58
+ );
59
+ };
60
+
61
+ return (
62
+ <th
63
+ className={clsx('np-table-header', className, `np-table-header--${alignment}`, {
64
+ 'np-table-header--error': status === 'error',
65
+ 'np-table-header--action': isActionHeader,
66
+ })}
67
+ style={{ minWidth: width, width }}
68
+ >
69
+ {getHeaderContent()}
70
+ </th>
71
+ );
72
+ };
73
+
74
+ export default TableHeader;
@@ -0,0 +1,112 @@
1
+ import { render, screen } from '../test-utils';
2
+ import TableRow, { TableRowType, TableRowClickableType } from './TableRow';
3
+ import { userEvent } from '@testing-library/user-event';
4
+
5
+ describe('TableRow Component', () => {
6
+ const mockData = {
7
+ cells: [
8
+ {
9
+ cell: {
10
+ type: 'text',
11
+ text: 'Cell content 1',
12
+ },
13
+ },
14
+ {
15
+ cell: {
16
+ type: 'text',
17
+ text: 'Cell content 2',
18
+ },
19
+ },
20
+ ],
21
+ } satisfies TableRowType;
22
+
23
+ const mockDataClickable = {
24
+ id: 1,
25
+ cells: mockData.cells,
26
+ } satisfies TableRowClickableType;
27
+
28
+ const handleClick = jest.fn();
29
+
30
+ it('renders row', () => {
31
+ render(<TableRow />);
32
+ expect(screen.getByTestId('np-table-row')).toBeInTheDocument();
33
+ });
34
+
35
+ it('renders `children` when data is not provided', () => {
36
+ render(
37
+ <TableRow>
38
+ <td>Cell text</td>
39
+ </TableRow>,
40
+ );
41
+ expect(screen.getByText('Cell text')).toBeInTheDocument();
42
+ });
43
+
44
+ it('renders cells when data is provided', () => {
45
+ render(<TableRow rowData={mockData} />);
46
+ expect(screen.getByText('Cell content 1')).toBeInTheDocument();
47
+ expect(screen.getByText('Cell content 2')).toBeInTheDocument();
48
+ });
49
+
50
+ it('renders correct number of cells', () => {
51
+ render(<TableRow rowData={mockData} />);
52
+ expect(screen.getAllByRole('cell')).toHaveLength(mockData.cells.length);
53
+ });
54
+
55
+ it('renders correct number of cells with chevron when clickable', () => {
56
+ render(<TableRow rowData={mockDataClickable} onRowClick={handleClick} />);
57
+ expect(screen.getAllByRole('cell')).toHaveLength(mockDataClickable.cells.length + 1);
58
+ });
59
+
60
+ it('renders a separator row when `hasSeparator` is passed', () => {
61
+ render(<TableRow rowData={mockData} hasSeparator />);
62
+ expect(screen.getAllByTestId('np-table-row')).toHaveLength(1);
63
+ expect(screen.getAllByTestId('np-table-row--separator')).toHaveLength(1);
64
+ });
65
+
66
+ it('renders correct `colSpan` for separator row', () => {
67
+ render(<TableRow rowData={mockData} hasSeparator />);
68
+ const separatorCell = screen.getByTestId('np-table-cell--cosmetic');
69
+ expect(separatorCell).toHaveAttribute('colSpan', mockData.cells.length.toString());
70
+ });
71
+
72
+ it('renders correct `colSpan` for separator row with clickable row', () => {
73
+ render(<TableRow rowData={mockDataClickable} hasSeparator onRowClick={handleClick} />);
74
+ const separatorCell = screen.getByTestId('np-table-cell--cosmetic');
75
+ expect(separatorCell).toHaveAttribute(
76
+ 'colSpan',
77
+ (mockDataClickable.cells.length + 1).toString(),
78
+ );
79
+ });
80
+
81
+ it('does not render separator row when `hasSeparator` is not provided', () => {
82
+ render(<TableRow rowData={mockData} />);
83
+ expect(screen.queryByTestId('np-table-row--separator')).not.toBeInTheDocument();
84
+ });
85
+
86
+ it('does not call `onRowClick` when row is not clickable', async () => {
87
+ render(<TableRow rowData={mockData} />);
88
+ await userEvent.click(screen.getByTestId('np-table-row'));
89
+ expect(handleClick).not.toHaveBeenCalled();
90
+ });
91
+
92
+ it('applies the clickable class when row is clickable', () => {
93
+ render(<TableRow rowData={mockDataClickable} onRowClick={handleClick} />);
94
+ expect(screen.getAllByTestId('np-table-row')[0]).toHaveClass('np-table-row--clickable');
95
+ });
96
+
97
+ it('calls `onRowClick` when row is clicked for clickable row', async () => {
98
+ render(<TableRow rowData={mockDataClickable} onRowClick={handleClick} />);
99
+ await userEvent.click(screen.getByTestId('np-table-row'));
100
+ expect(handleClick).toHaveBeenCalledWith(mockDataClickable);
101
+ });
102
+
103
+ it('renders a chevron icon when row is clickable', () => {
104
+ render(<TableRow rowData={mockDataClickable} onRowClick={handleClick} />);
105
+ expect(screen.getByTestId('chevron-up-icon')).toBeInTheDocument();
106
+ });
107
+
108
+ it('does not render a chevron icon when row is not clickable', () => {
109
+ render(<TableRow rowData={mockData} />);
110
+ expect(screen.queryByTestId('chevron-up-icon')).not.toBeInTheDocument();
111
+ });
112
+ });