@transferwise/components 46.17.3 → 46.19.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.
Files changed (58) hide show
  1. package/build/index.esm.js +24 -29
  2. package/build/index.esm.js.map +1 -1
  3. package/build/index.js +24 -29
  4. package/build/index.js.map +1 -1
  5. package/build/main.css +4 -0
  6. package/build/styles/instructionsList/InstructionsList.css +4 -0
  7. package/build/styles/main.css +4 -0
  8. package/build/types/accordion/Accordion.d.ts +3 -7
  9. package/build/types/accordion/Accordion.d.ts.map +1 -1
  10. package/build/types/accordion/index.d.ts +1 -0
  11. package/build/types/accordion/index.d.ts.map +1 -1
  12. package/build/types/actionButton/ActionButton.d.ts +1 -1
  13. package/build/types/body/Body.d.ts +1 -1
  14. package/build/types/circularButton/CircularButton.d.ts +16 -20
  15. package/build/types/circularButton/CircularButton.d.ts.map +1 -1
  16. package/build/types/circularButton/index.d.ts +2 -1
  17. package/build/types/circularButton/index.d.ts.map +1 -1
  18. package/build/types/dateLookup/DateLookup.d.ts +1 -0
  19. package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
  20. package/build/types/index.d.ts +3 -1
  21. package/build/types/index.d.ts.map +1 -1
  22. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +2 -1
  23. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  24. package/build/types/radioGroup/RadioGroup.d.ts +2 -1
  25. package/build/types/radioGroup/RadioGroup.d.ts.map +1 -1
  26. package/build/types/radioGroup/index.d.ts +1 -1
  27. package/build/types/radioGroup/index.d.ts.map +1 -1
  28. package/build/types/summary/Summary.d.ts.map +1 -1
  29. package/build/types/typeahead/Typeahead.d.ts +3 -1
  30. package/build/types/typeahead/Typeahead.d.ts.map +1 -1
  31. package/build/types/typeahead/typeaheadInput/TypeaheadInput.d.ts +1 -1
  32. package/package.json +1 -1
  33. package/src/accordion/Accordion.tsx +6 -7
  34. package/src/accordion/index.ts +1 -0
  35. package/src/circularButton/{CircularButton.story.js → CircularButton.story.tsx} +2 -2
  36. package/src/circularButton/CircularButton.tsx +51 -0
  37. package/src/circularButton/index.ts +2 -0
  38. package/src/dateLookup/DateLookup.js +2 -0
  39. package/src/dateLookup/DateLookup.story.js +3 -0
  40. package/src/dateLookup/DateLookup.view.spec.js +5 -0
  41. package/src/index.ts +3 -1
  42. package/src/instructionsList/InstructionsList.css +4 -0
  43. package/src/instructionsList/InstructionsList.less +5 -0
  44. package/src/instructionsList/InstructionsList.tsx +3 -3
  45. package/src/main.css +4 -0
  46. package/src/phoneNumberInput/PhoneNumberInput.rtl.spec.tsx +22 -0
  47. package/src/phoneNumberInput/PhoneNumberInput.tsx +3 -1
  48. package/src/radioGroup/RadioGroup.tsx +6 -1
  49. package/src/radioGroup/index.ts +1 -1
  50. package/src/summary/Summary.tsx +7 -1
  51. package/src/typeahead/Typeahead.spec.js +9 -0
  52. package/src/typeahead/Typeahead.story.tsx +109 -0
  53. package/src/typeahead/Typeahead.tsx +13 -4
  54. package/src/typeahead/typeaheadInput/TypeaheadInput.tsx +3 -3
  55. package/src/circularButton/CircularButton.js +0 -57
  56. package/src/circularButton/index.js +0 -1
  57. /package/src/circularButton/{CircularButton.spec.js → CircularButton.spec.tsx} +0 -0
  58. /package/src/circularButton/__snapshots__/{CircularButton.spec.js.snap → CircularButton.spec.tsx.snap} +0 -0
@@ -3,9 +3,14 @@ import { useState } from 'react';
3
3
  import Radio from '../radio';
4
4
  import { RadioProps } from '../radio/Radio';
5
5
 
6
+ export type RadioGroupRadio<T extends string | number = string> = Omit<
7
+ RadioProps<T>,
8
+ 'name' | 'checked' | 'onChange' | 'className'
9
+ >;
10
+
6
11
  export interface RadioGroupProps<T extends string | number = string> {
7
12
  name: string;
8
- radios: readonly Omit<RadioProps<T>, 'name' | 'checked' | 'onChange' | 'className'>[];
13
+ radios: readonly RadioGroupRadio<T>[];
9
14
  selectedValue?: T; // TODO: `NoInfer<T>` from TypeScript 5.4
10
15
  onChange: NonNullable<RadioProps<T>['onChange']>;
11
16
  }
@@ -1,2 +1,2 @@
1
1
  export { default } from './RadioGroup';
