@transferwise/components 46.133.1 → 46.134.0

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 (36) hide show
  1. package/build/main.css +43 -5
  2. package/build/moneyInput/MoneyInput.js +28 -12
  3. package/build/moneyInput/MoneyInput.js.map +1 -1
  4. package/build/moneyInput/MoneyInput.mjs +30 -14
  5. package/build/moneyInput/MoneyInput.mjs.map +1 -1
  6. package/build/moneyInput/currencyFormatting.js +8 -2
  7. package/build/moneyInput/currencyFormatting.js.map +1 -1
  8. package/build/moneyInput/currencyFormatting.mjs +5 -4
  9. package/build/moneyInput/currencyFormatting.mjs.map +1 -1
  10. package/build/statusIcon/StatusIcon.js +1 -12
  11. package/build/statusIcon/StatusIcon.js.map +1 -1
  12. package/build/statusIcon/StatusIcon.mjs +1 -12
  13. package/build/statusIcon/StatusIcon.mjs.map +1 -1
  14. package/build/styles/main.css +43 -5
  15. package/build/styles/sentimentSurface/SentimentSurface.css +1 -1
  16. package/build/styles/statusIcon/StatusIcon.css +35 -4
  17. package/build/types/moneyInput/MoneyInput.d.ts +6 -0
  18. package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
  19. package/build/types/moneyInput/currencyFormatting.d.ts +4 -3
  20. package/build/types/moneyInput/currencyFormatting.d.ts.map +1 -1
  21. package/build/types/statusIcon/StatusIcon.d.ts.map +1 -1
  22. package/package.json +6 -6
  23. package/src/main.css +43 -5
  24. package/src/moneyInput/MoneyInput.story.tsx +10 -1
  25. package/src/moneyInput/MoneyInput.test.story.tsx +141 -1
  26. package/src/moneyInput/MoneyInput.test.tsx +45 -0
  27. package/src/moneyInput/MoneyInput.tsx +27 -3
  28. package/src/moneyInput/currencyFormatting.ts +11 -5
  29. package/src/sentimentSurface/SentimentSurface.css +1 -1
  30. package/src/sentimentSurface/SentimentSurface.less +1 -1
  31. package/src/statusIcon/StatusIcon.css +35 -4
  32. package/src/statusIcon/StatusIcon.less +35 -4
  33. package/src/statusIcon/StatusIcon.story.tsx +119 -79
  34. package/src/statusIcon/StatusIcon.test.story.tsx +125 -0
  35. package/src/statusIcon/StatusIcon.test.tsx +16 -23
  36. package/src/statusIcon/StatusIcon.tsx +2 -16
@@ -1,102 +1,142 @@
1
1
  import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
 
3
- import { Sentiment, Size, Status } from '../common';
3
+ import { Sentiment } from '../common';
4
+ import SentimentSurface from '../sentimentSurface';
4
5
 
5
6
  import StatusIcon, { StatusIconSentiment } from './StatusIcon';
6
- import { withVariantConfig } from '../../.storybook/helpers';
7
+
8
+ const sentiments = [
9
+ Sentiment.POSITIVE,
10
+ Sentiment.NEGATIVE,
11
+ Sentiment.WARNING,
12
+ Sentiment.NEUTRAL,
13
+ Sentiment.PENDING,
14
+ ] as const;
15
+
16
+ const sizes = [16, 24, 32, 40, 48, 56, 72] as const;
17
+
18
+ const label: Record<string, string> = {
19
+ positive: 'Positive',
20
+ negative: 'Negative',
21
+ warning: 'Warning',
22
+ neutral: 'Neutral',
23
+ pending: 'Pending',
24
+ };
7
25
 
8
26
  export default {
9
27
  component: StatusIcon,
10
28
  title: 'Other/StatusIcon',
11
-
12
- argTypes: {
13
- iconLabel: {
14
- control: 'text',
15
- },
16
- },
17
29
  } satisfies Meta<typeof StatusIcon>;
18
30
 
19
31
  type Story = StoryObj<typeof StatusIcon>;
