@transferwise/components 0.0.0-experimental-4af964d → 0.0.0-experimental-04f7ce3

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.
@@ -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
+ };
@@ -1,11 +1,15 @@
1
- import { useState } from 'react';
1
+ import { useState, useEffect } from 'react';
2
2
  import { fn } from 'storybook/test';
3
3
 
4
+ import { storySourceWithoutNoise } from '../../.storybook/helpers';
4
5
  import Switch, { SwitchProps } from './Switch';
5
6
  import { Field } from '../field/Field';
6
7
  import { Meta, StoryObj } from '@storybook/react-webpack5';
7
8
  import { Label } from '../label';
8
9
 
10
+ /**
11
+ * **Design guidance**: <a href="https://wise.design/components/switch" target="_blank">wise.design/components/switch</a>
12
+ */
9
13
  const meta: Meta<typeof Switch> = {
10
14
  component: Switch,
11
15
  title: 'Actions/Switch',
@@ -18,56 +22,83 @@ const meta: Meta<typeof Switch> = {
18
22
  'aria-label': undefined,
19
23
  onClick: fn(),
20
24
  },
25
+ argTypes: {
26
+ id: { description: '', table: { category: 'Common' } },
27
+ className: { description: '', table: { category: 'Common' } },
28
+ },
29
+ parameters: {
30
+ docs: { toc: true },
31
+ },
21
32
  } satisfies Meta<typeof Switch>;
22
33
 
23
34
  export default meta;
24
35
  type Story = StoryObj<typeof Switch>;
25
36
 
26
- export const Basic: Story = {
27
- tags: ['!autodocs', '!dev'],
28
- };
37
+ export const Playground: Story = storySourceWithoutNoise({
38
+ render: function Render(args: SwitchProps) {
39
+ const [checked, setChecked] = useState(args.checked ?? false);
29
40
 
30
- export const Playground: Story = {
31
- tags: ['!autodocs'],
41
+ useEffect(() => {
42
+ setChecked(args.checked ?? false);
43
+ }, [args.checked]);
44
+
45
+ return (
46
+ <Field id="playground-field" label="Switch label">
47
+ <Switch
48
+ {...args}
49
+ checked={checked}
50
+ onClick={() => setChecked((value: boolean) => !value)}
51
+ />
52
+ </Field>
53
+ );
54
+ },
32
55
  argTypes: {
33
56
  onClick: { table: { disable: true } },
34
57
  },
35
- };
58
+ });
36
59
 
