@transferwise/components 46.133.0 → 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 (78) hide show
  1. package/build/chips/Chips.js.map +1 -1
  2. package/build/chips/Chips.mjs.map +1 -1
  3. package/build/label/Label.js +1 -1
  4. package/build/label/Label.js.map +1 -1
  5. package/build/label/Label.mjs +1 -1
  6. package/build/label/Label.mjs.map +1 -1
  7. package/build/logo/Logo.js +6 -0
  8. package/build/logo/Logo.js.map +1 -1
  9. package/build/logo/Logo.mjs +6 -0
  10. package/build/logo/Logo.mjs.map +1 -1
  11. package/build/main.css +47 -9
  12. package/build/moneyInput/MoneyInput.js +28 -12
  13. package/build/moneyInput/MoneyInput.js.map +1 -1
  14. package/build/moneyInput/MoneyInput.mjs +30 -14
  15. package/build/moneyInput/MoneyInput.mjs.map +1 -1
  16. package/build/moneyInput/currencyFormatting.js +8 -2
  17. package/build/moneyInput/currencyFormatting.js.map +1 -1
  18. package/build/moneyInput/currencyFormatting.mjs +5 -4
  19. package/build/moneyInput/currencyFormatting.mjs.map +1 -1
  20. package/build/statusIcon/StatusIcon.js +1 -12
  21. package/build/statusIcon/StatusIcon.js.map +1 -1
  22. package/build/statusIcon/StatusIcon.mjs +1 -12
  23. package/build/statusIcon/StatusIcon.mjs.map +1 -1
  24. package/build/styles/listItem/ListItem.css +4 -4
  25. package/build/styles/listItem/ListItem.grid.css +3 -3
  26. package/build/styles/main.css +47 -9
  27. package/build/styles/sentimentSurface/SentimentSurface.css +1 -1
  28. package/build/styles/statusIcon/StatusIcon.css +35 -4
  29. package/build/types/chips/Chips.d.ts +1 -1
  30. package/build/types/chips/Chips.d.ts.map +1 -1
  31. package/build/types/common/commonProps.d.ts +0 -6
  32. package/build/types/common/commonProps.d.ts.map +1 -1
  33. package/build/types/label/Label.d.ts.map +1 -1
  34. package/build/types/logo/Logo.d.ts +10 -1
  35. package/build/types/logo/Logo.d.ts.map +1 -1
  36. package/build/types/moneyInput/MoneyInput.d.ts +6 -0
  37. package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
  38. package/build/types/moneyInput/currencyFormatting.d.ts +4 -3
  39. package/build/types/moneyInput/currencyFormatting.d.ts.map +1 -1
  40. package/build/types/statusIcon/StatusIcon.d.ts.map +1 -1
  41. package/package.json +8 -8
  42. package/src/button/_stories/Button.story.tsx +15 -5
  43. package/src/checkboxButton/CheckboxButton.story.tsx +125 -44
  44. package/src/checkboxButton/CheckboxButton.test.story.tsx +236 -0
  45. package/src/chips/Chips.story.tsx +141 -102
  46. package/src/chips/Chips.test.story.tsx +177 -0
  47. package/src/chips/Chips.tsx +1 -1
  48. package/src/circularButton/CircularButton.story.tsx +261 -49
  49. package/src/circularButton/CircularButton.test.story.tsx +192 -2
  50. package/src/common/commonProps.ts +0 -6
  51. package/src/iconButton/IconButton.story.tsx +315 -110
  52. package/src/iconButton/IconButton.test.story.tsx +217 -44
  53. package/src/label/Label.tsx +1 -2
  54. package/src/listItem/ListItem.css +4 -4
  55. package/src/listItem/ListItem.grid.css +3 -3
  56. package/src/listItem/ListItem.grid.less +5 -3
  57. package/src/listItem/ListItem.less +1 -1
  58. package/src/listItem/ListItem.vars.less +2 -2
  59. package/src/listItem/_stories/ListItem.layout.test.story.tsx +55 -0
  60. package/src/logo/Logo.story.tsx +181 -21
  61. package/src/logo/Logo.test.story.tsx +40 -7
  62. package/src/logo/Logo.tsx +10 -1
  63. package/src/main.css +47 -9
  64. package/src/moneyInput/MoneyInput.story.tsx +10 -1
  65. package/src/moneyInput/MoneyInput.test.story.tsx +141 -1
  66. package/src/moneyInput/MoneyInput.test.tsx +45 -0
  67. package/src/moneyInput/MoneyInput.tsx +27 -3
  68. package/src/moneyInput/currencyFormatting.ts +11 -5
  69. package/src/sentimentSurface/SentimentSurface.css +1 -1
  70. package/src/sentimentSurface/SentimentSurface.less +1 -1
  71. package/src/statusIcon/StatusIcon.css +35 -4
  72. package/src/statusIcon/StatusIcon.less +35 -4
  73. package/src/statusIcon/StatusIcon.story.tsx +119 -79
  74. package/src/statusIcon/StatusIcon.test.story.tsx +125 -0
  75. package/src/statusIcon/StatusIcon.test.tsx +16 -23
  76. package/src/statusIcon/StatusIcon.tsx +2 -16
  77. package/src/switch/Switch.story.tsx +64 -42
  78. package/src/switch/Switch.test.story.tsx +123 -0
