@transferwise/components 46.133.0 → 46.133.1

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 (46) 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 +4 -4
  12. package/build/styles/listItem/ListItem.css +4 -4
  13. package/build/styles/listItem/ListItem.grid.css +3 -3
  14. package/build/styles/main.css +4 -4
  15. package/build/types/chips/Chips.d.ts +1 -1
  16. package/build/types/chips/Chips.d.ts.map +1 -1
  17. package/build/types/common/commonProps.d.ts +0 -6
  18. package/build/types/common/commonProps.d.ts.map +1 -1
  19. package/build/types/label/Label.d.ts.map +1 -1
  20. package/build/types/logo/Logo.d.ts +10 -1
  21. package/build/types/logo/Logo.d.ts.map +1 -1
  22. package/package.json +3 -3
  23. package/src/button/_stories/Button.story.tsx +15 -5
  24. package/src/checkboxButton/CheckboxButton.story.tsx +125 -44
  25. package/src/checkboxButton/CheckboxButton.test.story.tsx +236 -0
  26. package/src/chips/Chips.story.tsx +141 -102
  27. package/src/chips/Chips.test.story.tsx +177 -0
  28. package/src/chips/Chips.tsx +1 -1
  29. package/src/circularButton/CircularButton.story.tsx +261 -49
  30. package/src/circularButton/CircularButton.test.story.tsx +192 -2
  31. package/src/common/commonProps.ts +0 -6
  32. package/src/iconButton/IconButton.story.tsx +315 -110
  33. package/src/iconButton/IconButton.test.story.tsx +217 -44
  34. package/src/label/Label.tsx +1 -2
  35. package/src/listItem/ListItem.css +4 -4
  36. package/src/listItem/ListItem.grid.css +3 -3
  37. package/src/listItem/ListItem.grid.less +5 -3
  38. package/src/listItem/ListItem.less +1 -1
  39. package/src/listItem/ListItem.vars.less +2 -2
  40. package/src/listItem/_stories/ListItem.layout.test.story.tsx +55 -0
  41. package/src/logo/Logo.story.tsx +181 -21
  42. package/src/logo/Logo.test.story.tsx +40 -7
  43. package/src/logo/Logo.tsx +10 -1
  44. package/src/main.css +4 -4
  45. package/src/switch/Switch.story.tsx +64 -42
  46. package/src/switch/Switch.test.story.tsx +123 -0
@@ -1,9 +1,25 @@
1
1
  import { Meta, StoryObj } from '@storybook/react-webpack5';
2
- import { Menu, Plus, Settings, Star, Travel } from '@transferwise/icons';
3
- import { action } from 'storybook/actions';
2
+ import {
3
+ Menu,
4
+ Plus,
5
+ Settings,
6
+ Star,
7
+ Travel,
8
+ Cross,
9
+ Edit,
10
+ Briefcase,
11
+ Bank,
12
+ Freeze,
13
+ } from '@transferwise/icons';
14
+ import { expect, fn, userEvent, within } from 'storybook/test';
4
15
  import IconButton from './IconButton';
16
+ import { withVariantConfig } from '../../.storybook/helpers';
5
17
  import SentimentSurface from '../sentimentSurface';
6
- import { allModes } from '../../.storybook/modes';
18
+
19
+ const wait = async (ms: number) =>
20
+ new Promise<void>((resolve) => {
21
+ setTimeout(resolve, ms);
22
+ });
7
23
 
