@transferwise/components 0.0.0-experimental-ffbd2fc → 0.0.0-experimental-d24c870
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/package.json +1 -1
- package/src/alert/Alert.story.tsx +0 -6
- package/src/button/_stories/Button.story.tsx +0 -5
- package/src/checkboxButton/CheckboxButton.story.tsx +40 -117
- package/src/chips/Chips.story.tsx +88 -177
- package/src/circularButton/CircularButton.story.tsx +4 -23
- package/src/circularButton/CircularButton.test.story.tsx +1 -119
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.story.tsx +0 -1
- package/src/header/Header.story.tsx +0 -5
- package/src/iconButton/IconButton.story.tsx +1 -45
- package/src/iconButton/IconButton.test.story.tsx +2 -163
- package/src/inputs/SelectInput/_stories/SelectInput.story.tsx +0 -1
- package/src/listItem/AdditionalInfo/ListItemAdditionalInfo.story.tsx +1 -5
- package/src/listItem/AvatarLayout/ListItemAvatarLayout.story.tsx +1 -5
- package/src/listItem/AvatarView/ListItemAvatarView.story.tsx +1 -5
- package/src/listItem/Button/ListItemButton.story.tsx +1 -5
- package/src/listItem/Checkbox/ListItemCheckbox.story.tsx +1 -5
- package/src/listItem/IconButton/ListItemIconButton.story.tsx +1 -5
- package/src/listItem/Image/ListItemImage.story.tsx +1 -5
- package/src/listItem/Navigation/ListItemNavigation.story.tsx +1 -5
- package/src/listItem/Prompt/ListItemPrompt.story.tsx +1 -5
- package/src/listItem/Radio/ListItemRadio.story.tsx +1 -5
- package/src/listItem/Switch/ListItemSwitch.story.tsx +1 -5
- package/src/listItem/_stories/ListItem.disabled.story.tsx +0 -1
- package/src/listItem/_stories/ListItem.scenarios.story.tsx +0 -1
- package/src/listItem/_stories/ListItem.story.tsx +1 -5
- package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +0 -5
- package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +0 -5
- package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +0 -5
- package/src/provider/theme/ThemeProvider.story.tsx +8 -0
- package/src/sentimentSurface/SentimentSurface.story.tsx +0 -5
- package/src/switch/Switch.story.tsx +25 -45
- package/src/tokens/tokens.story.tsx +1 -1
- package/src/checkboxButton/CheckboxButton.test.story.tsx +0 -191
- package/src/chips/Chips.test.story.tsx +0 -147
- package/src/switch/Switch.test.story.tsx +0 -101
package/package.json
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
1
|
import { action } from 'storybook/actions';
|
|
3
2
|
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
3
|
+
import { useState } from 'react';
|
|
4
4
|
|
|
5
5
|
import CheckboxButton from './CheckboxButton';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* A styled checkbox input for selecting zero, one, or multiple items from a list,
|
|
9
|
-
* or confirming agreement to terms. Extends all standard `<input type="checkbox">`
|
|
10
|
-
* attributes and adds an `indeterminate` prop for mixed-state scenarios (e.g. a
|
|
11
|
-
* parent checkbox where only some children are selected). The `indeterminate`
|
|
12
|
-
* state is purely visual — it does not change the underlying `checked` value.
|
|
13
|
-
*
|
|
14
|
-
* Always pair with an accessible label via `aria-label`, `aria-labelledby`, or
|
|
15
|
-
* a visible `<label>` element.
|
|
16
|
-
*
|
|
17
|
-
* Keyboard: `Space` toggles the checkbox when focused.
|
|
18
|
-
*
|
|
19
|
-
* **Design guide**: [wise.design/components/checkbox](https://wise.design/components/checkbox)
|
|
20
|
-
*/
|
|
21
7
|
export default {
|
|
22
8
|
component: CheckboxButton,
|
|
23
9
|
title: 'Actions/CheckboxButton',
|
|
@@ -28,118 +14,55 @@ export default {
|
|
|
28
14
|
onClick: action('click'),
|
|
29
15
|
onFocus: action('focus'),
|
|
30
16
|
},
|
|
31
|
-
parameters: {
|
|
32
|
-
docs: { toc: true },
|
|
33
|
-
},
|
|
34
17
|
} satisfies Meta<typeof CheckboxButton>;
|
|
35
18
|
|
|
36
19
|
type Story = StoryObj<typeof CheckboxButton>;
|
|
37
20
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
docs: {
|
|
42
|
-
source: {
|
|
43
|
-
code: `<label>
|
|
44
|
-
<CheckboxButton checked={false} onChange={handleChange} />
|
|
45
|
-
Unchecked
|
|
46
|
-
</label>
|
|
47
|
-
|
|
48
|
-
<label>
|
|
49
|
-
<CheckboxButton checked onChange={handleChange} />
|
|
50
|
-
Checked
|
|
51
|
-
</label>
|
|
52
|
-
|
|
53
|
-
<label>
|
|
54
|
-
<CheckboxButton checked={false} indeterminate onChange={handleChange} />
|
|
55
|
-
Indeterminate
|
|
56
|
-
</label>
|
|
57
|
-
|
|
58
|
-
<label>
|
|
59
|
-
<CheckboxButton checked={false} disabled onChange={handleChange} />
|
|
60
|
-
Disabled unchecked
|
|
61
|
-
</label>
|
|
62
|
-
|
|
63
|
-
<label>
|
|
64
|
-
<CheckboxButton checked disabled onChange={handleChange} />
|
|
65
|
-
Disabled checked
|
|
66
|
-
</label>`,
|
|
67
|
-
},
|
|
68
|
-
},
|
|
21
|
+
export const Basic: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
'aria-label': 'Toggle email updates',
|
|
69
24
|
},
|
|
70
|
-
render: () => {
|
|
71
|
-
const [unchecked, setUnchecked] = useState(false);
|
|
25
|
+
render: (args) => {
|
|
72
26
|
const [checked, setChecked] = useState(true);
|
|
73
|
-
const [indeterminateChecked, setIndeterminateChecked] = useState(false);
|
|
74
|
-
const [isIndeterminate, setIsIndeterminate] = useState(true);
|
|
75
|
-
const [disabledUnchecked] = useState(false);
|
|
76
|
-
const [disabledChecked] = useState(true);
|
|
77
27
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
gap: '8px',
|
|
82
|
-
cursor: 'pointer',
|
|
83
|
-
};
|
|
28
|
+
return <CheckboxButton {...args} checked={checked} onChange={() => setChecked(!checked)} />;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
84
31
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The `indeterminate` state is predominantly visual and should match the [native HTML checkbox
|
|
34
|
+
* implementation](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/indeterminate).
|
|
35
|
+
* It is used to indicate a mixed state, where some but not all items are selected, but does not
|
|
36
|
+
* change the `checked` state of the input itself – it remains either checked or unchecked.
|
|
37
|
+
*
|
|
38
|
+
* In the example below the 1st checkbox is unchecked and the 2nd checkbox is checked.
|
|
39
|
+
*/
|
|
40
|
+
export const Indeterminate: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
indeterminate: true,
|
|
43
|
+
},
|
|
44
|
+
render: (args) => {
|
|
45
|
+
const [checked1, setChecked1] = useState<boolean | undefined>(false);
|
|
46
|
+
const [checked2, setChecked2] = useState<boolean | undefined>(true);
|
|
89
47
|
|
|
90
48
|
return (
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
<div style={labelStyle}>
|
|
111
|
-
<CheckboxButton
|
|
112
|
-
aria-label="Indeterminate"
|
|
113
|
-
checked={indeterminateChecked}
|
|
114
|
-
indeterminate={isIndeterminate}
|
|
115
|
-
onChange={() => {
|
|
116
|
-
setIsIndeterminate(false);
|
|
117
|
-
setIndeterminateChecked((v: boolean) => !v);
|
|
118
|
-
}}
|
|
119
|
-
/>
|
|
120
|
-
<span style={captionStyle}>Indeterminate</span>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
<div style={{ ...labelStyle, cursor: 'not-allowed', opacity: 0.6 }}>
|
|
124
|
-
<CheckboxButton
|
|
125
|
-
aria-label="Disabled unchecked"
|
|
126
|
-
checked={disabledUnchecked}
|
|
127
|
-
disabled
|
|
128
|
-
onChange={() => {}}
|
|
129
|
-
/>
|
|
130
|
-
<span style={captionStyle}>Disabled unchecked</span>
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
|
-
<div style={{ ...labelStyle, cursor: 'not-allowed', opacity: 0.6 }}>
|
|
134
|
-
<CheckboxButton
|
|
135
|
-
aria-label="Disabled checked"
|
|
136
|
-
checked={disabledChecked}
|
|
137
|
-
disabled
|
|
138
|
-
onChange={() => {}}
|
|
139
|
-
/>
|
|
140
|
-
<span style={captionStyle}>Disabled checked</span>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
49
|
+
<>
|
|
50
|
+
<CheckboxButton
|
|
51
|
+
{...args}
|
|
52
|
+
checked={checked1}
|
|
53
|
+
indeterminate={args.indeterminate}
|
|
54
|
+
aria-label="Initially disabled checkbox with indeterminate state"
|
|
55
|
+
onChange={() => setChecked1((current) => !current)}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<CheckboxButton
|
|
59
|
+
{...args}
|
|
60
|
+
checked={checked2}
|
|
61
|
+
indeterminate={args.indeterminate}
|
|
62
|
+
aria-label="Initially enabled checkbox with indeterminate state"
|
|
63
|
+
onChange={() => setChecked2((current) => !current)}
|
|
64
|
+
/>
|
|
65
|
+
</>
|
|
143
66
|
);
|
|
144
67
|
},
|
|
145
68
|
};
|
|
@@ -1,201 +1,112 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { fn } from 'storybook/test';
|
|
1
|
+
import { StoryFn, Meta } from '@storybook/react-webpack5';
|
|
3
2
|
import { useState } from 'react';
|
|
4
3
|
|
|
5
|
-
import Chips, {
|
|
4
|
+
import Chips, { ChipsProps, ChipValue } from './Chips';
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
* Chips allow users to filter content or make a choice from a set of options
|
|
9
|
-
* within a compact area. Two modes are supported:
|
|
10
|
-
*
|
|
11
|
-
* - **Filter** (`multiple`): zero or more chips can be active; selected chips
|
|
12
|
-
* gain a dismiss button. Unselected by default.
|
|
13
|
-
* - **Choice** (default): exactly one chip active at a time, like a radio group.
|
|
14
|
-
* Best practice is to pre-select the most common option.
|
|
15
|
-
*
|
|
16
|
-
* Place chips in a horizontal row, typically beneath a search field or above the
|
|
17
|
-
* content they filter. Always use chips in a set — never a single chip on its own.
|
|
18
|
-
*
|
|
19
|
-
* Keyboard: `Tab` moves between chips; `Space` or `Enter` toggles selection.
|
|
20
|
-
*
|
|
21
|
-
* **Design guide**: [wise.design/components/chip](https://wise.design/components/chip)
|
|
22
|
-
*/
|
|
23
|
-
export default {
|
|
24
|
-
component: Chips,
|
|
6
|
+
const meta: Meta<typeof Chips> = {
|
|
25
7
|
title: 'Actions/Chips',
|
|
26
|
-
|
|
27
|
-
onChange: fn(),
|
|
28
|
-
},
|
|
29
|
-
argTypes: {
|
|
30
|
-
selected: { table: { disable: true } },
|
|
31
|
-
},
|
|
32
|
-
parameters: {
|
|
33
|
-
docs: { toc: true },
|
|
34
|
-
},
|
|
35
|
-
} satisfies Meta<typeof Chips>;
|
|
36
|
-
|
|
37
|
-
type Story = StoryObj<typeof Chips>;
|
|
38
|
-
|
|
39
|
-
const categoryChips = [
|
|
40
|
-
{ value: 'all', label: 'All' },
|
|
41
|
-
{ value: 'accounting', label: 'Accounting' },
|
|
42
|
-
{ value: 'payroll', label: 'Payroll' },
|
|
43
|
-
{ value: 'reporting', label: 'Reporting' },
|
|
44
|
-
{ value: 'payments', label: 'Payments' },
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
const amountChips = [
|
|
48
|
-
{ value: 100, label: '100 GBP' },
|
|
49
|
-
{ value: 200, label: '200 GBP' },
|
|
50
|
-
{ value: 300, label: '300 GBP' },
|
|
51
|
-
{ value: 500, label: '500 GBP+' },
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Toggle `multiple` in the controls panel to switch between filter (multi-select)
|
|
56
|
-
* and choice (single-select) modes.
|
|
57
|
-
*/
|
|
58
|
-
export const Playground: Story = {
|
|
59
|
-
render: function Render({ onChange, chips, multiple }) {
|
|
60
|
-
const [selectedMulti, setSelectedMulti] = useState<readonly ChipValue[]>(['accounting']);
|
|
61
|
-
const [selectedSingle, setSelectedSingle] = useState<ChipValue>('accounting');
|
|
62
|
-
|
|
63
|
-
if (multiple) {
|
|
64
|
-
return (
|
|
65
|
-
<Chips
|
|
66
|
-
chips={chips}
|
|
67
|
-
multiple
|
|
68
|
-
selected={selectedMulti}
|
|
69
|
-
aria-label="Category filter"
|
|
70
|
-
onChange={({ selectedValue, isEnabled }) => {
|
|
71
|
-
setSelectedMulti((prev) =>
|
|
72
|
-
isEnabled ? [...prev, selectedValue] : prev.filter((v) => v !== selectedValue),
|
|
73
|
-
);
|
|
74
|
-
onChange({ selectedValue, isEnabled });
|
|
75
|
-
}}
|
|
76
|
-
/>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<Chips
|
|
82
|
-
chips={chips}
|
|
83
|
-
selected={selectedSingle}
|
|
84
|
-
aria-label="Category choice"
|
|
85
|
-
onChange={({ selectedValue, isEnabled }) => {
|
|
86
|
-
setSelectedSingle(selectedValue);
|
|
87
|
-
onChange({ selectedValue, isEnabled });
|
|
88
|
-
}}
|
|
89
|
-
/>
|
|
90
|
-
);
|
|
91
|
-
},
|
|
92
|
-
args: {
|
|
93
|
-
chips: categoryChips,
|
|
94
|
-
multiple: true,
|
|
95
|
-
},
|
|
8
|
+
component: Chips,
|
|
96
9
|
};
|
|
10
|
+
export default meta;
|
|
97
11
|
|
|
98
|
-
|
|
99
|
-
* Filter chips allow multiple selections simultaneously. Used to filter a list of
|
|
100
|
-
* results by one or more categories — e.g. an app marketplace or payment list.
|
|
101
|
-
*/
|
|
102
|
-
export const Filter: Story = {
|
|
103
|
-
render: function Render({ onChange, ...args }) {
|
|
104
|
-
const [selected, setSelected] = useState<readonly ChipValue[]>(['accounting', 'payments']);
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<Chips
|
|
108
|
-
{...args}
|
|
109
|
-
selected={selected}
|
|
110
|
-
multiple
|
|
111
|
-
aria-label="Category filter"
|
|
112
|
-
onChange={({ selectedValue, isEnabled }) => {
|
|
113
|
-
setSelected((prev) =>
|
|
114
|
-
isEnabled ? [...prev, selectedValue] : prev.filter((v) => v !== selectedValue),
|
|
115
|
-
);
|
|
116
|
-
onChange({ selectedValue, isEnabled });
|
|
117
|
-
}}
|
|
118
|
-
/>
|
|
119
|
-
);
|
|
120
|
-
},
|
|
121
|
-
args: {
|
|
122
|
-
chips: categoryChips,
|
|
123
|
-
},
|
|
124
|
-
parameters: {
|
|
125
|
-
docs: {
|
|
126
|
-
source: {
|
|
127
|
-
code: `function FilterChips() {
|
|
128
|
-
const [selected, setSelected] = useState(['accounting', 'payments']);
|
|
12
|
+
type Story = StoryFn<ChipsProps>;
|
|
129
13
|
|
|
14
|
+
const FilterTemplate: Story = (args: ChipsProps) => {
|
|
15
|
+
const [selected, setSelected] = useState<readonly ChipValue[]>(
|
|
16
|
+
args.selected == null || Array.isArray(args.selected) ? args.selected : [args.selected],
|
|
17
|
+
);
|
|
130
18
|
return (
|
|
131
19
|
<Chips
|
|
132
|
-
|
|
133
|
-
{ value: 'all', label: 'All' },
|
|
134
|
-
{ value: 'accounting', label: 'Accounting' },
|
|
135
|
-
{ value: 'payroll', label: 'Payroll' },
|
|
136
|
-
{ value: 'reporting', label: 'Reporting' },
|
|
137
|
-
{ value: 'payments', label: 'Payments' },
|
|
138
|
-
]}
|
|
139
|
-
multiple
|
|
20
|
+
{...args}
|
|
140
21
|
selected={selected}
|
|
141
|
-
|
|
22
|
+
multiple
|
|
142
23
|
onChange={({ selectedValue, isEnabled }) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
24
|
+
if (isEnabled) {
|
|
25
|
+
setSelected([...selected, selectedValue]);
|
|
26
|
+
} else {
|
|
27
|
+
const updatedSelected = selected.filter((value) => selectedValue !== value);
|
|
28
|
+
setSelected(updatedSelected);
|
|
29
|
+
}
|
|
146
30
|
}}
|
|
147
31
|
/>
|
|
148
32
|
);
|
|
149
|
-
}`,
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
33
|
};
|
|
154
34
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
* Useful for selecting a single value from a small set, such as a transfer amount.
|
|
158
|
-
*/
|
|
159
|
-
export const Choice: Story = {
|
|
160
|
-
render: function Render({ onChange, ...args }) {
|
|
161
|
-
const [selected, setSelected] = useState<ChipValue>(300);
|
|
162
|
-
|
|
163
|
-
return (
|
|
164
|
-
<Chips
|
|
165
|
-
{...args}
|
|
166
|
-
selected={selected}
|
|
167
|
-
aria-label="Transfer amount"
|
|
168
|
-
onChange={({ selectedValue, isEnabled }) => {
|
|
169
|
-
setSelected(selectedValue);
|
|
170
|
-
onChange({ selectedValue, isEnabled });
|
|
171
|
-
}}
|
|
172
|
-
/>
|
|
173
|
-
);
|
|
174
|
-
},
|
|
175
|
-
args: {
|
|
176
|
-
chips: amountChips,
|
|
177
|
-
},
|
|
178
|
-
parameters: {
|
|
179
|
-
docs: {
|
|
180
|
-
source: {
|
|
181
|
-
code: `function ChoiceChips() {
|
|
182
|
-
const [selected, setSelected] = useState(300);
|
|
183
|
-
|
|
35
|
+
const ChoiceTemplate: Story = (args: ChipsProps) => {
|
|
36
|
+
const [selected, setSelected] = useState<ChipValue>(args.selected as ChipValue);
|
|
184
37
|
return (
|
|
185
38
|
<Chips
|
|
186
|
-
|
|
187
|
-
{ value: 100, label: '100 GBP' },
|
|
188
|
-
{ value: 200, label: '200 GBP' },
|
|
189
|
-
{ value: 300, label: '300 GBP' },
|
|
190
|
-
{ value: 500, label: '500 GBP+' },
|
|
191
|
-
]}
|
|
39
|
+
{...args}
|
|
192
40
|
selected={selected}
|
|
193
|
-
aria-label="Transfer amount"
|
|
194
41
|
onChange={({ selectedValue }) => setSelected(selectedValue)}
|
|
195
42
|
/>
|
|
196
43
|
);
|
|
197
|
-
}
|
|
198
|
-
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const FilterChips: typeof FilterTemplate = FilterTemplate.bind({});
|
|
47
|
+
FilterChips.args = {
|
|
48
|
+
chips: [
|
|
49
|
+
{
|
|
50
|
+
value: 'accounting',
|
|
51
|
+
label: 'Accounting',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
value: 'payroll',
|
|
55
|
+
label: 'Payroll',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
value: 'reporting',
|
|
59
|
+
label: 'Reporting',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
value: 'payments',
|
|
63
|
+
label: 'Payments',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const PreSelectedFilterChips: typeof FilterTemplate = FilterTemplate.bind({});
|
|
69
|
+
PreSelectedFilterChips.args = {
|
|
70
|
+
chips: [
|
|
71
|
+
{
|
|
72
|
+
value: 'accounting',
|
|
73
|
+
label: 'Accounting',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
value: 'payroll',
|
|
77
|
+
label: 'Payroll',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
value: 'reporting',
|
|
81
|
+
label: 'Reporting',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
value: 'payments',
|
|
85
|
+
label: 'Payments',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
selected: ['accounting', 'payments'],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const ChoiceChips: typeof ChoiceTemplate = ChoiceTemplate.bind({});
|
|
92
|
+
ChoiceChips.args = {
|
|
93
|
+
chips: [
|
|
94
|
+
{
|
|
95
|
+
value: 1,
|
|
96
|
+
label: '100 GBP',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
value: 2,
|
|
100
|
+
label: '200 GBP',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
value: 3,
|
|
104
|
+
label: '300 GBP',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
value: 4,
|
|
108
|
+
label: '400 GBP+',
|
|
199
109
|
},
|
|
200
|
-
|
|
110
|
+
],
|
|
111
|
+
selected: 3,
|
|
201
112
|
};
|
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
import * as Icons from '@transferwise/icons';
|
|
2
2
|
|
|
3
|
+
import { ControlType, Priority } from '../common';
|
|
4
|
+
|
|
3
5
|
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
4
6
|
import CircularButton from './CircularButton';
|
|
5
7
|
import Title from '../title';
|
|
6
8
|
import Body from '../body';
|
|
7
9
|
|
|
8
|
-
/**
|
|
9
|
-
* A circular icon button with a text label below it. Use when the user can perform
|
|
10
|
-
* multiple equally important actions related to an entity on screen — e.g. "Send",
|
|
11
|
-
* "Add funds", "Convert". Always display in a set; don't use a single circular
|
|
12
|
-
* button on its own.
|
|
13
|
-
*
|
|
14
|
-
* Supports `primary` and `secondary` priorities combined with `default` and
|
|
15
|
-
* `negative` types. The icon shrinks automatically at 400% zoom (56px → 32px).
|
|
16
|
-
*
|
|
17
|
-
* Screen readers announce the text label and ignore the icon, so keep labels
|
|
18
|
-
* contextual and action-oriented (start with a verb, 1–2 words).
|
|
19
|
-
*
|
|
20
|
-
* Keyboard: `Space` or `Enter` activates the button when focused.
|
|
21
|
-
*
|
|
22
|
-
* **Design guide**: [wise.design/components/circular-button](https://wise.design/components/circular-button)
|
|
23
|
-
*/
|
|
24
10
|
export default {
|
|
25
11
|
component: CircularButton,
|
|
26
12
|
title: 'Actions/CircularButton',
|
|
@@ -37,15 +23,11 @@ export default {
|
|
|
37
23
|
),
|
|
38
24
|
},
|
|
39
25
|
},
|
|
40
|
-
parameters: {
|
|
41
|
-
docs: { toc: true },
|
|
42
|
-
},
|
|
43
26
|
} satisfies Meta<typeof CircularButton>;
|
|
44
27
|
|
|
45
28
|
type Story = StoryObj<typeof CircularButton>;
|
|
46
29
|
|
|
47
|
-
|
|
48
|
-
export const Playground: Story = {
|
|
30
|
+
export const Basic: Story = {
|
|
49
31
|
args: {
|
|
50
32
|
priority: 'primary',
|
|
51
33
|
type: 'default',
|
|
@@ -53,8 +35,7 @@ export const Playground: Story = {
|
|
|
53
35
|
},
|
|
54
36
|
};
|
|
55
37
|
|
|
56
|
-
|
|
57
|
-
export const AllTypes: Story = {
|
|
38
|
+
export const All: Story = {
|
|
58
39
|
args: {
|
|
59
40
|
className: 'm-r-2',
|
|
60
41
|
},
|
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
import { Freeze
|
|
1
|
+
import { Freeze } from '@transferwise/icons';
|
|
2
2
|
|
|
3
3
|
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
4
|
-
import { expect, fn, userEvent, within } from 'storybook/test';
|
|
5
4
|
import CircularButton from './CircularButton';
|
|
6
5
|
import Title from '../title';
|
|
7
6
|
import Body from '../body';
|
|
8
7
|
import { withVariantConfig } from '../../.storybook/helpers';
|
|
9
8
|
|
|
10
|
-
import { allModes } from '../../.storybook/modes';
|
|
11
|
-
|
|
12
|
-
const wait = async (ms: number) =>
|
|
13
|
-
new Promise<void>((resolve) => {
|
|
14
|
-
setTimeout(resolve, ms);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
9
|
export default {
|
|
18
10
|
component: CircularButton,
|
|
19
11
|
title: 'Actions/CircularButton/Tests',
|
|
@@ -22,116 +14,6 @@ export default {
|
|
|
22
14
|
|
|
23
15
|
type Story = StoryObj<typeof CircularButton>;
|
|
24
16
|
|
|
25
|
-
/** All priorities, types, and states across all themes. */
|
|
26
|
-
export const Variants: Story = {
|
|
27
|
-
render: () => (
|
|
28
|
-
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '24px', padding: '16px' }}>
|
|
29
|
-
<CircularButton priority="primary" type="default" icon={<Send />}>
|
|
30
|
-
Send
|
|
31
|
-
</CircularButton>
|
|
32
|
-
<CircularButton priority="secondary" type="default" icon={<Plus />}>
|
|
33
|
-
Add funds
|
|
34
|
-
</CircularButton>
|
|
35
|
-
<CircularButton priority="primary" type="negative" icon={<Convert />}>
|
|
36
|
-
Convert
|
|
37
|
-
</CircularButton>
|
|
38
|
-
<CircularButton priority="primary" type="default" icon={<Card />} disabled>
|
|
39
|
-
Disabled
|
|
40
|
-
</CircularButton>
|
|
41
|
-
</div>
|
|
42
|
-
),
|
|
43
|
-
parameters: {
|
|
44
|
-
variants: ['default', 'dark', 'bright-green', 'forest-green'],
|
|
45
|
-
chromatic: {
|
|
46
|
-
dark: allModes.dark,
|
|
47
|
-
brightGreen: allModes.brightGreen,
|
|
48
|
-
forestGreen: allModes.forestGreen,
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/** CircularButton at 400% zoom — icon shrinks from 56px to 32px. */
|
|
54
|
-
export const Zoom400: Story = {
|
|
55
|
-
render: () => (
|
|
56
|
-
<div style={{ display: 'flex', gap: '24px', padding: '16px' }}>
|
|
57
|
-
<CircularButton priority="primary" type="default" icon={<Send />}>
|
|
58
|
-
Send
|
|
59
|
-
</CircularButton>
|
|
60
|
-
<CircularButton priority="secondary" type="default" icon={<Plus />}>
|
|
61
|
-
Add funds
|
|
62
|
-
</CircularButton>
|
|
63
|
-
</div>
|
|
64
|
-
),
|
|
65
|
-
...withVariantConfig(['400%']),
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
/** Tab through all variants and activate each with Space. */
|
|
69
|
-
export const KeyboardInteraction: Story = {
|
|
70
|
-
render: () => {
|
|
71
|
-
const onClick = fn();
|
|
72
|
-
return (
|
|
73
|
-
<div style={{ display: 'flex', gap: '24px', padding: '16px' }}>
|
|
74
|
-
<CircularButton priority="primary" type="default" icon={<Send />} onClick={onClick}>
|
|
75
|
-
Send
|
|
76
|
-
</CircularButton>
|
|
77
|
-
<CircularButton priority="secondary" type="default" icon={<Plus />} onClick={onClick}>
|
|
78
|
-
Add funds
|
|
79
|
-
</CircularButton>
|
|
80
|
-
<CircularButton priority="primary" type="negative" icon={<Convert />} onClick={onClick}>
|
|
81
|
-
Convert
|
|
82
|
-
</CircularButton>
|
|
83
|
-
<CircularButton
|
|
84
|
-
priority="primary"
|
|
85
|
-
type="default"
|
|
86
|
-
icon={<Card />}
|
|
87
|
-
disabled
|
|
88
|
-
onClick={onClick}
|
|
89
|
-
>
|
|
90
|
-
Disabled
|
|
91
|
-
</CircularButton>
|
|
92
|
-
</div>
|
|
93
|
-
);
|
|
94
|
-
},
|
|
95
|
-
play: async ({ canvasElement, step }) => {
|
|
96
|
-
const canvas = within(canvasElement);
|
|
97
|
-
const buttons = canvas.getAllByRole('button');
|
|
98
|
-
|
|
99
|
-
await step('tab to Send and press Space', async () => {
|
|
100
|
-
await userEvent.tab();
|
|
101
|
-
await expect(buttons[0]).toHaveFocus();
|
|
102
|
-
await wait(400);
|
|
103
|
-
await userEvent.keyboard(' ');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
await wait(400);
|
|
107
|
-
|
|
108
|
-
await step('tab to Add funds and press Space', async () => {
|
|
109
|
-
await userEvent.tab();
|
|
110
|
-
await expect(buttons[1]).toHaveFocus();
|
|
111
|
-
await wait(400);
|
|
112
|
-
await userEvent.keyboard(' ');
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
await wait(400);
|
|
116
|
-
|
|
117
|
-
await step('tab to Convert and press Space', async () => {
|
|
118
|
-
await userEvent.tab();
|
|
119
|
-
await expect(buttons[2]).toHaveFocus();
|
|
120
|
-
await wait(400);
|
|
121
|
-
await userEvent.keyboard(' ');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
await wait(400);
|
|
125
|
-
|
|
126
|
-
await step('tab skips Disabled button', async () => {
|
|
127
|
-
await userEvent.tab();
|
|
128
|
-
// Disabled button should be skipped, focus leaves the component
|
|
129
|
-
await expect(buttons[3]).not.toHaveFocus();
|
|
130
|
-
});
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
/** Long labels are centered beneath the icon. */
|
|
135
17
|
export const CenteredText: Story = {
|
|
136
18
|
render: () => {
|
|
137
19
|
return (
|