@@ -24,7 +24,7 @@ import {
24
24
  import Title from '../title';
25
25
 
26
26
  import messages from './MoneyInput.messages';
27
- import { formatAmount, parseAmount } from './currencyFormatting';
27
+ import { formatAmount, formatNumber, getCurrencyDecimals, parseAmount } from './currencyFormatting';
28
28
  import withId from '../withId';
29
29
 
30
30
  export interface CurrencyOptionItem {
@@ -48,24 +48,34 @@ const formatAmountIfSet = ({
48
48
  amount,
49
49
  currency,
50
50
  locale,
51
+ decimals,
51
52
  }: {
52
53
  amount: number | null | undefined;
53
54
  currency: string;
54
55
  locale: string;
56
+ decimals?: number;
55
57
  }) => {
56
- return typeof amount === 'number' ? formatAmount(amount, currency, locale) : '';
58
+ if (typeof amount !== 'number') {
59
+ return '';
60
+ }
61
+ if (decimals != null && getCurrencyDecimals(currency) !== 0) {
62
+ return formatNumber(amount, locale, decimals);
63
+ }
64
+ return formatAmount(amount, currency, locale);
57
65
  };
58
66
 
59
67
  const parseNumber = ({
60
68
  amount,
61
69
  currency,
62
70
  locale,
71
+ decimals,
63
72
  }: {
64
73
  amount: string;
65
74
  currency: string;
66
75
  locale: string;
76
+ decimals?: number;
67
77
  }) => {
68
- return parseAmount(amount, currency, locale);
78
+ return parseAmount(amount, currency, locale, decimals);
69
79
  };
70
80
 
71
81
  const allowedInputKeys = new Set([
@@ -102,6 +112,12 @@ export interface MoneyInputProps extends WrappedComponentProps {
102
112
  onCustomAction?: () => void;
103
113
  classNames?: Record<string, string>;
104
114
  selectProps?: Partial<SelectInputProps<CurrencyOptionItem>>;
115
+ /**
116
+ * Specify the number of decimal places to format the amount. When not specified, the number of
117
+ * decimals is determined by the selected currency (e.g. 2 for EUR, 0 for JPY, 3 for BHD).
118
+ * This override is ignored for zero-decimal currencies (e.g. JPY, KRW, HUF), which always use 0.
119
+ */
120
+ decimals?: number;
105
121
  }
106
122
 
107
123
  export type MoneyInputPropsWithInputAttributes = MoneyInputProps &
@@ -133,6 +149,7 @@ class MoneyInput extends Component<MoneyInputPropsWithInputAttributes, MoneyInpu
133
149
  amount: props.amount,
134
150
  currency: props.selectedCurrency.currency,
135
151
  locale: props.intl.locale,
152
+ decimals: props.decimals,
136
153
  }),
137
154
  locale: props.intl.locale,
138
155
  };
@@ -147,6 +164,7 @@ class MoneyInput extends Component<MoneyInputPropsWithInputAttributes, MoneyInpu
147
164
  amount: nextProps.amount,
148
165
  currency: nextProps.selectedCurrency.currency,
149
166
  locale: nextProps.intl.locale,
167
+ decimals: nextProps.decimals,
150
168
  }),
151
169
  });
152
170
  }
@@ -174,6 +192,7 @@ class MoneyInput extends Component<MoneyInputPropsWithInputAttributes, MoneyInpu
174
192
  amount: paste,
175
193
  currency: this.props.selectedCurrency.currency,
176
194
  locale,
