@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.
- package/build/alert/Alert.js +2 -9
- package/build/alert/Alert.js.map +1 -1
- package/build/alert/Alert.mjs +2 -9
- package/build/alert/Alert.mjs.map +1 -1
- package/build/i18n/en.json +5 -0
- package/build/i18n/en.json.js +5 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +5 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/logo/Logo.js +11 -131
- package/build/logo/Logo.js.map +1 -1
- package/build/logo/Logo.mjs +1 -121
- package/build/logo/Logo.mjs.map +1 -1
- package/build/logo/logo-assets.js +134 -0
- package/build/logo/logo-assets.js.map +1 -0
- package/build/logo/logo-assets.mjs +125 -0
- package/build/logo/logo-assets.mjs.map +1 -0
- package/build/main.css +274 -0
- package/build/money/Money.js +5 -2
- package/build/money/Money.js.map +1 -1
- package/build/money/Money.mjs +5 -2
- package/build/money/Money.mjs.map +1 -1
- package/build/styles/main.css +274 -0
- package/build/styles/table/Table.css +274 -0
- package/build/types/alert/Alert.d.ts +1 -5
- package/build/types/alert/Alert.d.ts.map +1 -1
- package/build/types/logo/Logo.d.ts.map +1 -1
- package/build/types/logo/logo-assets.d.ts +10 -0
- package/build/types/logo/logo-assets.d.ts.map +1 -0
- package/build/types/money/Money.d.ts +2 -1
- package/build/types/money/Money.d.ts.map +1 -1
- package/build/types/table/Table.d.ts +23 -0
- package/build/types/table/Table.d.ts.map +1 -0
- package/build/types/table/Table.messages.d.ts +24 -0
- package/build/types/table/Table.messages.d.ts.map +1 -0
- package/build/types/table/TableCell.d.ts +40 -0
- package/build/types/table/TableCell.d.ts.map +1 -0
- package/build/types/table/TableHeader.d.ts +13 -0
- package/build/types/table/TableHeader.d.ts.map +1 -0
- package/build/types/table/TableRow.d.ts +17 -0
- package/build/types/table/TableRow.d.ts.map +1 -0
- package/build/types/table/TableStatusText.d.ts +10 -0
- package/build/types/table/TableStatusText.d.ts.map +1 -0
- package/build/types/table/index.d.ts +6 -0
- package/build/types/table/index.d.ts.map +1 -0
- package/build/types/test-utils/index.d.ts +10 -0
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/package.json +3 -4
- package/src/alert/Alert.spec.story.tsx +0 -82
- package/src/alert/Alert.spec.tsx +0 -30
- package/src/alert/Alert.tsx +51 -67
- package/src/i18n/en.json +5 -0
- package/src/logo/Logo.tsx +10 -8
- package/src/logo/__snapshots__/Logo.spec.tsx.snap +16 -16
- package/src/logo/logo-assets.tsx +137 -0
- package/src/main.css +274 -0
- package/src/main.less +1 -0
- package/src/money/Money.tsx +9 -2
- package/src/table/Table.css +274 -0
- package/src/table/Table.less +334 -0
- package/src/table/Table.messages.ts +24 -0
- package/src/table/Table.spec.tsx +82 -0
- package/src/table/Table.story.tsx +356 -0
- package/src/table/Table.tsx +167 -0
- package/src/table/TableCell.spec.tsx +298 -0
- package/src/table/TableCell.tsx +149 -0
- package/src/table/TableHeader.spec.tsx +50 -0
- package/src/table/TableHeader.tsx +74 -0
- package/src/table/TableRow.spec.tsx +112 -0
- package/src/table/TableRow.tsx +70 -0
- package/src/table/TableStatusText.spec.tsx +53 -0
- package/src/table/TableStatusText.tsx +40 -0
- package/src/table/index.ts +11 -0
- package/build/logo/svg/flag-inverse.svg +0 -1
- package/build/logo/svg/flag-platform-white.svg +0 -1
- package/build/logo/svg/flag-platform.svg +0 -1
- package/build/logo/svg/flag.svg +0 -1
- package/build/logo/svg/logo-business-inverse.svg +0 -1
- package/build/logo/svg/logo-business.svg +0 -1
- package/build/logo/svg/logo-inverse.svg +0 -1
- package/build/logo/svg/logo-platform-white.svg +0 -1
- package/build/logo/svg/logo-platform.svg +0 -1
- package/build/logo/svg/logo.svg +0 -1
- package/src/logo/svg/flag-inverse.svg +0 -1
- package/src/logo/svg/flag-platform-white.svg +0 -1
- package/src/logo/svg/flag-platform.svg +0 -1
- package/src/logo/svg/flag.svg +0 -1
- package/src/logo/svg/logo-business-inverse.svg +0 -1
- package/src/logo/svg/logo-business.svg +0 -1
- package/src/logo/svg/logo-inverse.svg +0 -1
- package/src/logo/svg/logo-platform-white.svg +0 -1
- package/src/logo/svg/logo-platform.svg +0 -1
- 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(' ');
|
|
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
|
+
|
|
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
|
+
});
|