2
- export type { RadioGroupProps } from './RadioGroup';
2
+ export type { RadioGroupProps, RadioGroupRadio } from './RadioGroup';
@@ -137,7 +137,13 @@ const Summary = ({
137
137
  </div>
138
138
  <div className="np-summary__body m-l-2">
139
139
  <div className="np-summary__title d-flex">
140
- <Body as="span" type={Typography.BODY_LARGE_BOLD} className="text-primary m-b-1">
140
+ <Body
141
+ as="span"
142
+ role="heading"
143
+ aria-level={4}
144
+ type={Typography.BODY_LARGE_BOLD}
145
+ className="text-primary m-b-1"
146
+ >
141
147
  {title}
142
148
  </Body>
143
149
  {info && (
@@ -370,5 +370,14 @@ describe('Typeahead', () => {
370
370
  input().simulate('change', { target: { value: 'test' } });
371
371
  expect(menu().is('.open')).toBe(true);
372
372
  });
373
+
374
+ it('sets aria-expanded to true when options are shown', () => {
375
+ expect(input().prop('aria-expanded')).toBe(false);
376
+ // we don't want aria-expanded to be true on focus or before 3 characters are entered (that's when the menu is shown)
377
+ input().simulate('change', { target: { value: 'aa' } });
378
+ expect(input().prop('aria-expanded')).toBe(false);
379
+ input().simulate('change', { target: { value: 'aaa' } });
380
+ expect(input().prop('aria-expanded')).toBe(true);
381
+ });
373
382
  });
374
383
  });
@@ -5,6 +5,7 @@ import { Search as SearchIcon } from '@transferwise/icons';
5
5
  import { useState } from 'react';
6
6
 
7
7
  import { Sentiment } from '../common';
8
+ import { Input } from '../inputs/Input';
8
9
 
9
10
  import Typeahead, { type TypeaheadOption } from './Typeahead';
10
11
 
@@ -120,3 +121,111 @@ Basic.play = async ({ canvasElement }: StoryContext) => {
120
121
  const canvas = within(canvasElement);
121
122
  await userEvent.type(canvas.getByRole('combobox'), 'abc{ArrowDown}');
122
123
  };
124
+
125
+ type Result =
126
+ | {
127
+ type: 'action';
128
+ value: string;
129
+ }
130
+ | {
131
+ type: 'search';
132
+ value: string;
133
+ };
134
+
135
+ type SearchState = 'success' | 'idle' | 'error' | 'loading';
136
+
137
+ export const Search = () => {
138
+ const [results, setResults] = useState<Result[]>([]);
139
+ const [state, setState] = useState<SearchState>('idle');
140
+ const [filledValue, setFilledValue] = useState<string | null>(null);
141
+
142
+ const onChange = (query: string) => {
143
+ if (query === 'loading') {
144
+ setState('loading');
145
+ setResults([]);
146
+ return;
147
+ }
148
+ if (query === 'error') {
149
+ setState('error');
150
+ setResults([]);
151
+ return;
152
+ }
153
+ if (query === 'nothing') {
154
+ setState('success');
155
+ setResults([]);
156
+ return;
157
+ }
158
+
159
+ setState('success');
160
+ setResults(getResults(query));
161
+ };
162
+
163
+ const onResultSelected = (option: Result) => {
164
+ if (option.type === 'search') {
165
+ setResults([
166
+ { type: 'action', value: `${option.value} Result #1` },
167
+ { type: 'action', value: `${option.value} Result #2` },
168
+ { type: 'action', value: `${option.value} Result #3` },
169
+ ]);
170
+ }
171
+ if (option.type === 'action') {
172
+ setFilledValue(option.value);
173
+ }
174
+ };
175
+
176
+ const getResults = (query: string): Result[] => {
177
+ return [
178
+ { type: 'action', value: `${query} Result #1` },
179
+ { type: 'action', value: `${query} Result #2` },
180
+ { type: 'action', value: `${query} Result #3` },
181
+ { type: 'search', value: `Search for more: '${query}'` },
182
+ ];
183
+ };
184
+
185
+ return (
186
+ <>
187
+ <Typeahead<Result>
188
+ id="typeahead-input-id"
189
+ name="typeahead-input-name"
190
+ size="md"
191
+ maxHeight={100}
192
+ footer={<SearchFooter options={results} state={state} />}
193
+ multiple={false}
194
+ clearable={false}
195
+ addon={<SearchIcon />}
196
+ options={results.map((option) => ({
197
+ value: option,
198
+ label: option.value,
199
+ keepFocusOnSelect: option.type === 'search',
200
+ clearQueryOnSelect: option.type === 'action',
201
+ }))}
202
+ onChange={(values) => {
203
+ if (values.length > 0) {
204
+ const [updatedValue] = values;
205
+ if (updatedValue.value) {
206
+ onResultSelected(updatedValue.value);
207
+ }
208
+ }
209
+ }}
210
+ onInputChange={onChange}
211
+ />
212
+ {filledValue != null ? <Input value={filledValue} /> : null}
213
+ </>
214
+ );
215
+ };
216
+
217
+ function SearchFooter({ options, state }: { options: Result[]; state: SearchState }) {
218
+ if (state === 'loading') {
219
+ return <p className="m-y-2 m-x-2">Loading...</p>;
220
+ }
221
+
222
+ if (state === 'success' && options.length === 0) {
223
+ return <p className="m-y-2 m-x-2">No results found</p>;
224
+ }
225
+
226
+ if (state === 'error' && options.length === 0) {
227
+ return <div className="m-y-2 m-x-2">Something went wrong</div>;
228
+ }
229
+
230
+ return null;
231
+ }
@@ -30,6 +30,8 @@ export type TypeaheadOption<T = string> = {
30
30
  note?: string;
31
31
  secondary?: string;
32
32
  value?: T;
33
+ clearQueryOnSelect?: boolean;
34
+ keepFocusOnSelect?: boolean;
33
35
  };