195
+ decimals: this.props.decimals,
177
196
  });
178
197
 
179
198
  if (isNumberOrNull(parsed)) {
@@ -182,6 +201,7 @@ class MoneyInput extends Component<MoneyInputPropsWithInputAttributes, MoneyInpu
182
201
  amount: parsed,
183
202
  currency: this.props.selectedCurrency.currency,
184
203
  locale,
204
+ decimals: this.props.decimals,
185
205
  }),
186
206
  });
187
207
  this.props.onAmountChange?.(parsed);
@@ -201,6 +221,7 @@ class MoneyInput extends Component<MoneyInputPropsWithInputAttributes, MoneyInpu
201
221
  amount: value,
202
222
  currency: this.props.selectedCurrency.currency,
203
223
  locale: this.state.locale,
224
+ decimals: this.props.decimals,
204
225
  });
205
226
  if (isNumberOrNull(parsed)) {
206
227
  this.props.onAmountChange?.(parsed);
@@ -248,6 +269,7 @@ class MoneyInput extends Component<MoneyInputPropsWithInputAttributes, MoneyInpu
248
269
  amount: previousState.formattedAmount,
249
270
  currency: this.props.selectedCurrency.currency,
250
271
  locale: previousState.locale,
272
+ decimals: this.props.decimals,
251
273
  });
252
274
  if (!isNumberOrNull(parsed)) {
253
275
  return {
@@ -259,6 +281,7 @@ class MoneyInput extends Component<MoneyInputPropsWithInputAttributes, MoneyInpu
259
281
  amount: parsed,
260
282
  currency: this.props.selectedCurrency.currency,
261
283
  locale: previousState.locale,
284
+ decimals: this.props.decimals,
262
285
  }),
263
286
  };
264
287
  });
@@ -340,6 +363,7 @@ class MoneyInput extends Component<MoneyInputPropsWithInputAttributes, MoneyInpu
340
363
  amount: this.props.placeholder,
341
364
  currency: this.props.selectedCurrency.currency,
342
365
  locale: this.state.locale,
366
+ decimals: this.props.decimals,
343
367
  })}
344
368
  autoComplete="off"
345
369
  aria-describedby={selectedCurrencyElementId}
@@ -1,8 +1,8 @@
1
- import { formatAmount } from '@transferwise/formatting';
1
+ import { formatAmount, formatNumber } from '@transferwise/formatting';
2
2
 
3
3
  import { DEFAULT_LOCALE } from '../common/locale';
4
4
 
5
- export { formatAmount };
5
+ export { formatAmount, formatNumber };
6
6
 
7
7
  // TODO: do not duplicate this between formatting and components
8
8
  const currencyDecimals: Record<string, number> = {
@@ -52,7 +52,7 @@ function getValidLocale(locale: string) {
52
52
  }
53
53
  }
54
54
 
55
- function getCurrencyDecimals(currency: string) {
55
+ export function getCurrencyDecimals(currency: string) {
56
56
  const upperCaseCurrency = currency.toUpperCase();
57
57
  return currencyDecimals[upperCaseCurrency] ?? DEFAULT_CURRENCY_DECIMALS;
58
58
  }
@@ -61,10 +61,16 @@ function getDecimalSeparator(locale: string) {
61
61
  return isNumberLocaleSupported() ? (1.1).toLocaleString(locale)[1] : '.';
62
62
  }
63
63
 
64
- export function parseAmount(number: string, currency: string, locale = DEFAULT_LOCALE) {
64
+ export function parseAmount(
65
+ number: string,
66
+ currency: string,
67
+ locale = DEFAULT_LOCALE,
68
+ decimals?: number,
69
+ ) {
65
70
  const validLocale = getValidLocale(locale);
66
71
 
67
- const precision = getCurrencyDecimals(currency);
72
+ const currencyDefault = getCurrencyDecimals(currency);
73
+ const precision = currencyDefault === 0 ? 0 : (decimals ?? currencyDefault);
68
74
  const groupSeparator = isNumberLocaleSupported() ? (10000).toLocaleString(validLocale)[2] : ',';
69
75
  const decimalSeparator = getDecimalSeparator(validLocale);
70
76
  const numberWithStandardDecimalSeparator = (number || '')
@@ -49,7 +49,7 @@
49
49
  --color-sentiment-interactive-control: #CB272F;
50
50
  --color-sentiment-interactive-control-hover: #B8232B;
51
51
  --color-sentiment-interactive-control-active: #A72027;
52
- --color-sentiment-background-surface: #90000D;
52
+ --color-sentiment-background-surface: #CB272F;
53
53
  --color-sentiment-background-surface-hover: #B8232B;
54
54
  --color-sentiment-background-surface-active: #A72027;
55
55
  }
@@ -27,7 +27,7 @@
27
27
  #CB272F, #B8232B, #A72027,
28
28
  #9B141B, #831116, #6D0e13,
29
29
  #CB272F, #B8232B, #A72027,
30
- #90000D, #B8232B, #A72027
30
+ #CB272F, #B8232B, #A72027
31
31
  );
32
32
  }