8
24
  export default {
9
25
  title: 'Actions/IconButton/Tests',
@@ -16,11 +32,19 @@ type Story = StoryObj<typeof IconButton>;
16
32
  /**
17
33
  * IconButton displayed across all themes and sentiments for visual regression testing.
18
34
  */
19
- export const AllThemesAndSentiments: Story = {
35
+ export const Variants: Story = {
20
36
  render: () => {
21
- const priorities = ['primary', 'secondary', 'tertiary', 'minimal', 'disabled'] as const;
22
- const sentiments = ['negative', 'warning', 'neutral', 'success', 'proposition'] as const;
23
- const icons = [Plus, Settings, Star, Travel];
37
+ const size = 32;
38
+ const priorities = [
39
+ { key: 'primary', label: 'primary' },
40
+ { key: 'secondary', label: 'secondary' },
41
+ { key: 'tertiary', label: 'tertiary' },
42
+ { key: 'minimal', label: 'minimal' },
43
+ { key: 'neg-primary', label: 'negative\nprimary' },
44
+ { key: 'neg-secondary', label: 'negative\nsecondary' },
45
+ { key: 'disabled', label: 'disabled' },
46
+ ] as const;
47
+ const sentiments = ['success', 'warning', 'negative', 'neutral', 'proposition'] as const;
24
48
 
25
49
  return (
26
50
  <div style={{ display: 'flex', flexDirection: 'column' }}>
@@ -29,21 +53,23 @@ export const AllThemesAndSentiments: Story = {
29
53
  <div style={{ width: '82px', paddingLeft: '8px' }} />
30
54
  {priorities.map((priority) => (
31
55
  <div
32
- key={priority}
56
+ key={priority.key}
33
57
  style={{
34
- width: '32px',
58
+ width: `${size}px`,
35
59
  textAlign: 'center',
36
60
  fontSize: '11px',
37
61
  fontWeight: 'bold',
38
62
  writingMode: 'vertical-rl',
39
63
  transform: 'rotate(180deg)',
40
- height: '60px',
64
+ height: '80px',
41
65
  display: 'flex',
42
66
  alignItems: 'center',
43
67
  justifyContent: 'center',
68
+ whiteSpace: 'pre',
69
+ lineHeight: '1',
44
70
  }}
45
71
  >
46
- {priority}
72
+ {priority.label}
47
73
  </div>
48
74
  ))}
49
75
  </div>
@@ -68,48 +94,66 @@ export const AllThemesAndSentiments: Story = {
68
94
  {sentiment} (base)
69
95
  </div>
70
96
  <IconButton
71
- size={32}
97
+ size={size}
72
98
  aria-label="Primary action"
73
99
  priority="primary"
74
100
  type="default"
75
- onClick={action('button click')}
101
+ onClick={fn()}
76
102
  >
77
103
  <Plus />
78
104
  </IconButton>
79
105
  <IconButton
80
- size={32}
106
+ size={size}
81
107
  aria-label="Secondary action"
82
108
  priority="secondary"
83
109
  type="default"
84
- onClick={action('button click')}
110
+ onClick={fn()}
85
111
  >
86
112
  <Settings />
87
113
  </IconButton>
88
114
  <IconButton
89
- size={32}
115
+ size={size}
90
116
  aria-label="Tertiary action"
91
117
  priority="tertiary"
92
118
  type="default"
93
- onClick={action('button click')}
119
+ onClick={fn()}
94
120
  >
95
121
  <Star />
96
122
  </IconButton>
97
123
  <IconButton
98
- size={32}
124
+ size={size}
99
125
  aria-label="Minimal action"
100
126
  priority="minimal"
101
127
  type="default"
102
- onClick={action('button click')}
128
+ onClick={fn()}
103
129
  >
104
130
  <Travel />
105
131
  </IconButton>
106
132
  <IconButton
107
- size={32}
133
+ size={size}
134
+ aria-label="Negative primary action"
135
+ priority="primary"
136
+ type="negative"
137
+ onClick={fn()}
138
+ >
139
+ <Cross />
140
+ </IconButton>
141
+ <IconButton
142
+ size={size}
143
+ aria-label="Negative secondary action"
144
+ priority="secondary"
145
+ type="negative"
146
+ onClick={fn()}
147
+ >
148
+ <Edit />
149
+ </IconButton>
150
+ <IconButton
151
+ size={size}
108
152
  aria-label="Disabled action"
109
153
  priority="primary"
110
154
  type="default"
111
155
  disabled
112
- onClick={action('button click')}
156
+ onClick={fn()}
113
157
  >
114
158
  <Menu />
115
159
  </IconButton>
@@ -132,63 +176,192 @@ export const AllThemesAndSentiments: Story = {
132
176
  {sentiment} (elevated)
133
177
  </div>
134
178
  <IconButton
135
- size={32}
179
+ size={size}
136
180
  aria-label="Primary action"
137
181
  priority="primary"
138
182
  type="default"
139
- onClick={action('button click')}
183
+ onClick={fn()}
140
184
  >
141
- <Plus />
185
+ <Briefcase />
142
186
  </IconButton>
143
187
  <IconButton
144
- size={32}
188
+ size={size}
145
189
  aria-label="Secondary action"
146
190
  priority="secondary"
147
191
  type="default"
148
- onClick={action('button click')}
192
+ onClick={fn()}
149
193
  >
150
- <Settings />
194
+ <Bank />
151
195
  </IconButton>
152
196
  <IconButton
153
- size={32}
197
+ size={size}
154
198
  aria-label="Tertiary action"
155
199
  priority="tertiary"
156
200
  type="default"
157
- onClick={action('button click')}
201
+ onClick={fn()}
158
202
  >
159
- <Star />
203
+ <Freeze />
160
204
  </IconButton>
161
205
  <IconButton
162
- size={32}
206
+ size={size}
163
207
  aria-label="Minimal action"
164
208
  priority="minimal"
165
209
  type="default"
166
- onClick={action('button click')}
210
+ onClick={fn()}
167
211
  >
168
- <Travel />
212
+ <Edit />
169
213
  </IconButton>
170
214
  <IconButton
171
- size={32}
215
+ size={size}
216
+ aria-label="Negative primary action"
217
+ priority="primary"
218
+ type="negative"
219
+ onClick={fn()}
220
+ >
221
+ <Cross />
222
+ </IconButton>
223
+ <IconButton
224
+ size={size}
225
+ aria-label="Negative secondary action"
226
+ priority="secondary"
227
+ type="negative"
228
+ onClick={fn()}
229
+ >
230
+ <Edit />
231
+ </IconButton>
232
+ <IconButton
233
+ size={size}
172
234
  aria-label="Disabled action"
173
235
  priority="primary"
174
236
  type="default"
175
237
  disabled
176
- onClick={action('button click')}
238
+ onClick={fn()}
177
239
  >
178
- <Menu />
240
+ <Cross />
179
241
  </IconButton>
180
242
  </SentimentSurface>,
181
243
  ])}
182
244
  </div>
183
245
  );
184
246
  },
185
- parameters: {
186
- padding: '16px',
187
- variants: ['default', 'dark', 'bright-green', 'forest-green'],
188
- chromatic: {
189
- dark: allModes.dark,
190
- brightGreen: allModes.brightGreen,
191
- forestGreen: allModes.forestGreen,
192
- },
247
+ ...withVariantConfig(['default', 'dark', 'bright-green', 'forest-green']),
248
+ };
249
+
250
+ /** Tab through each priority variant and activate with Space. */
251
+ export const KeyboardInteraction: Story = {
252
+ render: () => (
253
+ <div style={{ display: 'flex', gap: '12px', padding: '16px' }}>
254
+ <IconButton size={48} aria-label="Primary" priority="primary" type="default" onClick={fn()}>
255
+ <Plus />
256
+ </IconButton>
257
+ <IconButton
258
+ size={48}
259
+ aria-label="Secondary"
260
+ priority="secondary"
261
+ type="default"
262
+ onClick={fn()}
263
+ >
264
+ <Settings />
265
+ </IconButton>
266
+ <IconButton size={48} aria-label="Tertiary" priority="tertiary" type="default" onClick={fn()}>
267
+ <Star />
268
+ </IconButton>
269
+ <IconButton size={48} aria-label="Minimal" priority="minimal" type="default" onClick={fn()}>
270
+ <Travel />
271
+ </IconButton>
272
+ <IconButton
273
+ size={48}
274
+ aria-label="Disabled"
275
+ priority="primary"
276
+ type="default"
277
+ disabled
278
+ onClick={fn()}
279
+ >
280
+ <Menu />
281
+ </IconButton>
282
+ </div>
283
+ ),
284
+ play: async ({ canvasElement, step }) => {
285
+ const canvas = within(canvasElement);
286
+ const buttons = canvas.getAllByRole('button');
287
+
288
+ await step('tab to Primary and press Space', async () => {
289
+ await userEvent.tab();
290
+ await expect(buttons[0]).toHaveFocus();
291
+ await wait(400);
292
+ await userEvent.keyboard(' ');
293
+ });
294
+
295
+ await wait(400);
296
+
297
+ await step('tab to Secondary and press Space', async () => {
298
+ await userEvent.tab();
299
+ await expect(buttons[1]).toHaveFocus();
300
+ await wait(400);
301
+ await userEvent.keyboard(' ');
302
+ });
303
+
304
+ await wait(400);
305
+
306
+ await step('tab to Tertiary and press Space', async () => {
307
+ await userEvent.tab();
308
+ await expect(buttons[2]).toHaveFocus();
309
+ await wait(400);
310
+ await userEvent.keyboard(' ');
311
+ });
312
+
313
+ await wait(400);
314
+
315
+ await step('tab to Minimal and press Space', async () => {
316
+ await userEvent.tab();
317
+ await expect(buttons[3]).toHaveFocus();
318
+ await wait(400);
319
+ await userEvent.keyboard(' ');
320
+ });
321
+
322
+ await wait(400);
323
+
324
+ await step('tab skips Disabled button', async () => {
325
+ await userEvent.tab();
326
+ // Disabled button should be skipped
327
+ await expect(buttons[4]).not.toHaveFocus();
328
+ });
193
329
  },
194
330
  };
331
+
332
+ /** IconButton at 400% zoom for accessibility testing. */
333
+ export const Zoom400: Story = {
334
+ render: () => (
335
+ <div style={{ display: 'flex', gap: '8px', padding: '16px', flexWrap: 'wrap' }}>
336
+ <IconButton size={40} aria-label="Primary" priority="primary" type="default" onClick={fn()}>
337
+ <Plus />
338
+ </IconButton>
339
+ <IconButton
340
+ size={40}
341
+ aria-label="Secondary"
342
+ priority="secondary"
343
+ type="default"
344
+ onClick={fn()}
345
+ >
346
+ <Settings />
347
+ </IconButton>
348
+ <IconButton size={40} aria-label="Tertiary" priority="tertiary" type="default" onClick={fn()}>
349
+ <Star />
350
+ </IconButton>
351
+ <IconButton size={40} aria-label="Minimal" priority="minimal" type="default" onClick={fn()}>
352
+ <Travel />
353
+ </IconButton>
354
+ <IconButton
355
+ size={40}
356
+ aria-label="Disabled"
357
+ disabled
358
+ priority="primary"
359
+ type="default"
360
+ onClick={fn()}
361
+ >
362
+ <Menu />
363
+ </IconButton>
364
+ </div>
365
+ ),
366
+ ...withVariantConfig(['400%']),
367
+ };
@@ -39,8 +39,6 @@ const Label = forwardRef<HTMLLabelElement, LabelProps>(
39
39
  },
40
40
  );
41
41
 
42
- Label.displayName = 'Label';
43
-
44
42
  export type LabelOptionalProps = PropsWithChildren<CommonProps>;
45
43
 
46
44
  const Optional = function Optional({ children, className }: LabelOptionalProps) {
@@ -68,4 +66,5 @@ const Description = function Description({ id, children, className }: LabelDescr
68
66
  // eslint-disable-next-line functional/immutable-data
69
67
  const LabelNamespace = Object.assign(Label, { Optional, Description });
70
68
 
69
+ LabelNamespace.displayName = 'Label';
71
70
  export { LabelNamespace as Label };
@@ -9,7 +9,7 @@
9
9
  .wds-list-item-gridWrapper.wds-list-item-hasMedia-noControl.wds-list-item-noInfo-hasPrompt:not(:has(.wds-list-item-subtitle-value, .wds-list-item-subtitle)) .wds-list-item-prompt {
10
10
  margin-top: -2px;
11
11
  }
12
- @container (min-width: 309px) {
12
+ @container (width > 308px) {
13
13
  .wds-list-item-gridWrapper .wds-list-item-control-wrapper {
14
14
  height: var(--wds-list-item-control-wrapper-height);
15
15
  align-content: center;
@@ -105,7 +105,7 @@
105
105
  grid-template-areas: "body";
106
106
  }
107
107
  }
108
- @container (min-width: 241px) and (max-width: 308px) {
108
+ @container (240px < width <= 308px) {
109
109
  .wds-list-item-gridWrapper .wds-list-item-media-image {
110
110
  -o-object-position: bottom left;
111
111
  object-position: bottom left;
@@ -236,7 +236,7 @@
236
236
  margin-top: var(--size-4);
237
237
  }
238
238
  }
239
- @container (max-width: 240px) {
239
+ @container (width <= 240px) {
240
240
  .wds-list-item-gridWrapper .wds-list-item-control-wrapper {
241
241
  align-content: start;
242
242
  }
@@ -531,7 +531,7 @@
531
531
  flex-direction: column;
532
532
  justify-content: center;
533
533
  }
534
- @container (min-width: 309px) {
534
+ @container (width > 308px) {
535
535
  .wds-list-item-titles,
536
536
  .wds-list-item-value {
537
537
  min-height: 100%;
@@ -9,7 +9,7 @@
9
9
  .wds-list-item-gridWrapper.wds-list-item-hasMedia-noControl.wds-list-item-noInfo-hasPrompt:not(:has(.wds-list-item-subtitle-value, .wds-list-item-subtitle)) .wds-list-item-prompt {
10
10
  margin-top: -2px;
11
11
  }
12
- @container (min-width: 309px) {
12
+ @container (width > 308px) {
13
13
  .wds-list-item-gridWrapper .wds-list-item-control-wrapper {
14
14
  height: var(--wds-list-item-control-wrapper-height);
15
15
  align-content: center;
@@ -105,7 +105,7 @@
105
105
  grid-template-areas: "body";
106
106
  }
107
107
  }
108
- @container (min-width: 241px) and (max-width: 308px) {
108
+ @container (240px < width <= 308px) {
109
109
  .wds-list-item-gridWrapper .wds-list-item-media-image {
110
110
  -o-object-position: bottom left;
111
111
  object-position: bottom left;
@@ -236,7 +236,7 @@
236
236
  margin-top: var(--size-4);
237
237
  }
238
238
  }
239
- @container (max-width: 240px) {
239
+ @container (width <= 240px) {
240
240
  .wds-list-item-gridWrapper .wds-list-item-control-wrapper {
241
241
  align-content: start;
242
242
  }
@@ -14,7 +14,7 @@
14
14
  }
15
15
  }
16
16
 
17
- @container (min-width: unit(@wds-list-item-cq-max + 1, px)) {
17
+ @container (width > @wds-list-item-cq-max) {
18
18
  .wds-list-item-control-wrapper {
19
19
  height: var(--wds-list-item-control-wrapper-height);
20
20
  align-content: center;
@@ -169,7 +169,9 @@
169
169
  }
170
170
  }
171
171
 
172
- @container (min-width: unit(@wds-list-item-cq-min + 1, px)) and (max-width: unit(@wds-list-item-cq-max, px)) {
172
+ // LESS struggles with ranged container queries so
173
+ // we need to escape the interpolated expression.
174
+ @container (~"@{wds-list-item-cq-min} < width <= @{wds-list-item-cq-max}") {
173
175
  .wds-list-item-media-image{
174
176
  object-position: bottom left;
175
177
  }
@@ -404,7 +406,7 @@
404
406
  }
405
407
  }
406
408
 
407
- @container (max-width: unit(@wds-list-item-cq-min, px)) {
409
+ @container (width <= @wds-list-item-cq-min) {
408
410
  .wds-list-item-control-wrapper {
409
411
  align-content: start;
410
412
  }
@@ -189,7 +189,7 @@
189
189
  flex-direction: column;
190
190
  justify-content: center;
191
191
 
192
- @container (min-width: unit(@wds-list-item-cq-max + 1, px)) {
192
+ @container (width > @wds-list-item-cq-max) {
193
193
  min-height: var(--wds-list-item-value-min-height, 100%);
194
194
  }
195
195
  }
@@ -7,5 +7,5 @@
7
7
  // `packages/components/src/listItem/constants.ts`
8
8
  //
9
9
  // @see https://storybook.wise.design/?path=/story/content-listitem--responsiveness
10
- @wds-list-item-cq-min: 240;
11
- @wds-list-item-cq-max: 308;
10
+ @wds-list-item-cq-min: 240px;
11
+ @wds-list-item-cq-max: 308px;
@@ -225,3 +225,58 @@ export const ImageAlignment: StoryObj<PreviewStoryArgs> = {
225
225
  );
226
226
  },
227
227
  };
228
+
229
+ // Our container queries left 1px gap between breakpoint definitions.
230
+ // This test makes sure that LI placed in a parent of {width} still
231
+ // works fine, where: {breakpoint} < {width} < {breakpoint} + 1px
232
+ export const SubPixelWidths: StoryObj<PreviewStoryArgs> = {
233
+ parameters: {
234
+ controls: { disable: false },
235
+ },
236
+ args: {
237
+ title: lorem5,
238
+ subtitle: lorem10,
239
+ previewImageSize: 48,
240
+ previewWithLineGuides: true,
241
+ previewPrompt: false,
242
+ },
243
+ argTypes: previewArgTypes,
244
+ render: (args: PreviewStoryArgs) => {
245
+ const [props] = getPropsForPreview(args);
246
+
247
+ return (
248
+ <>
249
+ <List
250
+ style={{
251
+ display: 'grid',
252
+ gridTemplateColumns: '1fr 1fr',
253
+ width: `${2 * LISTITEM_CQ.MIN + 1}px`,
254
+ }}
255
+ >
256
+ <ListItem
257
+ {...props}
258
+ title={`This list item is ${LISTITEM_CQ.MIN + 0.5}px wide`}
259
+ subtitle="this translates to '@wds-list-item-cq-min + 0.5px'"
260
+ media={<ListItem.Image src={portraitImage} alt="portrait image" />}
261
+ />
262
+ <li />
263
+ </List>
264
+ <List
265
+ style={{
266
+ display: 'grid',
267
+ gridTemplateColumns: '1fr 1fr',
268
+ width: `${2 * LISTITEM_CQ.MAX + 1}px`,
269
+ }}
270
+ >
271
+ <ListItem
272
+ {...props}
273
+ title={`This list item is ${LISTITEM_CQ.MAX + 0.5}px wide`}
274
+ subtitle="this translates to '@wds-list-item-cq-max + 0.5px'"
275
+ media={<ListItem.Image src={portraitImage} alt="portrait image" />}
276
+ />
277
+ <li />
278
+ </List>
279
+ </>
280
+ );
281
+ },
282
+ };