@transferwise/components 0.0.0-experimental-77b2752 → 0.0.0-experimental-c9a3ec4

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.
@@ -4,10 +4,10 @@
4
4
 
5
5
  import { Cross as CrossIcon } from '@transferwise/icons';
6
6
  import classNames from 'classnames';
7
+ import { DebouncedFunc } from 'lodash';
7
8
  import clamp from 'lodash.clamp';
8
9
  import debounce from 'lodash.debounce';
9
- import PropTypes from 'prop-types';
10
- import { Component } from 'react';
10
+ import { Component, ReactNode } from 'react';
11
11
 
12
12
  import Chip from '../chips/Chip';
13
13
  import { Size, Sentiment } from '../common';
@@ -23,10 +23,59 @@ import TypeaheadOption from './typeaheadOption/TypeaheadOption';
23
23
  const DEFAULT_MIN_QUERY_LENGTH = 3;
24
24
  const SEARCH_DELAY = 200;
25
25
 
26
- export default class Typeahead extends Component {
27
- constructor(props) {
26
+ export type TypeaheadOption<T = string> = {
27
+ label: string;
28
+ note?: string;
29
+ secondary?: string;
30
+ value?: T;
31
+ };
32
+
33
+ export type TypeaheadProps<T> = {
34
+ id: string;
35
+ name: string;
36
+ options: TypeaheadOption<T>[];
37
+ initialValue?: TypeaheadOption<T>[];
38
+ onChange: (options: TypeaheadOption<T>[]) => void;
39
+ allowNew?: boolean;
40
+ autoFocus?: boolean;
41
+ clearable?: boolean;
42
+ multiple?: boolean;
43
+ showSuggestions?: boolean;
44
+ showNewEntry?: boolean;
45
+ searchDelay?: number;
46
+ maxHeight?: number;
47
+ minQueryLength?: number;
48
+ addon?: ReactNode;
49
+ placeholder?: string;
50
+ alert?: {
51
+ message: string;
52
+ type: 'error' | 'warning' | 'neutral';
53
+ };
54
+ footer: ReactNode;
55
+ validateChip?: (chip: TypeaheadOption<T>) => boolean;
56
+ onSearch?: (query: string) => void;
57
+ onBlur?: () => void;
58
+ onInputChange?: (query: string) => void;
59
+ onFocus?: () => void;
60
+ chipSeparators?: string[];
61
+ size?: 'md' | 'lg';
62
+ inputAutoComplete?: string;
63
+ autoFillOnBlur?: boolean;
64
+ };
65
+
66
+ type TypeaheadState<T> = {
67
+ selected: TypeaheadOption<T>[];
68
+ keyboardFocusedOptionIndex: number | null;
69
+ errorState: boolean;
70
+ query: string;
71
+ optionsShown?: boolean;
72
+ isFocused?: boolean;
73
+ };
74
+
75
+ export default class Typeahead<T> extends Component<TypeaheadProps<T>, TypeaheadState<T>> {
76
+ constructor(props: TypeaheadProps<T>) {
28
77
  super(props);
29
- const { searchDelay, initialValue, multiple } = props;
78
+ const { searchDelay = SEARCH_DELAY, initialValue = [], multiple } = props;
30
79
  this.handleSearchDebounced = debounce(this.handleSearch, searchDelay);
31
80
  const initialQuery = !multiple && initialValue.length > 0 ? initialValue[0].label : '';
32
81
  this.state = {
@@ -37,7 +86,9 @@ export default class Typeahead extends Component {
37
86
  };
38
87
  }
39
88
 
40
- UNSAFE_componentWillReceiveProps(nextProps) {
89
+ handleSearchDebounced: DebouncedFunc<(query: string) => void>;
90
+
91
+ UNSAFE_componentWillReceiveProps(nextProps: TypeaheadProps<T>) {
41
92
  if (nextProps.multiple !== this.props.multiple) {
42
93
  this.setState((previousState) => {
43
94
  const { selected } = previousState;
@@ -49,6 +100,7 @@ export default class Typeahead extends Component {
49
100
  }
50
101
  return {
51
102
  query: '',
103
+ selected: previousState.selected,
52
104
  };
53
105
  });
54
106
  }
@@ -59,20 +111,17 @@ export default class Typeahead extends Component {
59
111
  }
60
112
 
61
113
  handleOnFocus = () => {
62
- const { onFocus } = this.props;
63
114
  this.showMenu();
64
-
65
- if (onFocus) {
66
- this.props.onFocus();
67
- }
115
+ this.props.onFocus?.();
68
116
  };
69
117
 
70
- onOptionSelected = (event, item) => {
118
+ onOptionSelected = (event: React.MouseEvent, item: TypeaheadOption<T>) => {
71
119
  event.preventDefault();
72
120
  this.selectItem(item);
73
121
  };
74
122
 
75
- handleOnChange = (event) => {
123
+ // START HERE
124
+ handleOnChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
76
125
  const { optionsShown, selected } = this.state;
77
126
  const { multiple, onInputChange } = this.props;
78
127
 
@@ -94,8 +143,8 @@ export default class Typeahead extends Component {
94
143
  });
95
144
  };
96
145
 
97
- handleOnPaste = (event) => {
98
- const { allowNew, multiple, chipSeparators } = this.props;
146
+ handleOnPaste: React.ClipboardEventHandler<HTMLInputElement> = (event) => {
147
+ const { allowNew = false, multiple = false, chipSeparators = [] } = this.props;
99
148
  const { selected } = this.state;
100
149
 
101
150
  if (allowNew && multiple && chipSeparators.length > 0) {
@@ -113,8 +162,14 @@ export default class Typeahead extends Component {
113
162
  }
114
163
  };
115
164
 
116
- handleOnKeyDown = (event) => {
117
- const { showSuggestions, allowNew, multiple, chipSeparators, options } = this.props;
165
+ handleOnKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
166
+ const {
167
+ showSuggestions = true,
168
+ allowNew = false,
169
+ multiple = false,
170
+ chipSeparators = [],
171
+ options,
172
+ } = this.props;
118
173
  const { keyboardFocusedOptionIndex, query, selected } = this.state;
119
174
  const chipsMode = !showSuggestions && allowNew && multiple;
120
175
 
@@ -133,7 +188,7 @@ export default class Typeahead extends Component {
133
188
  break;
134
189
  case 'Enter':
135
190
  event.preventDefault();
136
- if (options[keyboardFocusedOptionIndex]) {
191
+ if (keyboardFocusedOptionIndex && options[keyboardFocusedOptionIndex]) {
137
192
  this.selectItem(options[keyboardFocusedOptionIndex]);
138
193
  } else if (allowNew && query.trim()) {
139
194
  this.selectItem({ label: query });
@@ -150,7 +205,7 @@ export default class Typeahead extends Component {
150
205
  }
151
206
  };
152
207
 
153
- moveFocusedOption(offset) {
208
+ moveFocusedOption(offset: number) {
154
209
  this.setState((previousState) => {
155
210
  const { keyboardFocusedOptionIndex } = previousState;
156
211
  const { options } = this.props;
@@ -164,7 +219,7 @@ export default class Typeahead extends Component {
164
219
  });
165
220
  }
166
221
 
167
- selectItem = (item) => {
222
+ selectItem = (item: TypeaheadOption<T>) => {
168
223
  const { multiple } = this.props;
169
224
  let selected = [...this.state.selected];
170
225
  let query;
@@ -183,7 +238,7 @@ export default class Typeahead extends Component {
183
238
  });
184
239
  };
185
240
 
186
- stopPropagation = (event) => {
241
+ stopPropagation = (event: React.MouseEvent) => {
187
242
  event.stopPropagation();
188
243
  event.preventDefault();
189
244
  if (event.nativeEvent && event.nativeEvent.stopImmediatePropagation) {
@@ -191,7 +246,7 @@ export default class Typeahead extends Component {
191
246
  }
192
247
  };
193
248
 
194
- handleSearch = (query) => {
249
+ handleSearch = (query: string) => {
195
250
  const { onSearch } = this.props;
196
251
  if (onSearch) {
197
252
  onSearch(query);
@@ -246,8 +301,8 @@ export default class Typeahead extends Component {
246
301
  );
247
302
  };
248
303
 
249
- updateSelectedValue = (selected) => {
250
- const { onChange, validateChip } = this.props;
304
+ updateSelectedValue = (selected: TypeaheadOption<T>[]) => {
305
+ const { onChange, validateChip = () => true } = this.props;
251
306
 
252
307
  const errorState = selected.some((chip) => !validateChip(chip));
253
308
  this.setState({ selected, errorState }, () => {
@@ -255,7 +310,7 @@ export default class Typeahead extends Component {
255
310
  });
256
311
  };
257
312
 
258
- clear = (event) => {
313
+ clear = (event: React.MouseEvent<HTMLButtonElement>) => {
259
314
  event.preventDefault();
260
315
  if (this.state.selected.length > 0) {
261
316
  this.updateSelectedValue([]);
@@ -266,7 +321,7 @@ export default class Typeahead extends Component {
266
321
  });
267
322
  };
268
323
 
269
- removeChip = (option) => {
324
+ removeChip = (option: TypeaheadOption<T>) => {
270
325
  const { selected } = this.state;
271
326
 
272
327
  if (selected.length > 0) {
@@ -274,8 +329,8 @@ export default class Typeahead extends Component {
274
329
  }
275
330
  };
276
331
 
277
- renderChip = (option, idx) => {
278
- const valid = this.props.validateChip(option);
332
+ renderChip = (option: TypeaheadOption<T>, idx: number): ReactNode => {
333
+ const valid = this.props.validateChip?.(option);
279
334
 
280
335
  return (
281
336
  <Chip
@@ -299,7 +354,10 @@ export default class Typeahead extends Component {
299
354
  allowNew,
300
355
  showNewEntry,
301
356
  dropdownOpen,
302
- }) => {
357
+ }: Pick<TypeaheadProps<T>, 'footer' | 'options' | 'id' | 'allowNew' | 'showNewEntry'> &
358
+ Pick<TypeaheadState<T>, 'keyboardFocusedOptionIndex' | 'query'> & {
359
+ dropdownOpen?: boolean;
360
+ }) => {
303
361
  const optionsToRender = [...options];
304
362
  if (
305
363
  allowNew &&
@@ -338,21 +396,21 @@ export default class Typeahead extends Component {
338
396
  const {
339
397
  id,
340
398
  placeholder,
341
- multiple,
342
- size,
343
- addon,
399
+ multiple = false,
400
+ size = Size.MEDIUM,
401
+ addon = null,
344
402
  name,
345
- clearable,
346
- allowNew,
347
- footer,
348
- showSuggestions,
349
- showNewEntry,
403
+ clearable = true,
404
+ allowNew = false,
405
+ footer = null,
406
+ showSuggestions = true,
407
+ showNewEntry = true,
350
408
  options,
351
- minQueryLength,
352
- autoFocus,
353
- maxHeight,
354
- alert,
355
- inputAutoComplete,
409
+ minQueryLength = DEFAULT_MIN_QUERY_LENGTH,
410
+ autoFocus = false,
411
+ maxHeight = null,
412
+ alert = null,
413
+ inputAutoComplete = 'new-password',
356
414
  } = this.props;
357
415
 
358
416
  const { errorState, query, selected, optionsShown, keyboardFocusedOptionIndex } = this.state;
@@ -364,6 +422,7 @@ export default class Typeahead extends Component {
364
422
  const menu = this.renderMenu({
365
423
  footer,
366
424
  options,
425
+ id,
367
426
  keyboardFocusedOptionIndex,
368
427
  query,
369
428
  allowNew,
@@ -435,75 +494,3 @@ export default class Typeahead extends Component {
435
494
  );
436
495
  }
437
496
  }
438
-
439
- Typeahead.propTypes = {
440
- id: PropTypes.string.isRequired,
441
- name: PropTypes.string.isRequired,
442
- options: PropTypes.arrayOf(
443
- PropTypes.shape({
444
- label: PropTypes.string.isRequired,
445
- note: PropTypes.string,
446
- secondary: PropTypes.string,
447
- value: PropTypes.object,
448
- }),
449
- ).isRequired,
450
- initialValue: PropTypes.arrayOf(
451
- PropTypes.shape({
452
- label: PropTypes.string.isRequired,
453
- note: PropTypes.string,
454
- secondary: PropTypes.string,
455
- }),
456
- ),
457
- onChange: PropTypes.func.isRequired,
458
- allowNew: PropTypes.bool,
459
- autoFocus: PropTypes.bool,
460
- clearable: PropTypes.bool,
461
- multiple: PropTypes.bool,
462
- showSuggestions: PropTypes.bool,
463
- showNewEntry: PropTypes.bool,
464
- searchDelay: PropTypes.number,
465
- maxHeight: PropTypes.number,
466
- minQueryLength: PropTypes.number,
467
- addon: PropTypes.node,
468
- placeholder: PropTypes.string,
469
- alert: PropTypes.shape({
470
- message: PropTypes.string.isRequired,
471
- type: PropTypes.oneOf(['error', 'warning', 'neutral']).isRequired,
472
- }),
473
- footer: PropTypes.node,
474
- validateChip: PropTypes.func,
475
- onSearch: PropTypes.func,
476
- onBlur: PropTypes.func,
477
- onInputChange: PropTypes.func,
478
- onFocus: PropTypes.func,
479
- chipSeparators: PropTypes.arrayOf(PropTypes.string),
480
- size: PropTypes.oneOf(['md', 'lg']),
481
- inputAutoComplete: PropTypes.string,
482
- autoFillOnBlur: PropTypes.bool,
483
- };
484
-
485
- Typeahead.defaultProps = {
486
- allowNew: false,
487
- autoFocus: false,
488
- clearable: true,
489
- multiple: false,
490
- maxHeight: null,
491
- showSuggestions: true,
492
- showNewEntry: true,
493
- searchDelay: SEARCH_DELAY,
494
- minQueryLength: DEFAULT_MIN_QUERY_LENGTH,
495
- addon: null,
496
- placeholder: null,
497
- alert: null,
498
- footer: null,
499
- size: Size.MEDIUM,
500
- chipSeparators: [],
501
- initialValue: [],
502
- onSearch: null,
503
- onBlur: null,
504
- onInputChange: null,
505
- onFocus: null,
506
- validateChip: () => true,
507
- inputAutoComplete: 'new-password',
508
- autoFillOnBlur: true,
509
- };
@@ -2,16 +2,44 @@
2
2
  /* eslint-disable jsx-a11y/click-events-have-key-events */
3
3
  /* eslint-disable jsx-a11y/no-static-element-interactions */
4
4
  import classnames from 'classnames';
5
- import PropTypes from 'prop-types';
6
- import { Component } from 'react';
5
+ import { Component, ReactNode } from 'react';
7
6
 
8
7
  import { Input } from '../../inputs/Input';
8
+ import { TypeaheadOption } from '../Typeahead';
9
9
 
10
10
  const DEFAULT_INPUT_MIN_WIDTH = 10;
11
11
 
12
- export default class TypeaheadInput extends Component {
13
- constructor() {
14
- super();
12
+ export type TypeaheadInputProps<T> = {
13
+ typeaheadId: string;
14
+ name: string;
15
+ autoFocus?: boolean;
16
+ multiple: boolean;
17
+ value: string;
18
+ selected: TypeaheadOption<T>[];
19
+ placeholder?: string;
20
+ optionsShown?: boolean;
21
+ maxHeight: number | null;
22
+ autoComplete: string;
23
+ onChange: React.ChangeEventHandler<HTMLInputElement>;
24
+ onKeyDown: React.KeyboardEventHandler<HTMLInputElement>;
25
+ onFocus: () => void;
26
+ onPaste: React.ClipboardEventHandler<HTMLInputElement>;
27
+ renderChip: (chip: TypeaheadOption<T>, index: number) => ReactNode;
28
+ };
29
+
30
+ type TypeaheadInputState = {
31
+ inputWidth: number;
32
+ };
33
+
34
+ export default class TypeaheadInput<T> extends Component<
35
+ TypeaheadInputProps<T>,
36
+ TypeaheadInputState
37
+ > {
38
+ inputRef: HTMLInputElement | null = null;
39
+ sizerRef: HTMLDivElement | null = null;
40
+
41
+ constructor(props: TypeaheadInputProps<T>) {
42
+ super(props);
15
43
  this.state = {
16
44
  inputWidth: DEFAULT_INPUT_MIN_WIDTH,
17
45
  };
@@ -20,11 +48,11 @@ export default class TypeaheadInput extends Component {
20
48
  componentDidMount() {
21
49
  const { autoFocus } = this.props;
22
50
  if (autoFocus) {
23
- this.inputRef.focus();
51
+ this.inputRef?.focus();
24
52
  }
25
53
  }
26
54
 
27
- componentDidUpdate(previousProps) {
55
+ componentDidUpdate(previousProps: TypeaheadInputProps<T>) {
28
56
  if (previousProps.value !== this.props.value && this.props.multiple) {
29
57
  this.recalculateWidth();
30
58
  }
@@ -33,7 +61,9 @@ export default class TypeaheadInput extends Component {
33
61
  recalculateWidth = () => {
34
62
  requestAnimationFrame(() => {
35
63
  this.setState({
36
- inputWidth: Math.max(DEFAULT_INPUT_MIN_WIDTH, this.sizerRef.scrollWidth + 10),
64
+ inputWidth: this.sizerRef
65
+ ? Math.max(DEFAULT_INPUT_MIN_WIDTH, this.sizerRef.scrollWidth + 10)
66
+ : DEFAULT_INPUT_MIN_WIDTH,
37
67
  });
38
68
  });
39
69
  };
@@ -41,12 +71,12 @@ export default class TypeaheadInput extends Component {
41
71
  renderInput = () => {
42
72
  const {
43
73
  typeaheadId,
44
- autoFocus,
74
+ autoFocus = false,
45
75
  multiple,
46
76
  name,
47
- optionsShown,
48
- placeholder,
49
- selected,
77
+ optionsShown = false,
78
+ placeholder = '',
79
+ selected = [],
50
80
  value,
51
81
  onChange,
52
82
  onKeyDown,
@@ -85,14 +115,14 @@ export default class TypeaheadInput extends Component {
85
115
  };
86
116
 
87
117
  render() {
88
- const { multiple, selected, value, maxHeight, renderChip } = this.props;
118
+ const { multiple, selected = [], value, maxHeight = null, renderChip } = this.props;
89
119
 
90
120
  return multiple ? (
91
121
  <div
92
122
  className="form-control typeahead__input-container"
93
- style={maxHeight && { maxHeight }}
123
+ style={{ maxHeight: maxHeight ?? undefined }}
94
124
  onClick={() => {
95
- this.inputRef.focus();
125
+ this.inputRef?.focus();
96
126
  }}
97
127
  >
98
128
  <div className="typeahead__input-wrapper">
@@ -115,30 +145,3 @@ export default class TypeaheadInput extends Component {
115
145
  );
116
146
  }
117
147
  }
118
-
119
- TypeaheadInput.propTypes = {
120
- typeaheadId: PropTypes.string.isRequired,
121
- name: PropTypes.string.isRequired,
122
- autoFocus: PropTypes.bool,
123
- multiple: PropTypes.bool.isRequired,
124
- value: PropTypes.string.isRequired,
125
- selected: PropTypes.arrayOf(PropTypes.object),
126
- placeholder: PropTypes.string,
127
- optionsShown: PropTypes.bool,
128
- maxHeight: PropTypes.number,
129
- autoComplete: PropTypes.string.isRequired,
130
-
131
- onChange: PropTypes.func.isRequired,
132
- renderChip: PropTypes.func.isRequired,
133
- onKeyDown: PropTypes.func.isRequired,
134
- onFocus: PropTypes.func.isRequired,
135
- onPaste: PropTypes.func.isRequired,
136
- };
137
-
138
- TypeaheadInput.defaultProps = {
139
- autoFocus: false,
140
- maxHeight: null,
141
- placeholder: '',
142
- optionsShown: false,
143
- selected: [],
144
- };
@@ -1,11 +1,18 @@
1
1
  /* eslint-disable jsx-a11y/anchor-is-valid */
2
2
  import classNames from 'classnames';
3
- import PropTypes from 'prop-types';
4
3
 
4
+ import { TypeaheadOption } from '../Typeahead';
5
5
  import highlight from '../util/highlight';
6
6
 
7
- const Option = (props) => {
8
- const { option, selected, onClick, query } = props;
7
+ export type TypeaheadOptionProps<T> = {
8
+ option: TypeaheadOption<T>;
9
+ selected?: boolean;
10
+ onClick?: React.MouseEventHandler;
11
+ query?: string;
12
+ };
13
+
14
+ const Option = <T,>(props: TypeaheadOptionProps<T>) => {
15
+ const { option, selected = false, onClick = () => {}, query = '' } = props;
9
16
  const { label, note, secondary } = option;
10
17
 
11
18
  return (
@@ -27,21 +34,4 @@ const Option = (props) => {
27
34
  );
28
35
  };
29
36
 
30
- Option.propTypes = {
31
- option: PropTypes.shape({
32
- label: PropTypes.string.isRequired,
33
- note: PropTypes.string,
34
- secondary: PropTypes.string,
35
- }).isRequired,
36
- query: PropTypes.string,
37
- selected: PropTypes.bool,
38
- onClick: PropTypes.func,
39
- };
40
-
41
- Option.defaultProps = {
42
- selected: false,
43
- query: '',
44
- onClick: () => {},
45
- };
46
-
47
37
  export default Option;