33
33
  }
@@ -1,6 +1,37 @@
1
- .wds-sentiment-surface.status-circle {
2
- background-color: var(--color-sentiment-interactive-primary);
1
+ .status-circle.negative,
2
+ .status-circle.error {
3
+ background-color: var(--color-sentiment-interactive-primary, var(--color-sentiment-negative));
3
4
  }
4
- .wds-sentiment-surface.status-circle .status-icon {
5
- color: var(--color-sentiment-interactive-control);
5
+ .status-circle.negative .status-icon,
6
+ .status-circle.error .status-icon {
7
+ color: var(--color-sentiment-interactive-control, var(--color-sentiment-negative-secondary));
8
+ }
9
+ .status-circle.positive,
10
+ .status-circle.success {
11
+ background-color: var(--color-sentiment-interactive-primary, var(--color-sentiment-positive));
12
+ }
13
+ .status-circle.positive .status-icon,
14
+ .status-circle.success .status-icon {
15
+ color: var(--color-sentiment-interactive-control, var(--color-sentiment-positive-secondary));
16
+ }
17
+ .status-circle.warning,
18
+ .status-circle.pending {
19
+ background-color: var(--color-sentiment-interactive-primary, var(--color-sentiment-warning));
20
+ }
21
+ .status-circle.warning .status-icon,
22
+ .status-circle.pending .status-icon {
23
+ color: var(--color-sentiment-interactive-control, var(--color-dark));
24
+ }
25
+ .status-circle.neutral,
26
+ .status-circle.info {
27
+ background-color: #5d7079;
28
+ background-color: var(--color-sentiment-interactive-primary, var(--color-content-secondary));
29
+ }
30
+ .status-circle.neutral .status-icon,
31
+ .status-circle.info .status-icon {
32
+ color: var(--color-sentiment-interactive-control, var(--color-contrast-overlay));
33
+ }
34
+ .np-theme-personal--bright-green .status-circle.neutral .status-icon,
35
+ .np-theme-personal--bright-green .status-circle.info .status-icon {
36
+ color: var(--color-sentiment-interactive-control, var(--color-white));
6
37
  }
@@ -1,6 +1,37 @@
1
- .wds-sentiment-surface.status-circle {
2
- background-color: var(--color-sentiment-interactive-primary);
3
- .status-icon {
4
- color: var(--color-sentiment-interactive-control);
1
+ .status-circle {
2
+ &.negative,
3
+ &.error {
4
+ background-color: var(--color-sentiment-interactive-primary, var(--color-sentiment-negative));
5
+ .status-icon {
6
+ color: var(--color-sentiment-interactive-control, var(--color-sentiment-negative-secondary));
7
+ }
8
+ }
9
+
10
+ &.positive,
11
+ &.success {
12
+ background-color: var(--color-sentiment-interactive-primary, var(--color-sentiment-positive));
13
+ .status-icon {
14
+ color: var(--color-sentiment-interactive-control, var(--color-sentiment-positive-secondary));
15
+ }
16
+ }
17
+
18
+ &.warning,
19
+ &.pending {
20
+ background-color: var(--color-sentiment-interactive-primary, var(--color-sentiment-warning));
21
+ .status-icon {
22
+ color: var(--color-sentiment-interactive-control, var(--color-dark));
23
+ }
24
+ }
25
+
26
+ &.neutral,
27
+ &.info {
28
+ background-color: var(--color-sentiment-interactive-primary, var(--color-content-secondary));
29
+ .status-icon {
30
+ color: var(--color-sentiment-interactive-control, var(--color-contrast-overlay));
31
+
32
+ .np-theme-personal--bright-green & {
33
+ color: var(--color-sentiment-interactive-control, var(--color-white));
34
+ }
35
+ }
5
36
  }
6
37
  }
@@ -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([