@transferwise/components 0.0.0-experimental-d1715ff → 0.0.0-experimental-f81f868

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 +215 -0
  7. package/build/styles/main.css +215 -0
  8. package/build/styles/table/Table.css +215 -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 +215 -0
  26. package/src/main.less +1 -0
  27. package/src/table/Table.css +215 -0
  28. package/src/table/Table.less +261 -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,261 @@
1
+ .np-table {
2
+ @table-background-color: var(--color-background-neutral);
3
+ @table-border-width: var(--size-8);
4
+ @table-border-radius: var(--radius-medium);
5
+ @table-inner-border-radius: 10px;
6
+
7
+ @table-spacing-sm: var(--size-4);
8
+ @table-spacing-md: var(--size-8);
9
+ @table-spacing-lg: var(--size-12);
10
+ @table-spacing-xl: var(--size-16);
11
+
12
+ @header-min-width: 160px;
13
+ @header-min-width-loading: calc(@header-min-width * 2);
14
+
15
+ width: 100%;
16
+ background-color: transparent;
17
+
18
+ &-container {
19
+ padding: @table-spacing-md;
20
+ background-color: @table-background-color;
21
+ border-radius: @table-border-radius;
22
+
23
+ &--loading {
24
+ .np-table-header {
25
+ min-width: @header-min-width-loading;
26
+ }
27
+ }
28
+
29
+ &--center {
30
+ margin-right: auto;
31
+ margin-left: auto;
32
+ }
33
+
34
+ &--full-width {
35
+ width: 100%;
36
+ }
37
+ }
38
+
39
+ &-inner-container {
40
+ background-image:
41
+ linear-gradient(to right, var(--color-contrast-overlay), var(--color-contrast-overlay)),
42
+ linear-gradient(to right, var(--color-contrast-overlay), var(--color-contrast-overlay)),
43
+ linear-gradient(to right, rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0)),
44
+ linear-gradient(to left, rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0));
45
+ background-position: left center, right center, left center, right center;
46
+ background-repeat: no-repeat;
47
+ background-color: var(--color-contrast-overlay);
48
+ background-size: 15px 100%, 15px 100%, 15px 100%, 15px 100%;
49
+ background-attachment: local, local, scroll, scroll;
50
+ overflow-x: auto;
51
+ border-radius: @table-inner-border-radius;
52
+
53
+ .np-theme-personal--dark & {
54
+ background-image:
55
+ linear-gradient(to right, var(--color-contrast-overlay), var(--color-contrast-overlay)),
56
+ linear-gradient(to right, var(--color-contrast-overlay), var(--color-contrast-overlay)),
57
+ linear-gradient(to right, rgba(255, 255, 255, 0.1), rgba(0, 0, 0, 0)),
58
+ linear-gradient(to left, rgba(255, 255, 255, 0.1), rgba(0, 0, 0, 0));
59
+ }
60
+ }
61
+
62
+ &-row {
63
+ &:last-child {
64
+ .np-table-cell {
65
+ &:first-child {
66
+ border-bottom-left-radius: @table-inner-border-radius;
67
+ }
68
+ &:last-child {
69
+ border-bottom-right-radius: @table-inner-border-radius;
70
+ }
71
+ }
72
+ }
73
+
74
+ &--clickable:hover {
75
+ .np-table-cell {
76
+ background-color: var(--color-background-screen-hover);
77
+ cursor: pointer;
78
+ }
79
+ }
80
+
81
+ &--separator {
82
+ .np-table-cell {
83
+ padding: 0;
84
+ }
85
+ }
86
+ }
87
+
88
+ &-header,
89
+ &-cell {
90
+ padding: 0;
91
+
92
+ &:first-child {
93
+ padding-left: @table-spacing-xl;
94
+
95
+ .np-table-header-content,
96
+ .np-table-cell-content {
97
+ padding-left: 0;
98
+ }
99
+ }
100
+
101
+ &:last-child {
102
+ padding-right: @table-spacing-xl;
103
+
104
+ .np-table-header-content,
105
+ .np-table-cell-content {
106
+ padding-right: 0;
107
+ }
108
+ }
109
+
110
+ .np-text-body-large-bold {
111
+ font-size: var(--font-size-14);
112
+ }
113
+ }
114
+
115
+ &-header.np-table-header--right,
116
+ &-cell.np-table-cell--right {
117
+ padding-right: calc(@table-spacing-md + @table-spacing-lg);
118
+ > .np-text-body-default {
119
+ text-align: right;
120
+ }
121
+ }
122
+
123
+ &-header.np-table-header--right .np-table-header-content,
124
+ &-cell.np-table-cell--right .np-table-content {
125
+ justify-content: end;
126
+ }
127
+
128
+ &-header-content,
129
+ &-content {
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: start;
133
+ }
134
+
135
+ &-header {
136
+ min-width: @header-min-width;
137
+ padding-right: @table-spacing-md;
138
+ padding-bottom: @table-spacing-md;
139
+ padding-left: @table-spacing-md;
140
+ background-color: var(--color-background-neutral);
141
+ color: var(--color-content-primary);
142
+
143
+ &-content {
144
+ padding-top: 5px;
145
+ padding-bottom: 5px;
146
+ white-space: nowrap;
147
+ }
148
+
149
+ &--has-error {
150
+ color: var(--color-sentiment-negative);
151
+ }
152
+ }
153
+
154
+ &-cell {
155
+ padding: @table-spacing-xl @table-spacing-md;
156
+
157
+ &--primary {
158
+ min-width: 200px;
159
+ }
160
+
161
+ &--currency {
162
+ .np-text-body-default {
163
+ white-space: nowrap;
164
+ }
165
+ }
166
+
167
+ // TODO: In next iterations this block of code should be removed after we'll have `24px` status icons
168
+ &--status {
169
+ .status-circle {
170
+ width: 24px;
171
+ height: 24px;
172
+
173
+ .status-icon > svg {
174
+ width: 21px;
175
+ height: 21px;
176
+ }
177
+ }
178
+ }
179
+
180
+ .tw-chevron {
181
+ margin-left: @table-spacing-md;
182
+ }
183
+
184
+ &-separator {
185
+ margin-top: @table-spacing-sm;
186
+ margin-bottom: @table-spacing-sm;
187
+ height: 1px;
188
+ padding: 0;
189
+ background-color: @table-background-color;
190
+ }
191
+
192
+ .np-text-body-default {
193
+ line-height: var(--line-height-22);
194
+ }
195
+
196
+ .np-text-body-large-bold {
197
+ display: flex;
198
+ align-items: center;
199
+ color: var(--color-content-primary);
200
+ white-space: nowrap;
201
+ }
202
+
203
+ .np-table-content--success {
204
+ color: var(--color-sentiment-positive);
205
+ }
206
+
207
+ .np-table-content--error {
208
+ color: var(--color-sentiment-negative);
209
+ }
210
+
211
+ .tw-loader {
212
+ // The loading area of the table should equals to 5 rows of the table for huge screens and 3 rows for small screens
213
+ margin: 150px auto;
214
+ @media (--screen-400-zoom) {
215
+ margin-top: 70px;
216
+ margin-bottom: 70px;
217
+ }
218
+ }
219
+ }
220
+
221
+ &-content {
222
+ gap: @table-spacing-lg;
223
+
224
+ &--success,
225
+ &--error {
226
+ gap: @table-spacing-sm;
227
+
228
+ .np-table-content-text {
229
+ line-height: 155%;
230
+ }
231
+ }
232
+
233
+ &-media {
234
+ flex-shrink: 0;
235
+ }
236
+
237
+ &-body {
238
+ display: flex;
239
+ flex-direction: column;
240
+ font-size: var(--font-size-12);
241
+ color: var(--color-content-tertiary);
242
+ }
243
+
244
+ &--reversed {
245
+ flex-direction: row-reverse;
246
+
247
+ .np-table-content-body {
248
+ align-items: end;
249
+ }
250
+ }
251
+ }
252
+
253
+ &-empty-data {
254
+ display: flex;
255
+ align-items: center;
256
+
257
+ .status-circle {
258
+ margin-right: @table-spacing-lg;
259
+ }
260
+ }
261
+ }
@@ -0,0 +1,12 @@
1
+ import { defineMessages } from 'react-intl';
2
+
3
+ export default defineMessages({
4
+ refreshPage: {
5
+ id: 'neptune.Table.refreshPage',
6
+ defaultMessage: 'Refresh page',
7
+ },
8
+ emptyData: {
9
+ id: 'neptune.Table.emptyData',
10
+ defaultMessage: 'No results found',
11
+ },
12
+ });
@@ -0,0 +1,87 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { userEvent } from '@testing-library/user-event';
4
+ import '@testing-library/jest-dom';
5
+ import { IntlProvider } from 'react-intl';
6
+ import Table, { TableProps } from './Table';
7
+ import { mockMatchMedia } from '../test-utils';
8
+
9
+ mockMatchMedia();
10
+
11
+ const renderWithIntl = (component: React.ReactNode) => {
12
+ return render(<IntlProvider locale="en">{component}</IntlProvider>);
13
+ };
14
+
15
+ describe('Table Component', () => {
16
+ const defaultProps: TableProps = {
17
+ data: {
18
+ headers: [{ header: 'Header 1' }, { header: 'Header 2' }],
19
+ content: [
20
+ {
21
+ rowContent: [
22
+ { content: { text: 'Cell content 1' } },
23
+ { content: { text: 'Cell content 2' } },
24
+ ],
25
+ },
26
+ {
27
+ rowContent: [
28
+ { content: { text: 'Cell content 3' } },
29
+ { content: { text: 'Cell content 4' } },
30
+ ],
31
+ },
32
+ ],
33
+ onRowClick: jest.fn(),
34
+ },
35
+ loading: false,
36
+ className: '',
37
+ fullWidth: true,
38
+ error: undefined,
39
+ onRetry: jest.fn(),
40
+ };
41
+
42
+ it('renders table headers and content', () => {
43
+ renderWithIntl(<Table {...defaultProps} />);
44
+ expect(screen.getByText('Header 1')).toBeInTheDocument();
45
+ expect(screen.getByText('Header 2')).toBeInTheDocument();
46
+ expect(screen.getByText('Cell content 1')).toBeInTheDocument();
47
+ expect(screen.getByText('Cell content 2')).toBeInTheDocument();
48
+ expect(screen.getByText('Cell content 3')).toBeInTheDocument();
49
+ expect(screen.getByText('Cell content 4')).toBeInTheDocument();
50
+ });
51
+
52
+ it('renders loading state', () => {
53
+ renderWithIntl(<Table {...defaultProps} loading />);
54
+ expect(screen.getAllByTestId('np-table-loader')).toHaveLength(1);
55
+ });
56
+
57
+ it('renders empty data message', () => {
58
+ renderWithIntl(<Table {...defaultProps} data={{ headers: [], content: [] }} />);
59
+ expect(screen.getAllByTestId('np-table-empty-data')).toHaveLength(1);
60
+ });
61
+
62
+ it('renders error message', () => {
63
+ const errorProps: TableProps = {
64
+ ...defaultProps,
65
+ error: { message: 'Error occurred', action: { text: 'Retry' } },
66
+ };
67
+ renderWithIntl(<Table {...errorProps} />);
68
+ expect(screen.getByText('Error occurred')).toBeInTheDocument();
69
+ expect(screen.getByText('Retry')).toBeInTheDocument();
70
+ });
71
+
72
+ it('renders with custom className', () => {
73
+ renderWithIntl(<Table {...defaultProps} className="custom-class" />);
74
+ expect(screen.getByTestId('np-table-container')).toHaveClass('custom-class');
75
+ });
76
+
77
+ it('renders with fullWidth set to false', () => {
78
+ renderWithIntl(<Table {...defaultProps} fullWidth={false} />);
79
+ expect(screen.getByTestId('np-table-container')).toHaveClass('np-table-container--center');
80
+ });
81
+
82
+ it('calls onRowClick when a row is clicked', async () => {
83
+ renderWithIntl(<Table {...defaultProps} />);
84
+ await userEvent.click(screen.getByText('Cell content 1'));
85
+ expect(defaultProps.data.onRowClick).toHaveBeenCalled();
86
+ });
87
+ });
@@ -0,0 +1,352 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import Table from './Table';
3
+ import { TableHeaderType } from './TableHeader';
4
+ import { lorem1000 } from '../test-utils';
5
+ import { TableRowType, TableRowClickableType } from './TableRow';
6
+
7
+ export default {
8
+ component: Table,
9
+ title: 'Option/Table',
10
+ tags: ['autodocs'],
11
+ } satisfies Meta<typeof Table>;
12
+
13
+ type Story = StoryObj<typeof Table>;
14
+
15
+ const getLoremIpsumWords = (count: number, hasWordsOnly = false) => {
16
+ const loremIpsumWords = hasWordsOnly ? lorem1000.replace(/[^a-z\s]/gi, '') : lorem1000;
17
+ return loremIpsumWords.split(' ').slice(0, count).join(' ');
18
+ };
19
+
20
+ const sampleHeading = 'Header sample';
21
+ const loremDescription = 'Description';
22
+ const lorem2 = getLoremIpsumWords(2, true);
23
+ const lorem5 = getLoremIpsumWords(5);
24
+
25
+ const tableData = {
26
+ headers: [
27
+ {
28
+ header: sampleHeading,
29
+ },
30
+ {
31
+ header: sampleHeading,
32
+ },
33
+ {
34
+ header: sampleHeading,
35
+ className: 'np-table-header--custom-class',
36
+ },
37
+ {
38
+ header: sampleHeading,
39
+ alignment: 'right',
40
+ },
41
+ {
42
+ header: sampleHeading,
43
+ hasError: true,
44
+ },
45
+ ] satisfies TableHeaderType[],
46
+ content: [
47
+ {
48
+ id: 0,
49
+ rowContent: [
50
+ {
51
+ type: 'leading',
52
+ content: {
53
+ primaryText: lorem2,
54
+ secondaryText: loremDescription,
55
+ initials: 'PT',
56
+ },
57
+ },
58
+ {
59
+ content: { text: lorem2 },
60
+ },
61
+ {
62
+ content: { text: lorem2 },
63
+ },
64
+ {
65
+ type: 'currency',
66
+ content: {
67
+ primaryCurrency: {
68
+ value: '12345.67',
69
+ currency: 'eur',
70
+ },
71
+ secondaryCurrency: {
72
+ value: '11000.00',
73
+ currency: 'gbp',
74
+ },
75
+ },
76
+ alignment: 'right',
77
+ },
78
+ {
79
+ type: 'status',
80
+ content: {
81
+ primaryText: lorem2,
82
+ secondaryText: loremDescription,
83
+ sentiment: 'negative',
84
+ },
85
+ },
86
+ ],
87
+ },
88
+ {
89
+ id: 1,
90
+ rowContent: [
91
+ {
92
+ type: 'leading',
93
+ content: {
94
+ primaryText: lorem2,
95
+ secondaryText: loremDescription,
96
+ initials: 'PT',
97
+ status: 'error',
98
+ },
99
+ },
100
+ {
101
+ content: { text: lorem2 },
102
+ },
103
+ {
104
+ content: {
105
+ text: lorem2,
106
+ status: 'error',
107
+ },
108
+ },
109
+ {
110
+ type: 'currency',
111
+ content: {
112
+ primaryCurrency: {
113
+ value: 23456.78,
114
+ currency: 'usd',
115
+ },
116
+ secondaryCurrency: {
117
+ value: 21000.0,
118
+ currency: 'gbp',
119
+ },
120
+ status: 'error',
121
+ },
122
+ alignment: 'right',
123
+ },
124
+ {
125
+ type: 'status',
126
+ content: {
127
+ primaryText: lorem2,
128
+ secondaryText: loremDescription,
129
+ sentiment: 'neutral',
130
+ },
131
+ },
132
+ ],
133
+ },
134
+ {
135
+ id: 2,
136
+ rowContent: [
137
+ {
138
+ type: 'leading',
139
+ content: {
140
+ primaryText: lorem2,
141
+ secondaryText: loremDescription,
142
+ initials: 'PT',
143
+ status: 'success',
144
+ },
145
+ },
146
+ {
147
+ content: { text: lorem2 },
148
+ },
149
+ {
150
+ content: {
151
+ text: lorem2,
152
+ status: 'success',
153
+ },
154
+ },
155
+ {
156
+ type: 'currency',
157
+ content: {
158
+ primaryCurrency: {
159
+ value: '34567.89',
160
+ currency: 'gbp',
161
+ },
162
+ secondaryCurrency: {
163
+ value: '31000.00',
164
+ currency: 'eur',
165
+ },
166
+ status: 'success',
167
+ },
168
+ alignment: 'right',
169
+ },
170
+ {
171
+ type: 'status',
172
+ content: {
173
+ primaryText: lorem2,
174
+ secondaryText: loremDescription,
175
+ sentiment: 'positive',
176
+ },
177
+ },
178
+ ],
179
+ },
180
+ {
181
+ id: 3,
182
+ rowContent: [
183
+ {
184
+ type: 'leading',
185
+ content: {
186
+ primaryText: lorem2,
187
+ secondaryText: loremDescription,
188
+ initials: 'PT',
189
+ },
190
+ },
191
+ {
192
+ content: { text: lorem2 },
193
+ },
194
+ {
195
+ content: { text: lorem2 },
196
+ },
197
+ {
198
+ type: 'currency',
199
+ content: {
200
+ primaryCurrency: {
201
+ value: '45678.90',
202
+ currency: 'nok',
203
+ },
204
+ secondaryCurrency: {
205
+ value: '41000.00',
206
+ currency: 'gbp',
207
+ },
208
+ },
209
+ alignment: 'right',
210
+ },
211
+ {
212
+ type: 'status',
213
+ content: {
214
+ primaryText: lorem2,
215
+ secondaryText: loremDescription,
216
+ sentiment: 'warning',
217
+ },
218
+ },
219
+ ],
220
+ },
221
+ {
222
+ id: 4,
223
+ rowContent: [
224
+ {
225
+ type: 'leading',
226
+ content: {
227
+ primaryText: lorem2,
228
+ secondaryText: loremDescription,
229
+ initials: 'PT',
230
+ },
231
+ },
232
+ {
233
+ content: { text: lorem2 },
234
+ },
235
+ {
236
+ content: { text: lorem2 },
237
+ },
238
+ {
239
+ type: 'currency',
240
+ content: {
241
+ primaryCurrency: {
242
+ value: '56789.01',
243
+ currency: 'sek',
244
+ },
245
+ secondaryCurrency: {
246
+ value: '51000.00',
247
+ currency: 'gbp',
248
+ },
249
+ },
250
+ alignment: 'right',
251
+ },
252
+ {
253
+ type: 'status',
254
+ content: {
255
+ primaryText: lorem2,
256
+ secondaryText: loremDescription,
257
+ sentiment: 'pending',
258
+ },
259
+ },
260
+ ],
261
+ },
262
+ ] satisfies TableRowType[] | TableRowClickableType[],
263
+ };
264
+
265
+ export const Basic: Story = {
266
+ args: {
267
+ data: tableData,
268
+ },
269
+ render: (args) => {
270
+ return <Table {...args} />;
271
+ },
272
+ };
273
+
274
+ export const WithClickableRow: Story = {
275
+ args: {
276
+ data: {
277
+ ...tableData,
278
+ onRowClick: (rowData: TableRowType | TableRowClickableType) => {
279
+ // eslint-disable-next-line no-console
280
+ console.log(`Row clicked, data:`, rowData);
281
+ },
282
+ },
283
+ },
284
+ render: (args) => {
285
+ return <Table {...args} />;
286
+ },
287
+ };
288
+
289
+ export const WithLoading: Story = {
290
+ args: {
291
+ loading: true,
292
+ },
293
+ };
294
+
295
+ export const WithEmptyData: Story = {
296
+ args: {
297
+ data: {
298
+ headers: tableData.headers,
299
+ content: [],
300
+ },
301
+ },
302
+ };
303
+
304
+ export const WithError: Story = {
305
+ args: {
306
+ error: {
307
+ message: 'Something went wrong during data fetching',
308
+ action: {
309
+ href: '/?path=/story/option-table--with-error',
310
+ text: 'To Refresh page, click here',
311
+ },
312
+ },
313
+ },
314
+ };
315
+
316
+ export const WithSimpleContent: Story = {
317
+ args: {
318
+ data: {
319
+ headers: [
320
+ {
321
+ header: sampleHeading,
322
+ },
323
+ {
324
+ header: sampleHeading,
325
+ },
326
+ ] satisfies TableHeaderType[],
327
+ content: [
328
+ {
329
+ id: 0,
330
+ rowContent: [{ content: { text: lorem5 } }, { content: { text: lorem5 } }],
331
+ },
332
+ {
333
+ id: 1,
334
+ rowContent: [{ content: { text: lorem5 } }, { content: { text: lorem5 } }],
335
+ },
336
+ {
337
+ id: 2,
338
+ rowContent: [{ content: { text: lorem5 } }, { content: { text: lorem5 } }],
339
+ },
340
+ {
341
+ id: 3,
342
+ rowContent: [{ content: { text: lorem5 } }, { content: { text: lorem5 } }],
343
+ },
344
+ {
345
+ id: 4,
346
+ rowContent: [{ content: { text: lorem5 } }, { content: { text: lorem5 } }],
347
+ },
348
+ ] satisfies TableRowType[] | TableRowClickableType[],
349
+ },
350
+ fullWidth: false,
351
+ },
352
+ };