@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.
- package/build/chips/Chips.js.map +1 -1
- package/build/chips/Chips.mjs.map +1 -1
- package/build/label/Label.js +1 -1
- package/build/label/Label.js.map +1 -1
- package/build/label/Label.mjs +1 -1
- package/build/label/Label.mjs.map +1 -1
- package/build/logo/Logo.js +6 -0
- package/build/logo/Logo.js.map +1 -1
- package/build/logo/Logo.mjs +6 -0
- package/build/logo/Logo.mjs.map +1 -1
- package/build/main.css +47 -9
- package/build/moneyInput/MoneyInput.js +28 -12
- package/build/moneyInput/MoneyInput.js.map +1 -1
- package/build/moneyInput/MoneyInput.mjs +30 -14
- package/build/moneyInput/MoneyInput.mjs.map +1 -1
- package/build/moneyInput/currencyFormatting.js +8 -2
- package/build/moneyInput/currencyFormatting.js.map +1 -1
- package/build/moneyInput/currencyFormatting.mjs +5 -4
- package/build/moneyInput/currencyFormatting.mjs.map +1 -1
- package/build/statusIcon/StatusIcon.js +1 -12
- package/build/statusIcon/StatusIcon.js.map +1 -1
- package/build/statusIcon/StatusIcon.mjs +1 -12
- package/build/statusIcon/StatusIcon.mjs.map +1 -1
- package/build/styles/listItem/ListItem.css +4 -4
- package/build/styles/listItem/ListItem.grid.css +3 -3
- package/build/styles/main.css +47 -9
- package/build/styles/sentimentSurface/SentimentSurface.css +1 -1
- package/build/styles/statusIcon/StatusIcon.css +35 -4
- package/build/types/chips/Chips.d.ts +1 -1
- package/build/types/chips/Chips.d.ts.map +1 -1
- package/build/types/common/commonProps.d.ts +0 -6
- package/build/types/common/commonProps.d.ts.map +1 -1
- package/build/types/label/Label.d.ts.map +1 -1
- package/build/types/logo/Logo.d.ts +10 -1
- package/build/types/logo/Logo.d.ts.map +1 -1
- package/build/types/moneyInput/MoneyInput.d.ts +6 -0
- package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
- package/build/types/moneyInput/currencyFormatting.d.ts +4 -3
- package/build/types/moneyInput/currencyFormatting.d.ts.map +1 -1
- package/build/types/statusIcon/StatusIcon.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/button/_stories/Button.story.tsx +15 -5
- package/src/checkboxButton/CheckboxButton.story.tsx +125 -44
- package/src/checkboxButton/CheckboxButton.test.story.tsx +236 -0
- package/src/chips/Chips.story.tsx +141 -102
- package/src/chips/Chips.test.story.tsx +177 -0
- package/src/chips/Chips.tsx +1 -1
- package/src/circularButton/CircularButton.story.tsx +261 -49
- package/src/circularButton/CircularButton.test.story.tsx +192 -2
- package/src/common/commonProps.ts +0 -6
- package/src/iconButton/IconButton.story.tsx +315 -110
- package/src/iconButton/IconButton.test.story.tsx +217 -44
- package/src/label/Label.tsx +1 -2
- package/src/listItem/ListItem.css +4 -4
- package/src/listItem/ListItem.grid.css +3 -3
- package/src/listItem/ListItem.grid.less +5 -3
- package/src/listItem/ListItem.less +1 -1
- package/src/listItem/ListItem.vars.less +2 -2
- package/src/listItem/_stories/ListItem.layout.test.story.tsx +55 -0
- package/src/logo/Logo.story.tsx +181 -21
- package/src/logo/Logo.test.story.tsx +40 -7
- package/src/logo/Logo.tsx +10 -1
- package/src/main.css +47 -9
- package/src/moneyInput/MoneyInput.story.tsx +10 -1
- package/src/moneyInput/MoneyInput.test.story.tsx +141 -1
- package/src/moneyInput/MoneyInput.test.tsx +45 -0
- package/src/moneyInput/MoneyInput.tsx +27 -3
- package/src/moneyInput/currencyFormatting.ts +11 -5
- package/src/sentimentSurface/SentimentSurface.css +1 -1
- package/src/sentimentSurface/SentimentSurface.less +1 -1
- package/src/statusIcon/StatusIcon.css +35 -4
- package/src/statusIcon/StatusIcon.less +35 -4
- package/src/statusIcon/StatusIcon.story.tsx +119 -79
- package/src/statusIcon/StatusIcon.test.story.tsx +125 -0
- package/src/statusIcon/StatusIcon.test.tsx +16 -23
- package/src/statusIcon/StatusIcon.tsx +2 -16
- package/src/switch/Switch.story.tsx +64 -42
- 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
|
-
|
|
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(
|
|
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
|
|
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: #
|
|
52
|
+
--color-sentiment-background-surface: #CB272F;
|
|
53
53
|
--color-sentiment-background-surface-hover: #B8232B;
|
|
54
54
|
--color-sentiment-background-surface-active: #A72027;
|
|
55
55
|
}
|
|
@@ -1,6 +1,37 @@
|
|
|
1
|
-
.
|
|
2
|
-
|
|
1
|
+
.status-circle.negative,
|
|
2
|
+
.status-circle.error {
|
|
3
|
+
background-color: var(--color-sentiment-interactive-primary, var(--color-sentiment-negative));
|
|
3
4
|
}
|
|
4
|
-
.
|
|
5
|
-
|
|
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
|
-
.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
color: var(--color-sentiment-interactive-
|
|
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
|
|
3
|
+
import { Sentiment } from '../common';
|
|
4
|
+
import SentimentSurface from '../sentimentSurface';
|
|
4
5
|
|
|
5
6
|
import StatusIcon, { StatusIconSentiment } from './StatusIcon';
|
|
6
|
-
|
|
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
|
|
33
|
+
export const Playground: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
size: 40,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
22
38
|
|
|
23
39
|
/**
|
|
24
|
-
*
|
|
40
|
+
* All available sentiments at the selected size.
|
|
25
41
|
*/
|
|
26
|
-
export const
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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([
|