37
- export const Interactivity: Story = {
60
+ /**
61
+ * Three labelling patterns: <a href="?path=/docs/forms-label--docs">Label</a>, `aria-labelledby`, and <a href="?path=/docs/forms-field--docs">Field</a>. <br />
62
+ * Use Label for simple label-input pairs. Use `aria-labelledby` to reference existing text as the accessible name. Use Field for form layouts with labels, descriptions, and validation.
63
+ */
64
+ export const LabelPatterns: Story = {
38
65
  render: function Render({ disabled }: SwitchProps) {
39
- const [checked1, setCheck1] = useState(false);
40
- const [checked2, setCheck2] = useState(true);
41
- const [checked3, setCheck3] = useState(false);
66
+ const [checked1, setCheck1] = useState(true);
67
+ const [checked2, setCheck2] = useState(false);
68
+ const [checked3, setCheck3] = useState(true);
42
69
 
43
70
  return (
44
- <>
45
- <Field id="fieldId" label="Using Field component">
46
- <Switch checked={checked1} disabled={disabled} onClick={() => setCheck1(!checked1)} />
47
- </Field>
48
-
71
+ <div className="d-flex flex-column" style={{ gap: '1rem' }}>
49
72
  <div>
50
- <Label htmlFor="labelId">Using Label component</Label>
73
+ <Label htmlFor="labelId">Enable two-factor authentication</Label>
51
74
  <Switch
52
75
  id="labelId"
53
- checked={checked2}
76
+ checked={checked1}
54
77
  disabled={disabled}
55
- onClick={() => setCheck2(!checked2)}
78
+ onClick={() => setCheck1(!checked1)}
56
79
  />
57
80
  </div>
58
81
 
59
82
  <div>
60
83
  <strong id="ariaId" className="d-block">
61
- Using `aria-labelledby`
84
+ Marketing emails
62
85
  </strong>
63
86
  <Switch
64
87
  aria-labelledby="ariaId"
65
- checked={checked3}
88
+ checked={checked2}
66
89
  disabled={disabled}
67
- onClick={() => setCheck3(!checked3)}
90
+ onClick={() => setCheck2(!checked2)}
68
91
  />
69
92
  </div>
70
- </>
93
+
94
+ <Field
95
+ id="fieldId"
96
+ label="Enable notifications"
97
+ description="Get alerts when transfers complete"
98
+ >
99
+ <Switch checked={checked3} disabled={disabled} onClick={() => setCheck3(!checked3)} />
100
+ </Field>
101
+ </div>
71
102
  );
72
103
  },
73
104
  parameters: {
@@ -78,7 +109,7 @@ function Render() {
78
109
  const [checked1, setCheck1] = useState(false);
79
110
  const [checked2, setCheck2] = useState(true);
80
111
  const [checked3, setCheck3] = useState(false);
81
-
112
+
82
113
  return (
83
114
  <>
84
115
  <Field id="fieldId" label="Using Field component">
@@ -87,7 +118,7 @@ function Render() {
87
118
  onClick={() => setCheck1(!checked1)}
88
119
  />
89
120
  </Field>
90
-
121
+
91
122
  <div>
92
123
  <Label htmlFor="labelId">Using standalone Label component</Label>
93
124
  <Switch
@@ -96,7 +127,7 @@ function Render() {
96
127
  onClick={() => setCheck2(!checked2)}
97
128
  />
98
129
  </div>
99
-
130
+
100
131
  <div>
101
132
  <strong id="ariaId" className="d-block">Using \`aria-labelledby\`</strong>
102
133
  <Switch
@@ -111,13 +142,6 @@ function Render() {
111
142
  },
112
143
  },
113
144
  },
114
- decorators: [
115
- (Story: any) => (
116
- <div className="d-flex flex-column" style={{ gap: '1rem' }}>
117
- <Story />
118
- </div>
119
- ),
120
- ],
121
145
  args: {},
122
146
  argTypes: {
123
147
  checked: { table: { disable: true } },
@@ -126,17 +150,17 @@ function Render() {
126
150
  };
127
151
 
128
152
  export const Disabled: Story = {
129
- render: function Render(args) {
153
+ render: function Render() {
130
154
  const [checked1, setCheck1] = useState(false);
131
155
  const [checked2, setCheck2] = useState(true);
132
156
 
133
157
  return (
134
158
  <>
135
- <Field label="Switch label">
159
+ <Field label="Disabled unchecked">
136
160
  <Switch checked={checked1} disabled onClick={() => setCheck1(!checked1)} />
137
161
  </Field>
138
162
 
139
- <Field label="Switch label">
163
+ <Field label="Disabled checked">
140
164
  <Switch checked={checked2} disabled onClick={() => setCheck2(!checked2)} />
141
165
  </Field>
142
166
  </>
@@ -152,14 +176,14 @@ function Render() {
152
176
 
153
177
  return (
154
178
  <>
155
- <Field label="Switch label">
179
+ <Field label="Disabled unchecked">
156
180
  <Switch
157
181
  checked={checked1}
158
182
  disabled
159
183
  onClick={() => setCheck1(!checked1)}
160
184
  />
161
- </Fie
162
- <Field label="Switch label">
185
+ </Field>
186
+ <Field label="Disabled checked">
163
187
  <Switch
164
188
  checked={checked2}
165
189
  disabled