20
32
 
21
- export const Basic: Story = {};
33
+ export const Playground: Story = {
34
+ args: {
35
+ size: 40,
36
+ },
37
+ };
22
38
 
23
39
  /**
24
- * Ignored by the screen readers. Use with care.
40
+ * All available sentiments at the selected size.
25
41
  */
26
- export const Presentational: Story = {
27
- args: {
28
- iconLabel: null,
42
+ export const Sentiments: Story = {
43
+ argTypes: {
44
+ sentiment: { table: { disable: true } },
29
45
  },
46
+ render: ({ size }) => (
47
+ <div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
48
+ {sentiments.map((sentiment) => (
49
+ <div
50
+ key={sentiment}
51
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px' }}
52
+ >
53
+ <StatusIcon sentiment={sentiment as StatusIconSentiment} size={size} />
54
+ <span style={{ fontSize: '11px' }}>{label[sentiment]}</span>
55
+ </div>
56
+ ))}
57
+ </div>
58
+ ),
30
59
  };
31
60
 
32
- export const Variants: Story = {
33
- ...withVariantConfig(['default', 'dark', 'bright-green', 'forest-green'], {
34
- parameters: {
35
- padding: '0',
36
- },
37
- }),
38
- render: () => (
39
- <span style={{ display: 'flex', justifyContent: 'space-between', maxWidth: '400px' }}>
40
- {[
41
- Sentiment.POSITIVE,
42
- Sentiment.NEGATIVE,
43
- Sentiment.WARNING,
44
- Sentiment.NEUTRAL,
45
- Sentiment.PENDING,
46
- Status.PENDING,
47
- ].map((sentiment) => {
48
- return (
49
- <span
50
- key={sentiment}
51
- style={{
52
- display: 'flex',
53
- justifyContent: 'space-between',
54
- flexDirection: 'column',
55
- minHeight: '150px',
56
- alignItems: 'center',
57
- }}
58
- >
59
- {([16, 24, 32, 40, 48, 56, 72] as const).map((size) => {
60
- return (
61
- <StatusIcon key={size} size={size} sentiment={sentiment as StatusIconSentiment} />
62
- );
63
- })}
64
- </span>
65
- );
66
- })}
67
- </span>
61
+ /**
62
+ * All available sizes at the selected sentiment.
63
+ */
64
+ export const Sizes: Story = {
65
+ argTypes: {
66
+ size: { table: { disable: true } },
67
+ },
68
+ render: ({ sentiment }) => (
69
+ <div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
70
+ {sizes.map((size) => (
71
+ <div
72
+ key={size}
73
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px' }}
74
+ >
75
+ <StatusIcon sentiment={sentiment} size={size} />
76
+ <span style={{ fontSize: '11px' }}>{size}</span>
77
+ </div>
78
+ ))}
79
+ </div>
68
80
  ),
69
81
  };
70
82
 
71
- export const LegacySizes: Story = {
72
- render: () => (
73
- <span style={{ display: 'flex', justifyContent: 'space-between', maxWidth: '400px' }}>
74
- {[
75
- Sentiment.POSITIVE,
76
- Sentiment.NEGATIVE,
77
- Sentiment.NEUTRAL,
78
- Sentiment.WARNING,
79
- Status.PENDING,
80
- ].map((sentiment) => {
81
- return (
82
- <span
83
- key={sentiment}
84
- style={{
85
- display: 'flex',
86
- justifyContent: 'space-between',
87
- flexDirection: 'column',
88
- minHeight: '150px',
89
- alignItems: 'center',
90
- }}
83
+ /**
84
+ * `StatusIcon` is sentiment-aware and will automatically adjust its colours when placed inside
85
+ * a [SentimentSurface](?path=/docs/foundations-sentimentsurface--docs) component no extra
86
+ * props needed. Each row below is a `SentimentSurface` at either `base` or `elevated` emphasis (and a last row with no sentiment for reference).
87
+ */
88
+ export const SentimentAwareness: Story = {
89
+ argTypes: {
90
+ sentiment: { table: { disable: true } },
91
+ },
92
+ render: ({ size }) => {
93
+ const surfaceSentiments = ['success', 'warning', 'negative', 'neutral'] as const;
94
+
95
+ return (
96
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
97
+ {surfaceSentiments.flatMap((sentiment) => [
98
+ <SentimentSurface
99
+ key={`${sentiment}-base`}
100
+ sentiment={sentiment}
101
+ emphasis="base"
102
+ style={{ display: 'flex', alignItems: 'center', padding: '8px', gap: '8px' }}
91
103
  >
92
- {([Size.SMALL, Size.MEDIUM, Size.LARGE] as const).map((size) => {
93
- return (
94
- <StatusIcon key={size} size={size} sentiment={sentiment as StatusIconSentiment} />
95
- );
96
- })}
97
- </span>
98
- );
99
- })}
100
- </span>
101
- ),
104
+ <div
105
+ style={{ width: '120px', fontSize: '11px', fontWeight: 'bold', paddingLeft: '8px' }}
106
+ >
107
+ {sentiment} (base)
108
+ </div>
109
+ <StatusIcon sentiment={sentiment as StatusIconSentiment} size={size} />
110
+ </SentimentSurface>,
111
+ <SentimentSurface
112
+ key={`${sentiment}-elevated`}
113
+ sentiment={sentiment}
114
+ emphasis="elevated"
115
+ style={{ display: 'flex', alignItems: 'center', padding: '8px', gap: '8px' }}
116
+ >
117
+ <div
118
+ style={{ width: '120px', fontSize: '11px', fontWeight: 'bold', paddingLeft: '8px' }}
119
+ >
120
+ {sentiment} (elevated)
121
+ </div>
122
+ <StatusIcon sentiment={sentiment as StatusIconSentiment} size={size} />
123
+ </SentimentSurface>,
124
+ ])}
125
+
126
+ {/* Row without a SentimentSurface wrapper */}
127
+ <div style={{ display: 'flex', alignItems: 'center', padding: '8px', gap: '8px' }}>
128
+ <div style={{ width: '120px', fontSize: '11px', fontWeight: 'bold', paddingLeft: '8px' }}>
129
+ none
130
+ </div>
131
+ <StatusIcon sentiment="pending" size={size} />
132
+ </div>
133
+ </div>
134
+ );
135
+ },
136
+ parameters: {
137
+ docs: {
138
+ source: { type: 'dynamic' },
139
+ canvas: { sourceState: 'hidden' },
140
+ },
141
+ },
102
142
  };
@@ -0,0 +1,125 @@
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
+
3
+ import { Sentiment, Size } from '../common';
4
+ import SentimentSurface from '../sentimentSurface';
5
+ import { withVariantConfig } from '../../.storybook/helpers';
6
+ import { allModes } from '../../.storybook/modes';
7
+
8
+ import StatusIcon, { StatusIconSentiment } from './StatusIcon';
9
+
10
+ export default {
11
+ title: 'Other/StatusIcon/Tests',
12
+ component: StatusIcon,
13
+ tags: ['!autodocs', '!manifest'],
14
+ } satisfies Meta<typeof StatusIcon>;
15
+
16
+ type Story = StoryObj<typeof StatusIcon>;
17
+
18
+ const sizes = [16, 24, 32, 40, 48, 56, 72] as const;
19
+
20
+ const surfaceSentiments = ['success', 'warning', 'negative', 'neutral'] as const;
21
+
22
+ // Cycle through different sentiments per size so each row shows varied icons
23
+ const iconSentiments: StatusIconSentiment[] = [
24
+ Sentiment.POSITIVE,
25
+ Sentiment.NEGATIVE,
26
+ Sentiment.WARNING,
27
+ Sentiment.NEUTRAL,
28
+ Sentiment.PENDING,
29
+ Sentiment.POSITIVE,
30
+ Sentiment.NEGATIVE,
31
+ ];
32
+
33
+ const rowStyle = {
34
+ display: 'flex',
35
+ alignItems: 'center',
36
+ padding: '8px',
37
+ gap: '8px',
38
+ } as const;
39
+
40
+ const labelStyle = {
41
+ width: '120px',
42
+ fontSize: '11px',
43
+ fontWeight: 'bold',
44
+ paddingLeft: '8px',
45
+ flexShrink: 0,
46
+ } as const;
47
+
48
+ /**
49
+ * All sentiments, emphasis levels, and sizes across all themes for visual regression testing.
50
+ */
51
+ export const Variants: Story = {
52
+ render: () => (
53
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
54
+ {surfaceSentiments.flatMap((sentiment) => [
55
+ <SentimentSurface
56
+ key={`${sentiment}-base`}
57
+ sentiment={sentiment}
58
+ emphasis="base"
59
+ style={rowStyle}
60
+ >
61
+ <div style={labelStyle}>{sentiment} (base)</div>
62
+ {sizes.map((size, i) => (
63
+ <StatusIcon key={size} sentiment={iconSentiments[i]} size={size} />
64
+ ))}
65
+ </SentimentSurface>,
66
+ <SentimentSurface
67
+ key={`${sentiment}-elevated`}
68
+ sentiment={sentiment}
69
+ emphasis="elevated"
70
+ style={rowStyle}
71
+ >
72
+ <div style={labelStyle}>{sentiment} (elevated)</div>
73
+ {sizes.map((size, i) => (
74
+ <StatusIcon key={size} sentiment={iconSentiments[i]} size={size} />
75
+ ))}
76
+ </SentimentSurface>,
77
+ ])}
78
+
79
+ {/* Row without a SentimentSurface wrapper — standalone fallback colours */}
80
+ <div style={rowStyle}>
81
+ <div style={labelStyle}>none</div>
82
+ {sizes.map((size, i) => (
83
+ <StatusIcon key={size} sentiment={iconSentiments[i]} size={size} />
84
+ ))}
85
+ </div>
86
+ </div>
87
+ ),
88
+ parameters: {
89
+ padding: '16px',
90
+ variants: ['default', 'dark', 'bright-green', 'forest-green'],
91
+ },
92
+ };
93
+
94
+ export const RTL: Story = {
95
+ render: Variants.render,
96
+ ...withVariantConfig(['rtl']),
97
+ };
98
+
99
+ /**
100
+ * @deprecated Legacy `Size.SMALL | Size.MEDIUM | Size.LARGE` values still work but
101
+ * consumers should migrate to numeric sizes.
102
+ */
103
+ export const LegacySizes: Story = {
104
+ render: () => {
105
+ const legacySizes = [
106
+ { value: Size.SMALL, label: 'Size.SMALL' },
107
+ { value: Size.MEDIUM, label: 'Size.MEDIUM' },
108
+ { value: Size.LARGE, label: 'Size.LARGE' },
109
+ ] as const;
110
+
111
+ return (
112
+ <div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
113
+ {legacySizes.map(({ value, label }) => (
114
+ <div
115
+ key={label}
116
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px' }}
117
+ >
118
+ <StatusIcon size={value} />
119
+ <span style={{ fontSize: '11px' }}>{label}</span>
120
+ </div>
121
+ ))}
122
+ </div>
123
+ );
124
+ },
125
+ };
@@ -39,35 +39,28 @@ describe('StatusIcon', () => {
39
39
  renderStatusIcon({ sentiment: sentiment as StatusIconSentiment });
40
40
 
41
41
  expect(screen.getByTestId('status-icon')).toHaveClass(expectedClass);
42
- cleanup();
43
42
  },
44
43
  );
45
44
 
46
- it("'warning' and 'pending' sentiments generate 'dark' colored icons", () => {
47
- renderStatusIcon({ sentiment: Sentiment.WARNING });
48
- expect(screen.getByTestId('alert-icon')).toHaveClass('dark');
49
- cleanup();
50
45
 
51
- renderStatusIcon({ sentiment: Sentiment.PENDING });
52
- expect(screen.getByTestId('clock-borderless-icon')).toHaveClass('dark');
53
- cleanup();
54
-
55
- renderStatusIcon({ sentiment: Status.PENDING });
56
- expect(screen.getByTestId('clock-borderless-icon')).toHaveClass('dark');
57
- });
58
-
59
- it("'positive', 'negative', and 'neutral' sentiments generate 'light' colored icons", () => {
60
- renderStatusIcon({ sentiment: Sentiment.POSITIVE });
61
- expect(screen.getByTestId('check-icon')).toHaveClass('light');
62
- cleanup();
46
+ it.each([
47
+ [Sentiment.WARNING, 'alert-icon'],
48
+ [Sentiment.PENDING, 'clock-borderless-icon'],
49
+ [Status.PENDING, 'clock-borderless-icon'],
50
+ [Sentiment.POSITIVE, 'check-icon'],
51
+ [Sentiment.NEGATIVE, 'cross-icon'],
52
+ [Sentiment.NEUTRAL, 'info-icon'],
53
+ ])(
54
+ "renders the correct icon for '%s' sentiment",
55
+ (sentiment: Sentiment | Status, expectedIconTestId: string) => {
56
+ renderStatusIcon({ sentiment: sentiment as StatusIconSentiment });
63
57
 
64
- renderStatusIcon({ sentiment: Sentiment.NEGATIVE });
65
- expect(screen.getByTestId('cross-icon')).toHaveClass('light');
66
- cleanup();
58
+ const icon = screen.getByTestId(expectedIconTestId);
59
+ expect(icon).toBeInTheDocument();
60
+ expect(icon).toHaveClass('status-icon');
61
+ },
62
+ );
67
63
 
68
- renderStatusIcon();
69
- expect(screen.getByTestId('info-icon')).toHaveClass('light');
70
- });
71
64
 
72
65
  describe('accessible name', () => {
73
66
  it.each([
@@ -1,9 +1,7 @@
1
1
  import { Info, Alert, Cross, Check, ClockBorderless } from '@transferwise/icons';
2
2
  import { clsx } from 'clsx';
3
3
  import { useIntl } from 'react-intl';
4
- import { PropsWithChildren } from 'react';
5
4
 
6
- import SentimentSurface from '../sentimentSurface';
7
5
  import { SizeSmall, SizeMedium, SizeLarge, Sentiment, Size, Breakpoint, Status } from '../common';
8
6
  import Circle, { CircleProps } from '../common/circle';
9
7
  import { useMedia } from '../common/hooks/useMedia';
@@ -88,30 +86,18 @@ const StatusIcon = ({
88
86
  };
89
87
  const { Icon, defaultIconLabel } = iconMetaBySentiment[sentiment];
90
88
 
91
- const iconColor = sentiment === 'warning' || sentiment === 'pending' ? 'dark' : 'light';
92
89
  const isTinyViewport = useMedia(`(max-width: ${Breakpoint.ZOOM_400}px)`);
93
90
  const size = mapLegacySize[sizeProp] ?? sizeProp;
94
- // eslint-disable-next-line react/no-unstable-nested-components
95
- const SentimentSurfaceSetting = (props: PropsWithChildren<Pick<CircleProps, 'className'>>) => (
96
- <SentimentSurface
97
- as="span"
98
- // @ts-expect-error sentiment and SentimentSurface types mismatch
99
- sentiment={
100
- sentiment === 'positive' ? 'success' : sentiment === 'pending' ? 'warning' : sentiment
101
- }
102
- {...props}
103
- />
104
- );
91
+
105
92
  return (
106
93
  <Circle
107
- as={SentimentSurfaceSetting}
108
94
  size={isTinyViewport && size < 40 ? 32 : size}
109
95
  data-testid="status-icon"
110
96
  className={clsx('status-circle', sentiment)}
111
97
  id={id}
112
98
  >
113
99
  <Icon
114
- className={clsx('status-icon', iconColor)}
100
+ className="status-icon"
115
101
  title={iconLabel === null ? undefined : iconLabel || defaultIconLabel}
116
102
  />
117
103
  </Circle>