34
36
 
35
37
  export interface TypeaheadProps<T> {
@@ -250,7 +252,15 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
250
252
  }
251
253
 
252
254
  this.updateSelectedValue(selected);
253
- this.hideMenu();
255
+
256
+ if (!item.keepFocusOnSelect) {
257
+ this.hideMenu();
258
+ }
259
+
260
+ if (item.clearQueryOnSelect) {
261
+ query = '';
262
+ }
263
+
254
264
  this.setState({
255
265
  query,
256
266
  });
@@ -474,7 +484,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
474
484
  {...{
475
485
  autoFocus,
476
486
  multiple,
477
- optionsShown,
487
+ dropdownOpen,
478
488
  placeholder,
479
489
  selected,
480
490
  maxHeight,
@@ -499,8 +509,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
499
509
  </div>
500
510
  )}
501
511
  </div>
502
- {displayAlert && <InlineAlert type={alert.type}>{alert.message}</InlineAlert>}
503
- {menu}
512
+ {displayAlert ? <InlineAlert type={alert.type}>{alert.message}</InlineAlert> : menu}
504
513
  </div>
505
514
  </div>
506
515
  );
@@ -13,7 +13,7 @@ export type TypeaheadInputProps<T> = {
13
13
  typeaheadId: string;
14
14
  value: string;
15
15
  selected: readonly TypeaheadOption<T>[];
16
- optionsShown?: boolean;
16
+ dropdownOpen?: boolean;
17
17
  autoComplete: string;
18
18
  onChange: React.ChangeEventHandler<HTMLInputElement>;
19
19
  onKeyDown: React.KeyboardEventHandler<HTMLInputElement>;
@@ -67,7 +67,7 @@ export default class TypeaheadInput<T> extends Component<
67
67
  autoFocus,
68
68
  multiple,
69
69
  name,
70
- optionsShown,
70
+ dropdownOpen,
71
71
  placeholder,
72
72
  selected,
73
73
  value,
@@ -89,7 +89,7 @@ export default class TypeaheadInput<T> extends Component<
89
89
  autoFocus={autoFocus}
90
90
  placeholder={hasPlaceholder ? placeholder : ''}
91
91
  aria-autocomplete="list"
92
- aria-expanded={optionsShown}
92
+ aria-expanded={dropdownOpen}
93
93
  aria-haspopup="listbox"
94
94
  aria-controls={`menu-${typeaheadId}`}
95
95
  autoComplete={autoComplete}
@@ -1,57 +0,0 @@
1
- import classNames from 'classnames';
2
- import PropTypes from 'prop-types';
3
- import { cloneElement } from 'react';
4
-
5
- import Body from '../body/Body';
6
- import { typeClassMap, priorityClassMap } from '../button/classMap';
7
- import { ControlType, Priority } from '../common';
8
- import { Typography } from '../common';
9
-
10
- const CircularButton = ({ className, children, disabled, icon, priority, type, ...rest }) => {
11
- const classes = classNames('btn np-btn', typeClassMap[type], priorityClassMap[priority]);
12
-
13
- const iconElement = icon.props.size !== 24 ? cloneElement(icon, { size: 24 }) : icon;
14
-
15
- return (
16
- <label
17
- className={classNames(
18
- 'np-circular-btn',
19
- priority,
20
- type,
21
- disabled ? 'disabled' : '',
22
- className,
23
- )}
24
- >
25
- <input
26
- type="button"
27
- aria-label={children}
28
- className={classes}
29
- disabled={disabled}
30
- {...rest}
31
- />
32
- {iconElement}
33
- <Body as="span" className="np-circular-btn__label" type={Typography.BODY_DEFAULT_BOLD}>
34
- {children}
35
- </Body>
36
- </label>
37
- );
38
- };
39
-
40
- CircularButton.propTypes = {
41
- className: PropTypes.string,
42
- children: PropTypes.string.isRequired,
43
- disabled: PropTypes.bool,
44
- icon: PropTypes.element.isRequired,
45
- onClick: PropTypes.func,
46
- priority: PropTypes.oneOf(['primary', 'secondary']),
47
- type: PropTypes.oneOf(['accent', 'positive', 'negative']),
48
- };
49
-
50
- CircularButton.defaultProps = {
51
- className: undefined,
52
- disabled: false,
53
- priority: Priority.PRIMARY,
54
- type: ControlType.ACCENT,
55
- };
56
-
57
- export default CircularButton;
@@ -1 +0,0 @@
1
- export { default } from './CircularButton';