@transferwise/components 46.115.1 → 46.116.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/criticalBanner/CriticalCommsBanner.js +1 -0
- package/build/criticalBanner/CriticalCommsBanner.js.map +1 -1
- package/build/criticalBanner/CriticalCommsBanner.mjs +1 -0
- package/build/criticalBanner/CriticalCommsBanner.mjs.map +1 -1
- package/build/main.css +428 -36
- package/build/mocks.js +7 -0
- package/build/mocks.js.map +1 -1
- package/build/mocks.mjs +7 -1
- package/build/mocks.mjs.map +1 -1
- package/build/sentimentSurface/SentimentSurface.js +43 -0
- package/build/sentimentSurface/SentimentSurface.js.map +1 -0
- package/build/sentimentSurface/SentimentSurface.mjs +39 -0
- package/build/sentimentSurface/SentimentSurface.mjs.map +1 -0
- package/build/sentimentSurface/classMap.js +17 -0
- package/build/sentimentSurface/classMap.js.map +1 -0
- package/build/sentimentSurface/classMap.mjs +14 -0
- package/build/sentimentSurface/classMap.mjs.map +1 -0
- package/build/statusIcon/StatusIcon.js +10 -1
- package/build/statusIcon/StatusIcon.js.map +1 -1
- package/build/statusIcon/StatusIcon.mjs +10 -1
- package/build/statusIcon/StatusIcon.mjs.map +1 -1
- package/build/styles/main.css +428 -36
- package/build/styles/sentimentSurface/SentimentSurface.css +424 -0
- package/build/styles/statusIcon/StatusIcon.css +4 -36
- package/build/types/criticalBanner/CriticalCommsBanner.d.ts +2 -1
- package/build/types/criticalBanner/CriticalCommsBanner.d.ts.map +1 -1
- package/build/types/mocks.d.ts +1 -0
- package/build/types/mocks.d.ts.map +1 -1
- package/build/types/sentimentSurface/SentimentSurface.d.ts +30 -0
- package/build/types/sentimentSurface/SentimentSurface.d.ts.map +1 -0
- package/build/types/sentimentSurface/SentimentSurface.types.d.ts +80 -0
- package/build/types/sentimentSurface/SentimentSurface.types.d.ts.map +1 -0
- package/build/types/sentimentSurface/classMap.d.ts +4 -0
- package/build/types/sentimentSurface/classMap.d.ts.map +1 -0
- package/build/types/sentimentSurface/index.d.ts +3 -0
- package/build/types/sentimentSurface/index.d.ts.map +1 -0
- package/build/types/statusIcon/StatusIcon.d.ts.map +1 -1
- package/build/types/test-utils/window-mock.d.ts +1 -0
- package/build/types/test-utils/window-mock.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/criticalBanner/CriticalCommsBanner.tsx +3 -2
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.spec.tsx +229 -0
- package/src/expressiveMoneyInput/amountInput/AmountInput.spec.tsx +282 -0
- package/src/expressiveMoneyInput/currencySelector/CurrencySelector.spec.tsx +160 -0
- package/src/inputs/SelectInput.spec.tsx +7 -1
- package/src/main.css +428 -36
- package/src/main.less +2 -0
- package/src/mocks.ts +7 -0
- package/src/moneyInput/MoneyInput.spec.tsx +9 -1
- package/src/provider/theme/ThemeProvider.story.tsx +78 -11
- package/src/sentimentSurface/SentimentSurface.css +424 -0
- package/src/sentimentSurface/SentimentSurface.docs.mdx +527 -0
- package/src/sentimentSurface/SentimentSurface.less +296 -0
- package/src/sentimentSurface/SentimentSurface.spec.tsx +140 -0
- package/src/sentimentSurface/SentimentSurface.story.tsx +340 -0
- package/src/sentimentSurface/SentimentSurface.tests.story.tsx +123 -0
- package/src/sentimentSurface/SentimentSurface.tsx +72 -0
- package/src/sentimentSurface/SentimentSurface.types.ts +104 -0
- package/src/sentimentSurface/classMap.ts +15 -0
- package/src/sentimentSurface/index.ts +8 -0
- package/src/statusIcon/StatusIcon.css +4 -36
- package/src/statusIcon/StatusIcon.less +3 -41
- package/src/statusIcon/StatusIcon.tsx +14 -1
- package/src/test-utils/jest.setup.ts +0 -5
- package/src/test-utils/window-mock.ts +5 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
|
+
import SentimentSurface from './SentimentSurface';
|
|
3
|
+
import { storyConfig } from '../test-utils';
|
|
4
|
+
import type { Sentiment, Emphasis } from './SentimentSurface.types';
|
|
5
|
+
|
|
6
|
+
const withContainer = (Story: () => JSX.Element) => (
|
|
7
|
+
<div style={{ display: 'flex', justifyContent: 'center', padding: '2rem' }}>
|
|
8
|
+
<Story />
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const withComponentGrid = (Story: () => JSX.Element) => (
|
|
13
|
+
<div
|
|
14
|
+
style={{
|
|
15
|
+
width: '100%',
|
|
16
|
+
display: 'flex',
|
|
17
|
+
flexDirection: 'column',
|
|
18
|
+
gap: '1rem',
|
|
19
|
+
maxWidth: '800px',
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
<Story />
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* SentimentSurface is a polymorphic container component that applies contextual background colours
|
|
28
|
+
* and text styling based on sentiment types.
|
|
29
|
+
*/
|
|
30
|
+
const meta: Meta<typeof SentimentSurface> = {
|
|
31
|
+
component: SentimentSurface,
|
|
32
|
+
title: 'Content/SentimentSurface',
|
|
33
|
+
argTypes: {
|
|
34
|
+
sentiment: {
|
|
35
|
+
control: 'select',
|
|
36
|
+
options: ['negative', 'warning', 'neutral', 'success', 'proposition'],
|
|
37
|
+
description: 'The sentiment type that determines the colour scheme',
|
|
38
|
+
},
|
|
39
|
+
emphasis: {
|
|
40
|
+
control: 'select',
|
|
41
|
+
options: ['base', 'elevated'],
|
|
42
|
+
description: 'The emphasis level affecting background and text contrast',
|
|
43
|
+
table: {
|
|
44
|
+
defaultValue: { summary: 'base' },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
as: {
|
|
48
|
+
control: 'text',
|
|
49
|
+
description: 'The underlying HTML element to render',
|
|
50
|
+
table: {
|
|
51
|
+
defaultValue: { summary: 'div' },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
children: {
|
|
55
|
+
control: 'text',
|
|
56
|
+
description: 'Content to render inside the surface',
|
|
57
|
+
},
|
|
58
|
+
className: {
|
|
59
|
+
control: 'text',
|
|
60
|
+
description: 'Additional CSS classes',
|
|
61
|
+
},
|
|
62
|
+
id: {
|
|
63
|
+
control: 'text',
|
|
64
|
+
description: 'Unique identifier for the component',
|
|
65
|
+
},
|
|
66
|
+
testId: {
|
|
67
|
+
control: 'text',
|
|
68
|
+
description: 'Test ID for React Testing Library',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
args: {
|
|
72
|
+
sentiment: 'neutral',
|
|
73
|
+
emphasis: 'base',
|
|
74
|
+
children: 'This is a sentiment surface',
|
|
75
|
+
style: { padding: '24px', borderRadius: '16px' },
|
|
76
|
+
},
|
|
77
|
+
decorators: [withContainer],
|
|
78
|
+
parameters: {
|
|
79
|
+
docs: {
|
|
80
|
+
toc: true,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default meta;
|
|
86
|
+
|
|
87
|
+
type Story = StoryObj<typeof SentimentSurface>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The Playground story allows you to experiment with all the props of the SentimentSurface component.
|
|
91
|
+
* Use the controls below to customise the sentiment type, emphasis level, and content.
|
|
92
|
+
*/
|
|
93
|
+
export const Playground: Story = {
|
|
94
|
+
args: {
|
|
95
|
+
sentiment: 'negative',
|
|
96
|
+
emphasis: 'base',
|
|
97
|
+
children: 'Customise the sentiment and emphasis using the controls below',
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* There are five different sentiment types that communicate different types of information or states.
|
|
103
|
+
*/
|
|
104
|
+
export const Sentiments: Story = {
|
|
105
|
+
render: (args) => (
|
|
106
|
+
<>
|
|
107
|
+
<SentimentSurface {...args} sentiment="negative">
|
|
108
|
+
Negative: Your payment has failed
|
|
109
|
+
</SentimentSurface>
|
|
110
|
+
<SentimentSurface {...args} sentiment="warning">
|
|
111
|
+
Warning: Action required on your account
|
|
112
|
+
</SentimentSurface>
|
|
113
|
+
<SentimentSurface {...args} sentiment="neutral">
|
|
114
|
+
Neutral: Your account is up to date
|
|
115
|
+
</SentimentSurface>
|
|
116
|
+
<SentimentSurface {...args} sentiment="success">
|
|
117
|
+
Success: Your payment was successful
|
|
118
|
+
</SentimentSurface>
|
|
119
|
+
<SentimentSurface {...args} sentiment="proposition">
|
|
120
|
+
Proposition: Try our new feature
|
|
121
|
+
</SentimentSurface>
|
|
122
|
+
</>
|
|
123
|
+
),
|
|
124
|
+
parameters: {
|
|
125
|
+
controls: { disable: true },
|
|
126
|
+
},
|
|
127
|
+
decorators: [withComponentGrid],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Each sentiment type has two emphasis levels – base and elevated – to provide different levels of visual prominence.
|
|
132
|
+
*/
|
|
133
|
+
export const EmphasisLevels: Story = {
|
|
134
|
+
render: (args) => (
|
|
135
|
+
<>
|
|
136
|
+
<SentimentSurface {...args} sentiment="negative" emphasis="base">
|
|
137
|
+
Negative - Base emphasis
|
|
138
|
+
</SentimentSurface>
|
|
139
|
+
<SentimentSurface {...args} sentiment="negative" emphasis="elevated">
|
|
140
|
+
Negative - Elevated emphasis
|
|
141
|
+
</SentimentSurface>
|
|
142
|
+
<SentimentSurface {...args} sentiment="success" emphasis="base">
|
|
143
|
+
Success - Base emphasis
|
|
144
|
+
</SentimentSurface>
|
|
145
|
+
<SentimentSurface {...args} sentiment="success" emphasis="elevated">
|
|
146
|
+
Success - Elevated emphasis
|
|
147
|
+
</SentimentSurface>
|
|
148
|
+
</>
|
|
149
|
+
),
|
|
150
|
+
parameters: {
|
|
151
|
+
controls: { disable: true },
|
|
152
|
+
},
|
|
153
|
+
decorators: [withComponentGrid],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The component can be rendered as any HTML element using the `as` prop.
|
|
158
|
+
*/
|
|
159
|
+
export const PolymorphicRendering: Story = {
|
|
160
|
+
render: (args) => (
|
|
161
|
+
<>
|
|
162
|
+
<SentimentSurface {...args} sentiment="negative">
|
|
163
|
+
Rendered as div (default)
|
|
164
|
+
</SentimentSurface>
|
|
165
|
+
<SentimentSurface {...args} sentiment="negative" as="section">
|
|
166
|
+
Rendered as section
|
|
167
|
+
</SentimentSurface>
|
|
168
|
+
<SentimentSurface {...args} sentiment="negative" as="article">
|
|
169
|
+
Rendered as article
|
|
170
|
+
</SentimentSurface>
|
|
171
|
+
</>
|
|
172
|
+
),
|
|
173
|
+
parameters: {
|
|
174
|
+
controls: { disable: true },
|
|
175
|
+
},
|
|
176
|
+
decorators: [withComponentGrid],
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const sentiments: Sentiment[] = ['negative', 'warning', 'neutral', 'success', 'proposition'];
|
|
180
|
+
const emphasisLevels: Emphasis[] = ['base', 'elevated'];
|
|
181
|
+
const tokenCategories = [
|
|
182
|
+
{
|
|
183
|
+
name: 'Content',
|
|
184
|
+
tokens: [
|
|
185
|
+
'--color-sentiment-content-primary',
|
|
186
|
+
'--color-sentiment-content-primary-hover',
|
|
187
|
+
'--color-sentiment-content-primary-active',
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'Interactive Primary',
|
|
192
|
+
tokens: [
|
|
193
|
+
'--color-sentiment-interactive-primary',
|
|
194
|
+
'--color-sentiment-interactive-primary-hover',
|
|
195
|
+
'--color-sentiment-interactive-primary-active',
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'Interactive Secondary',
|
|
200
|
+
tokens: [
|
|
201
|
+
'--color-sentiment-interactive-secondary',
|
|
202
|
+
'--color-sentiment-interactive-secondary-hover',
|
|
203
|
+
'--color-sentiment-interactive-secondary-active',
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'Interactive Control',
|
|
208
|
+
tokens: [
|
|
209
|
+
'--color-sentiment-interactive-control',
|
|
210
|
+
'--color-sentiment-interactive-control-hover',
|
|
211
|
+
'--color-sentiment-interactive-control-active',
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'Background Surface',
|
|
216
|
+
tokens: [
|
|
217
|
+
'--color-sentiment-background-surface',
|
|
218
|
+
'--color-sentiment-background-surface-hover',
|
|
219
|
+
'--color-sentiment-background-surface-active',
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
const TokenSwatch = ({ token }: { token: string }) => (
|
|
225
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '4px' }}>
|
|
226
|
+
<div
|
|
227
|
+
style={{
|
|
228
|
+
width: '32px',
|
|
229
|
+
height: '32px',
|
|
230
|
+
borderRadius: '4px',
|
|
231
|
+
backgroundColor: `var(${token})`,
|
|
232
|
+
border: '1px solid rgba(0,0,0,0.1)',
|
|
233
|
+
flexShrink: 0,
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
<code style={{ fontSize: '12px', color: 'var(--color-sentiment-content-primary)' }}>
|
|
237
|
+
{token}
|
|
238
|
+
</code>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Visual demonstration of the CSS custom properties (tokens) set by each sentiment and emphasis combination.
|
|
244
|
+
* These tokens enable child components to automatically adapt their styling to the sentiment context.
|
|
245
|
+
*/
|
|
246
|
+
export const Tokens: Story = {
|
|
247
|
+
render: () => (
|
|
248
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '32px', maxWidth: '100%' }}>
|
|
249
|
+
{sentiments.map((sentiment) =>
|
|
250
|
+
emphasisLevels.map((emphasis) => (
|
|
251
|
+
<SentimentSurface
|
|
252
|
+
key={`${sentiment}-${emphasis}`}
|
|
253
|
+
sentiment={sentiment}
|
|
254
|
+
emphasis={emphasis}
|
|
255
|
+
style={{ padding: '24px', borderRadius: '16px' }}
|
|
256
|
+
>
|
|
257
|
+
<div style={{ marginBottom: '16px' }}>
|
|
258
|
+
<strong
|
|
259
|
+
style={{
|
|
260
|
+
fontSize: '16px',
|
|
261
|
+
textTransform: 'capitalize',
|
|
262
|
+
color: 'var(--color-sentiment-content-primary)',
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
{sentiment} - {emphasis}
|
|
266
|
+
</strong>
|
|
267
|
+
</div>
|
|
268
|
+
<div
|
|
269
|
+
style={{
|
|
270
|
+
display: 'grid',
|
|
271
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
|
272
|
+
gap: '24px',
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
{tokenCategories.map((category) => (
|
|
276
|
+
<div key={category.name}>
|
|
277
|
+
<div
|
|
278
|
+
style={{
|
|
279
|
+
fontSize: '12px',
|
|
280
|
+
fontWeight: 600,
|
|
281
|
+
marginBottom: '8px',
|
|
282
|
+
textTransform: 'uppercase',
|
|
283
|
+
letterSpacing: '0.5px',
|
|
284
|
+
opacity: 0.7,
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
{category.name}
|
|
288
|
+
</div>
|
|
289
|
+
{category.tokens.map((token) => (
|
|
290
|
+
<TokenSwatch key={token} token={token} />
|
|
291
|
+
))}
|
|
292
|
+
</div>
|
|
293
|
+
))}
|
|
294
|
+
</div>
|
|
295
|
+
</SentimentSurface>
|
|
296
|
+
)),
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
),
|
|
300
|
+
parameters: {
|
|
301
|
+
controls: { disable: true },
|
|
302
|
+
},
|
|
303
|
+
decorators: [
|
|
304
|
+
(Story: () => JSX.Element) => (
|
|
305
|
+
<div style={{ padding: '2rem', maxWidth: '1200px' }}>
|
|
306
|
+
<Story />
|
|
307
|
+
</div>
|
|
308
|
+
),
|
|
309
|
+
],
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export const AllVariants = storyConfig(
|
|
313
|
+
{
|
|
314
|
+
tags: ['!autodocs'],
|
|
315
|
+
parameters: {
|
|
316
|
+
padding: '0',
|
|
317
|
+
controls: { disable: true },
|
|
318
|
+
},
|
|
319
|
+
render: () => (
|
|
320
|
+
<div
|
|
321
|
+
className="sentiment-surface-variants"
|
|
322
|
+
style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '800px' }}
|
|
323
|
+
>
|
|
324
|
+
{sentiments.map((sentiment) =>
|
|
325
|
+
emphasisLevels.map((emphasis) => (
|
|
326
|
+
<SentimentSurface
|
|
327
|
+
key={`${sentiment}-${emphasis}`}
|
|
328
|
+
sentiment={sentiment}
|
|
329
|
+
emphasis={emphasis}
|
|
330
|
+
style={{ padding: '24px', borderRadius: '16px' }}
|
|
331
|
+
>
|
|
332
|
+
{sentiment} - {emphasis} emphasis
|
|
333
|
+
</SentimentSurface>
|
|
334
|
+
)),
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
),
|
|
338
|
+
},
|
|
339
|
+
{ variants: ['default', 'dark', 'bright-green', 'forest-green'] },
|
|
340
|
+
);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
|
+
import { expect, within } from 'storybook/test';
|
|
3
|
+
import SentimentSurface from './SentimentSurface';
|
|
4
|
+
import { storyConfig } from '../test-utils';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
component: SentimentSurface,
|
|
8
|
+
title: 'Content/SentimentSurface/Tests',
|
|
9
|
+
} satisfies Meta<typeof SentimentSurface>;
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof SentimentSurface>;
|
|
12
|
+
|
|
13
|
+
export const AllSentiments: Story = {
|
|
14
|
+
render: () => (
|
|
15
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
|
16
|
+
<SentimentSurface sentiment="negative">
|
|
17
|
+
Negative sentiment - Your payment has failed
|
|
18
|
+
</SentimentSurface>
|
|
19
|
+
<SentimentSurface sentiment="warning">
|
|
20
|
+
Warning sentiment - Action required on your account
|
|
21
|
+
</SentimentSurface>
|
|
22
|
+
<SentimentSurface sentiment="neutral">
|
|
23
|
+
Neutral sentiment - Your account is up to date
|
|
24
|
+
</SentimentSurface>
|
|
25
|
+
<SentimentSurface sentiment="success">
|
|
26
|
+
Success sentiment - Your payment was successful
|
|
27
|
+
</SentimentSurface>
|
|
28
|
+
<SentimentSurface sentiment="proposition">
|
|
29
|
+
Proposition sentiment - Try our new feature
|
|
30
|
+
</SentimentSurface>
|
|
31
|
+
</div>
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const AllEmphasisLevels: Story = {
|
|
36
|
+
render: () => (
|
|
37
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
|
38
|
+
<SentimentSurface sentiment="neutral" emphasis="base">
|
|
39
|
+
Base emphasis (default)
|
|
40
|
+
</SentimentSurface>
|
|
41
|
+
<SentimentSurface sentiment="neutral" emphasis="elevated">
|
|
42
|
+
Elevated emphasis
|
|
43
|
+
</SentimentSurface>
|
|
44
|
+
</div>
|
|
45
|
+
),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const PolymorphicRendering: Story = {
|
|
49
|
+
play: async ({ canvasElement }) => {
|
|
50
|
+
const canvas = within(canvasElement);
|
|
51
|
+
|
|
52
|
+
// Test that all elements are rendered with correct tags
|
|
53
|
+
const divElement = canvas.getByText('Rendered as div');
|
|
54
|
+
await expect(divElement.tagName).toBe('DIV');
|
|
55
|
+
|
|
56
|
+
const sectionElement = canvas.getByText('Rendered as section');
|
|
57
|
+
await expect(sectionElement.tagName).toBe('SECTION');
|
|
58
|
+
|
|
59
|
+
const articleElement = canvas.getByText('Rendered as article');
|
|
60
|
+
await expect(articleElement.tagName).toBe('ARTICLE');
|
|
61
|
+
},
|
|
62
|
+
render: () => (
|
|
63
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
|
64
|
+
<SentimentSurface sentiment="neutral">Rendered as div</SentimentSurface>
|
|
65
|
+
<SentimentSurface sentiment="success" as="section">
|
|
66
|
+
Rendered as section
|
|
67
|
+
</SentimentSurface>
|
|
68
|
+
<SentimentSurface sentiment="proposition" as="article">
|
|
69
|
+
Rendered as article
|
|
70
|
+
</SentimentSurface>
|
|
71
|
+
</div>
|
|
72
|
+
),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const AllVariants = storyConfig(
|
|
76
|
+
{
|
|
77
|
+
render: () => (
|
|
78
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
|
79
|
+
{(['negative', 'warning', 'neutral', 'success', 'proposition'] as const).map(
|
|
80
|
+
(sentiment) => (
|
|
81
|
+
<div key={sentiment} style={{ display: 'contents' }}>
|
|
82
|
+
<SentimentSurface sentiment={sentiment} emphasis="base">
|
|
83
|
+
{sentiment} - base
|
|
84
|
+
</SentimentSurface>
|
|
85
|
+
<SentimentSurface sentiment={sentiment} emphasis="elevated">
|
|
86
|
+
{sentiment} - elevated
|
|
87
|
+
</SentimentSurface>
|
|
88
|
+
</div>
|
|
89
|
+
),
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
{ variants: ['default', 'dark'] },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
export const NestedSentiments: Story = {
|
|
98
|
+
render: () => (
|
|
99
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
|
100
|
+
<SentimentSurface sentiment="negative" emphasis="base">
|
|
101
|
+
Outer: Negative, base
|
|
102
|
+
<div style={{ margin: '16px 0 0 16px' }}>
|
|
103
|
+
<SentimentSurface sentiment="success" emphasis="elevated">
|
|
104
|
+
Inner: Success, elevated
|
|
105
|
+
<div style={{ margin: '12px 0 0 12px' }}>
|
|
106
|
+
<SentimentSurface sentiment="warning" emphasis="base">
|
|
107
|
+
Deepest: Warning, base
|
|
108
|
+
</SentimentSurface>
|
|
109
|
+
</div>
|
|
110
|
+
</SentimentSurface>
|
|
111
|
+
</div>
|
|
112
|
+
</SentimentSurface>
|
|
113
|
+
<SentimentSurface sentiment="proposition" emphasis="elevated">
|
|
114
|
+
Outer: Proposition, elevated
|
|
115
|
+
<div style={{ margin: '16px 0 0 16px' }}>
|
|
116
|
+
<SentimentSurface sentiment="neutral" emphasis="base">
|
|
117
|
+
Inner: Neutral, base
|
|
118
|
+
</SentimentSurface>
|
|
119
|
+
</div>
|
|
120
|
+
</SentimentSurface>
|
|
121
|
+
</div>
|
|
122
|
+
),
|
|
123
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { forwardRef, ElementType, ForwardedRef } from 'react';
|
|
2
|
+
import { clsx } from 'clsx';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
SentimentSurfaceComponentProps,
|
|
6
|
+
SentimentSurfaceComponent,
|
|
7
|
+
} from './SentimentSurface.types';
|
|
8
|
+
import { getSentimentSurfaceClassName } from './classMap';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* SentimentSurface is a polymorphic container component that applies contextual background colours
|
|
12
|
+
* and text styling based on sentiment types (negative, warning, neutral, success, proposition).
|
|
13
|
+
* It's designed to visually communicate the nature or importance of its content through colour.
|
|
14
|
+
*
|
|
15
|
+
* @param {ElementType} [as='div'] - Optional prop to override the HTML element rendered.
|
|
16
|
+
* @param {Sentiment} sentiment - Required prop to set the sentiment type (negative, warning, neutral, success, proposition).
|
|
17
|
+
* @param {Emphasis} [emphasis='base'] - Optional prop to specify the emphasis level (base or elevated).
|
|
18
|
+
* @param {ReactNode} [children] - Content to render inside the surface.
|
|
19
|
+
* @param {string} [className] - Additional CSS classes to apply.
|
|
20
|
+
* @param {CSSProperties} [style] - Inline styles to apply.
|
|
21
|
+
* @param {string} [id] - Unique identifier for the component.
|
|
22
|
+
* @param {string} [testId] - A unique string that appears as data attribute `data-testid` in the rendered code.
|
|
23
|
+
*
|
|
24
|
+
* @component
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* // Basic usage with negative sentiment
|
|
28
|
+
* <SentimentSurface sentiment="negative">
|
|
29
|
+
* Your payment has failed
|
|
30
|
+
* </SentimentSurface>
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @see {@link SentimentSurface} for further information.
|
|
34
|
+
* @see {@link https://storybook.wise.design/?path=/docs/sentiment-surface--docs|Storybook Wise Design}
|
|
35
|
+
*/
|
|
36
|
+
// @ts-expect-error - Generic forwardRef limitation. See: https://fettblog.eu/typescript-react-generic-forward-refs/
|
|
37
|
+
const SentimentSurface: SentimentSurfaceComponent = forwardRef(function SentimentSurface<
|
|
38
|
+
T extends ElementType = 'div',
|
|
39
|
+
>(
|
|
40
|
+
{
|
|
41
|
+
as,
|
|
42
|
+
sentiment,
|
|
43
|
+
emphasis = 'base',
|
|
44
|
+
className,
|
|
45
|
+
style,
|
|
46
|
+
children,
|
|
47
|
+
id,
|
|
48
|
+
testId,
|
|
49
|
+
...props
|
|
50
|
+
}: SentimentSurfaceComponentProps<T>,
|
|
51
|
+
ref: ForwardedRef<HTMLElement>,
|
|
52
|
+
) {
|
|
53
|
+
const Element = as ?? 'div';
|
|
54
|
+
const classNames = clsx(getSentimentSurfaceClassName(sentiment, emphasis), className);
|
|
55
|
+
const sentimentProps = {
|
|
56
|
+
ref,
|
|
57
|
+
id,
|
|
58
|
+
'data-testid': testId,
|
|
59
|
+
className: classNames,
|
|
60
|
+
style,
|
|
61
|
+
...props,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
// @ts-expect-error - Generic forwardRef limitation. See: https://fettblog.eu/typescript-react-generic-forward-refs/
|
|
66
|
+
<Element {...sentimentProps}>{children}</Element>
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
SentimentSurface.displayName = 'SentimentSurface';
|
|
71
|
+
|
|
72
|
+
export default SentimentSurface;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReactNode,
|
|
3
|
+
ElementType,
|
|
4
|
+
CSSProperties,
|
|
5
|
+
ComponentPropsWithoutRef,
|
|
6
|
+
HTMLAttributes,
|
|
7
|
+
} from 'react';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sentiment types for the SentimentSurface component
|
|
11
|
+
*/
|
|
12
|
+
export type Sentiment = 'negative' | 'warning' | 'neutral' | 'success' | 'proposition';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Emphasis levels for the SentimentSurface component
|
|
16
|
+
*/
|
|
17
|
+
export type Emphasis = 'base' | 'elevated';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Common properties for the SentimentSurface component
|
|
21
|
+
*/
|
|
22
|
+
export interface CommonProps {
|
|
23
|
+
/**
|
|
24
|
+
* The sentiment type that determines the colour scheme
|
|
25
|
+
*/
|
|
26
|
+
sentiment: Sentiment;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The emphasis level affecting background and text contrast
|
|
30
|
+
* @default 'base'
|
|
31
|
+
*/
|
|
32
|
+
emphasis?: Emphasis;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Content to render inside the surface
|
|
36
|
+
*/
|
|
37
|
+
children?: ReactNode;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Additional CSS classes
|
|
41
|
+
*/
|
|
42
|
+
className?: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Inline styles
|
|
46
|
+
*/
|
|
47
|
+
style?: CSSProperties;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Unique identifier for the component
|
|
51
|
+
*/
|
|
52
|
+
id?: string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A unique string that appears as data attribute `data-testid` in the rendered code, serving as a hook for automated tests
|
|
56
|
+
*/
|
|
57
|
+
testId?: string;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `data-testid` is strictly controlled through the `testId` prop.
|
|
61
|
+
* This lets consumers know that this data attribute will not be applied.
|
|
62
|
+
*/
|
|
63
|
+
'data-testid'?: never;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Props when rendering as a div or custom element
|
|
68
|
+
*/
|
|
69
|
+
export type SentimentSurfaceDivProps<T extends ElementType = 'div'> = Omit<
|
|
70
|
+
HTMLAttributes<HTMLDivElement>,
|
|
71
|
+
keyof CommonProps
|
|
72
|
+
> &
|
|
73
|
+
CommonProps & {
|
|
74
|
+
/**
|
|
75
|
+
* The underlying HTML element to render
|
|
76
|
+
* @default 'div'
|
|
77
|
+
*/
|
|
78
|
+
as?: T;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* All possible props for the SentimentSurface component
|
|
83
|
+
*/
|
|
84
|
+
export type SentimentSurfaceProps<T extends ElementType = 'div'> = SentimentSurfaceDivProps<T>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Combines SentimentSurface props with all valid HTML attributes for the specified element type.
|
|
88
|
+
* Supports polymorphic rendering via the `as` prop while maintaining full type safety.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* <SentimentSurface sentiment="negative" role="alert" aria-live="polite">
|
|
92
|
+
* Error message
|
|
93
|
+
* </SentimentSurface>
|
|
94
|
+
*/
|
|
95
|
+
export type SentimentSurfaceComponentProps<T extends ElementType = 'div'> =
|
|
96
|
+
SentimentSurfaceProps<T> & Omit<ComponentPropsWithoutRef<T>, keyof SentimentSurfaceProps<T>>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The SentimentSurface component type signature
|
|
100
|
+
*/
|
|
101
|
+
export interface SentimentSurfaceComponent {
|
|
102
|
+
<T extends ElementType = 'div'>(props: SentimentSurfaceComponentProps<T>): ReactNode;
|
|
103
|
+
displayName?: string;
|
|
104
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Sentiment, Emphasis } from './SentimentSurface.types';
|
|
2
|
+
|
|
3
|
+
const BASE_CLASS = 'wds-sentiment-surface';
|
|
4
|
+
|
|
5
|
+
export const sentimentClassMap: Record<Sentiment, string> = {
|
|
6
|
+
negative: `${BASE_CLASS}-negative`,
|
|
7
|
+
warning: `${BASE_CLASS}-warning`,
|
|
8
|
+
neutral: `${BASE_CLASS}-neutral`,
|
|
9
|
+
success: `${BASE_CLASS}-success`,
|
|
10
|
+
proposition: `${BASE_CLASS}-proposition`,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const getSentimentSurfaceClassName = (sentiment: Sentiment, emphasis: Emphasis): string => {
|
|
14
|
+
return `${BASE_CLASS} ${sentimentClassMap[sentiment]}-${emphasis}`;
|
|
15
|
+
};
|