@transferwise/components 0.0.0-experimental-4d1e1cf → 0.0.0-experimental-a53ae95

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 (38) hide show
  1. package/README.md +14 -1
  2. package/build/index.esm.js +147 -49
  3. package/build/index.esm.js.map +1 -1
  4. package/build/index.js +147 -49
  5. package/build/index.js.map +1 -1
  6. package/build/mocks.esm.js +40 -0
  7. package/build/mocks.esm.js.map +1 -0
  8. package/build/mocks.js +43 -0
  9. package/build/mocks.js.map +1 -0
  10. package/build/types/index.d.ts +0 -1
  11. package/build/types/index.d.ts.map +1 -1
  12. package/build/types/mocks.d.ts +9 -0
  13. package/build/types/mocks.d.ts.map +1 -0
  14. package/build/types/test-utils/window-mock.d.ts.map +1 -1
  15. package/build/types/typeahead/Typeahead.d.ts +57 -98
  16. package/build/types/typeahead/Typeahead.d.ts.map +1 -1
  17. package/build/types/typeahead/index.d.ts +2 -2
  18. package/build/types/typeahead/index.d.ts.map +1 -1
  19. package/build/types/typeahead/typeaheadInput/TypeaheadInput.d.ts +41 -23
  20. package/build/types/typeahead/typeaheadInput/TypeaheadInput.d.ts.map +1 -1
  21. package/build/types/typeahead/typeaheadOption/TypeaheadOption.d.ts +17 -9
  22. package/build/types/typeahead/typeaheadOption/TypeaheadOption.d.ts.map +1 -1
  23. package/build/types/typeahead/util/highlight.d.ts +1 -2
  24. package/build/types/typeahead/util/highlight.d.ts.map +1 -1
  25. package/package.json +23 -10
  26. package/src/dimmer/Dimmer.spec.js +0 -4
  27. package/src/index.ts +0 -1
  28. package/src/mocks.ts +48 -0
  29. package/src/snackbar/Snackbar.spec.js +0 -4
  30. package/src/test-utils/window-mock.ts +7 -23
  31. package/src/typeahead/{Typeahead.tsx → Typeahead.js} +108 -107
  32. package/src/typeahead/{Typeahead.story.tsx → Typeahead.story.js} +7 -8
  33. package/src/typeahead/index.js +3 -0
  34. package/src/typeahead/typeaheadInput/{TypeaheadInput.tsx → TypeaheadInput.js} +51 -43
  35. package/src/typeahead/typeaheadOption/{TypeaheadOption.tsx → TypeaheadOption.js} +20 -10
  36. package/src/typeahead/util/{highlight.tsx → highlight.js} +1 -1
  37. package/src/withNextPortal/withNextPortal.spec.js +0 -4
  38. package/src/typeahead/index.ts +0 -2
@@ -1,30 +1,14 @@
1
+ import {
2
+ mockMatchMedia as baseMockMatchMedia,
3
+ mockResizeObserver as baseMockResizeObserver,
4
+ } from '../mocks';
5
+
1
6
  export function mockMatchMedia() {
2
- Object.defineProperty(window, 'matchMedia', {
3
- writable: true,
4
- value: jest.fn().mockImplementation((query: string) => {
5
- const matches = /^\(min-width: ([0-9]+)px\)$/.exec(query);
6
- const minWidth = matches != null ? Number(matches[1]) : undefined;
7
- return {
8
- matches: minWidth != null ? window.innerWidth >= minWidth : false,
9
- media: query,
10
- onchange: null,
11
- addListener: jest.fn(), // deprecated
12
- removeListener: jest.fn(), // deprecated
13
- addEventListener: jest.fn(),
14
- removeEventListener: jest.fn(),
15
- dispatchEvent: jest.fn(),
16
- };
17
- }),
18
- });
7
+ baseMockMatchMedia(jest);
19
8
  }
20
9
 
21
10
  export function mockResizeObserver() {
22
11
  // mock ResizeObserver because it's not implemented in jsdoc lib
23
12
  // https://github.com/jsdom/jsdom/issues/3368
24
- // eslint-disable-next-line compat/compat
25
- window.ResizeObserver = class ResizeObserver {
26
- observe = jest.fn();
27
- unobserve = jest.fn();
28
- disconnect = jest.fn();
29
- };
13
+ baseMockResizeObserver(jest);
30
14
  }
@@ -4,20 +4,18 @@
4
4
 
5
5
  import { Cross as CrossIcon } from '@transferwise/icons';
6
6
  import classNames from 'classnames';
7
- import { DebouncedFunc } from 'lodash';
8
7
  import clamp from 'lodash.clamp';
9
8
  import debounce from 'lodash.debounce';
10
- import { Component, ReactNode } from 'react';
9
+ import PropTypes from 'prop-types';
10
+ import { Component } from 'react';
11
11
 
12
12
  import Chip from '../chips/Chip';
13
- import { Size, Sentiment, SizeMedium, SizeLarge } from '../common';
13
+ import { Size, Sentiment } from '../common';
14
14
  import {
15
15
  addClickClassToDocumentOnIos,
16
16
  removeClickClassFromDocumentOnIos,
17
- stopPropagation,
18
17
  } from '../common/domHelpers';
19
18
  import InlineAlert from '../inlineAlert';
20
- import { InlineAlertProps } from '../inlineAlert/InlineAlert';
21
19
 
22
20
  import TypeaheadInput from './typeaheadInput/TypeaheadInput';
23
21
  import TypeaheadOption from './typeaheadOption/TypeaheadOption';
@@ -25,81 +23,8 @@ import TypeaheadOption from './typeaheadOption/TypeaheadOption';
25
23
  const DEFAULT_MIN_QUERY_LENGTH = 3;
26
24
  const SEARCH_DELAY = 200;
27
25
 
28
- export type TypeaheadOption<T = string> = {
29
- label: string;
30
- note?: string;
31
- secondary?: string;
32
- value?: T;
33
- };
34
-
35
- export interface TypeaheadProps<T> {
36
- id: string;
37
- name: string;
38
- addon: ReactNode;
39
- alert?: {
40
- message: InlineAlertProps['children'];
41
- type: InlineAlertProps['type'];
42
- };
43
- allowNew: boolean;
44
- autoFillOnBlur?: boolean;
45
- autoFocus: boolean;
46
- chipSeparators?: string[];
47
- clearable: boolean;
48
- footer: ReactNode;
49
- initialValue: TypeaheadOption<T>[];
50
- inputAutoComplete?: string;
51
- maxHeight: number;
52
- minQueryLength: number;
53
- placeholder?: string;
54
- multiple: boolean;
55
- options: TypeaheadOption<T>[];
56
- searchDelay: number;
57
- showSuggestions: boolean;
58
- showNewEntry: boolean;
59
- size?: SizeMedium | SizeLarge;
60
-
61
- onBlur?: () => void;
62
- onChange: (options: TypeaheadOption<T>[]) => void;
63
- onFocus?: () => void;
64
- onInputChange?: (query: string) => void;
65
- onSearch?: (query: string) => void;
66
- validateChip?: (chip: TypeaheadOption<T>) => boolean;
67
- }
68
-
69
- type TypeaheadState<T> = {
70
- selected: TypeaheadOption<T>[];
71
- keyboardFocusedOptionIndex: number | null;
72
- errorState: boolean;
73
- query: string;
74
- optionsShown?: boolean;
75
- isFocused?: boolean;
76
- };
77
-
78
- export default class Typeahead<T> extends Component<TypeaheadProps<T>, TypeaheadState<T>> {
79
- declare props: TypeaheadProps<T> &
80
- Required<Pick<TypeaheadProps<T>, keyof typeof Typeahead.defaultProps>>;
81
-
82
- static defaultProps = {
83
- addon: null,
84
- allowNew: false,
85
- autoFillOnBlur: true,
86
- autoFocus: false,
87
- chipSeparators: [],
88
- clearable: true,
89
- footer: null,
90
- initialValue: [],
91
- inputAutoComplete: 'new-password',
92
- maxHeight: null,
93
- minQueryLength: DEFAULT_MIN_QUERY_LENGTH,
94
- multiple: false,
95
- searchDelay: SEARCH_DELAY,
96
- showSuggestions: true,
97
- showNewEntry: true,
98
- size: Size.MEDIUM,
99
- validateChip: () => true,
100
- };
101
-
102
- constructor(props: TypeaheadProps<T>) {
26
+ export default class Typeahead extends Component {
27
+ constructor(props) {
103
28
  super(props);
104
29
  const { searchDelay, initialValue, multiple } = props;
105
30
  this.handleSearchDebounced = debounce(this.handleSearch, searchDelay);
@@ -112,9 +37,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
112
37
  };
113
38
  }
114
39
 
115
- handleSearchDebounced: DebouncedFunc<(query: string) => void>;
116
-
117
- UNSAFE_componentWillReceiveProps(nextProps: TypeaheadProps<T>) {
40
+ UNSAFE_componentWillReceiveProps(nextProps) {
118
41
  if (nextProps.multiple !== this.props.multiple) {
119
42
  this.setState((previousState) => {
120
43
  const { selected } = previousState;
@@ -126,7 +49,6 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
126
49
  }
127
50
  return {
128
51
  query: '',
129
- selected: previousState.selected,
130
52
  };
131
53
  });
132
54
  }
@@ -137,16 +59,20 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
137
59
  }
138
60
 
139
61
  handleOnFocus = () => {
62
+ const { onFocus } = this.props;
140
63
  this.showMenu();
141
- this.props.onFocus?.();
64
+
65
+ if (onFocus) {
66
+ this.props.onFocus();
67
+ }
142
68
  };
143
69
 
144
- onOptionSelected = (event: React.MouseEvent, item: TypeaheadOption<T>) => {
70
+ onOptionSelected = (event, item) => {
145
71
  event.preventDefault();
146
72
  this.selectItem(item);
147
73
  };
148
74
 
149
- handleOnChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
75
+ handleOnChange = (event) => {
150
76
  const { optionsShown, selected } = this.state;
151
77
  const { multiple, onInputChange } = this.props;
152
78
 
@@ -168,7 +94,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
168
94
  });
169
95
  };
170
96
 
171
- handleOnPaste: React.ClipboardEventHandler<HTMLInputElement> = (event) => {
97
+ handleOnPaste = (event) => {
172
98
  const { allowNew, multiple, chipSeparators } = this.props;
173
99
  const { selected } = this.state;
174
100
 
@@ -187,7 +113,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
187
113
  }
188
114
  };
189
115
 
190
- handleOnKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
116
+ handleOnKeyDown = (event) => {
191
117
  const { showSuggestions, allowNew, multiple, chipSeparators, options } = this.props;
192
118
  const { keyboardFocusedOptionIndex, query, selected } = this.state;
193
119
  const chipsMode = !showSuggestions && allowNew && multiple;
@@ -207,7 +133,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
207
133
  break;
208
134
  case 'Enter':
209
135
  event.preventDefault();
210
- if (keyboardFocusedOptionIndex && options[keyboardFocusedOptionIndex]) {
136
+ if (options[keyboardFocusedOptionIndex]) {
211
137
  this.selectItem(options[keyboardFocusedOptionIndex]);
212
138
  } else if (allowNew && query.trim()) {
213
139
  this.selectItem({ label: query });
@@ -224,7 +150,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
224
150
  }
225
151
  };
226
152
 
227
- moveFocusedOption(offset: number) {
153
+ moveFocusedOption(offset) {
228
154
  this.setState((previousState) => {
229
155
  const { keyboardFocusedOptionIndex } = previousState;
230
156
  const { options } = this.props;
@@ -238,7 +164,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
238
164
  });
239
165
  }
240
166
 
241
- selectItem = (item: TypeaheadOption<T>) => {
167
+ selectItem = (item) => {
242
168
  const { multiple } = this.props;
243
169
  let selected = [...this.state.selected];
244
170
  let query;
@@ -257,7 +183,15 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
257
183
  });
258
184
  };
259
185
 
260
- handleSearch = (query: string) => {
186
+ stopPropagation = (event) => {
187
+ event.stopPropagation();
188
+ event.preventDefault();
189
+ if (event.nativeEvent && event.nativeEvent.stopImmediatePropagation) {
190
+ event.nativeEvent.stopImmediatePropagation();
191
+ }
192
+ };
193
+
194
+ handleSearch = (query) => {
261
195
  const { onSearch } = this.props;
262
196
  if (onSearch) {
263
197
  onSearch(query);
@@ -312,7 +246,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
312
246
  );
313
247
  };
314
248
 
315
- updateSelectedValue = (selected: TypeaheadOption<T>[]) => {
249
+ updateSelectedValue = (selected) => {
316
250
  const { onChange, validateChip } = this.props;
317
251
 
318
252
  const errorState = selected.some((chip) => !validateChip(chip));
@@ -321,7 +255,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
321
255
  });
322
256
  };
323
257
 
324
- clear = (event: React.MouseEvent<HTMLButtonElement>) => {
258
+ clear = (event) => {
325
259
  event.preventDefault();
326
260
  if (this.state.selected.length > 0) {
327
261
  this.updateSelectedValue([]);
@@ -332,7 +266,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
332
266
  });
333
267
  };
334
268
 
335
- removeChip = (option: TypeaheadOption<T>) => {
269
+ removeChip = (option) => {
336
270
  const { selected } = this.state;
337
271
 
338
272
  if (selected.length > 0) {
@@ -340,8 +274,8 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
340
274
  }
341
275
  };
342
276
 
343
- renderChip = (option: TypeaheadOption<T>, idx: number): ReactNode => {
344
- const valid = this.props.validateChip?.(option);
277
+ renderChip = (option, idx) => {
278
+ const valid = this.props.validateChip(option);
345
279
 
346
280
  return (
347
281
  <Chip
@@ -365,10 +299,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
365
299
  allowNew,
366
300
  showNewEntry,
367
301
  dropdownOpen,
368
- }: Pick<TypeaheadProps<T>, 'footer' | 'options' | 'id' | 'allowNew' | 'showNewEntry'> &
369
- Pick<TypeaheadState<T>, 'keyboardFocusedOptionIndex' | 'query'> & {
370
- dropdownOpen?: boolean;
371
- }) => {
302
+ }) => {
372
303
  const optionsToRender = [...options];
373
304
  if (
374
305
  allowNew &&
@@ -387,7 +318,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
387
318
  >
388
319
  <ul className="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1">
389
320
  {optionsToRender.map((option, idx) => (
390
- <TypeaheadOption<T>
321
+ <TypeaheadOption
391
322
  key={`${option.label}${idx.toString()}`}
392
323
  query={query}
393
324
  option={option}
@@ -433,7 +364,6 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
433
364
  const menu = this.renderMenu({
434
365
  footer,
435
366
  options,
436
- id,
437
367
  keyboardFocusedOptionIndex,
438
368
  query,
439
369
  allowNew,
@@ -454,7 +384,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
454
384
  'typeahead--multiple': multiple,
455
385
  open: dropdownOpen,
456
386
  })}
457
- onClick={stopPropagation}
387
+ onClick={this.stopPropagation}
458
388
  >
459
389
  <div
460
390
  className={classNames('form-group', {
@@ -470,7 +400,7 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
470
400
  >
471
401
  {addon && <span className="input-group-addon input-group-addon--search">{addon}</span>}
472
402
 
473
- <TypeaheadInput<T>
403
+ <TypeaheadInput
474
404
  {...{
475
405
  autoFocus,
476
406
  multiple,
@@ -479,7 +409,6 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
479
409
  selected,
480
410
  maxHeight,
481
411
  }}
482
- id={id}
483
412
  name={name}
484
413
  value={query}
485
414
  typeaheadId={id}
@@ -506,3 +435,75 @@ export default class Typeahead<T> extends Component<TypeaheadProps<T>, Typeahead
506
435
  );
507
436
  }
508
437
  }
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
+ };
@@ -1,19 +1,18 @@
1
1
  import { select, boolean } from '@storybook/addon-knobs';
2
- import { StoryContext } from '@storybook/react';
3
2
  import { userEvent, within } from '@storybook/test';
4
3
  import { Search as SearchIcon } from '@transferwise/icons';
5
4
  import { useState } from 'react';
6
5
 
7
6
  import { Sentiment } from '../common';
8
7
 
9
- import Typeahead, { type TypeaheadOption } from './Typeahead';
8
+ import Typeahead from './Typeahead';
10
9
 
11
10
  export default {
12
11
  component: Typeahead,
13
12
  title: 'Forms/Typeahead',
14
13
  };
15
14
 
16
- const validateChip = (option: TypeaheadOption) => {
15
+ const validateChip = (option) => {
17
16
  // eslint-disable-next-line unicorn/no-unsafe-regex
18
17
  return /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
19
18
  option.label,
@@ -44,7 +43,7 @@ export const createable = () => {
44
43
  );
45
44
  };
46
45
 
47
- createable.play = async ({ canvasElement }: StoryContext) => {
46
+ createable.play = async ({ canvasElement }) => {
48
47
  const canvas = within(canvasElement);
49
48
  await userEvent.type(canvas.getByRole('combobox'), 'chip{Enter}chip2{Enter}');
50
49
  };
@@ -77,7 +76,7 @@ export const Basic = () => {
77
76
  ]);
78
77
 
79
78
  const validateChipWhenMultiple = () => {
80
- return multiple && allowNew ? (option: TypeaheadOption) => validateChip(option) : undefined;
79
+ return multiple && allowNew ? (option) => validateChip(option) : undefined;
81
80
  };
82
81
 
83
82
  const multiple = boolean('multiple', false);
@@ -102,8 +101,8 @@ export const Basic = () => {
102
101
  showNewEntry={showNewEntry}
103
102
  placeholder="placeholder"
104
103
  chipSeparators={[',', ' ']}
105
- validateChip={validateChipWhenMultiple()}
106
- alert={showAlert ? { message: `Couldn't add item`, type: alertType } : undefined}
104
+ validateChip={validateChipWhenMultiple}
105
+ alert={showAlert && { message: `Couldn't add item`, type: alertType }}
107
106
  addon={<SearchIcon size={24} />}
108
107
  options={options}
109
108
  inputAutoComplete="off"
@@ -116,7 +115,7 @@ export const Basic = () => {
116
115
  );
117
116
  };
118
117
 
119
- Basic.play = async ({ canvasElement }: StoryContext) => {
118
+ Basic.play = async ({ canvasElement }) => {
120
119
  const canvas = within(canvasElement);
121
120
  await userEvent.type(canvas.getByRole('combobox'), 'abc{ArrowDown}');
122
121
  };
@@ -0,0 +1,3 @@
1
+ import Typeahead from './Typeahead';
2
+
3
+ export default Typeahead;
@@ -2,42 +2,16 @@
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 { Component, createRef, ReactNode } from 'react';
5
+ import PropTypes from 'prop-types';
6
+ import { Component } from 'react';
6
7
 
7
8
  import { Input } from '../../inputs/Input';
8
- import { TypeaheadOption, TypeaheadProps } from '../Typeahead';
9
9
 
10
10
  const DEFAULT_INPUT_MIN_WIDTH = 10;
11
11
 
12
- export type TypeaheadInputProps<T> = {
13
- typeaheadId: string;
14
- value: string;
15
- selected: TypeaheadOption<T>[];
16
- optionsShown?: boolean;
17
- autoComplete: string;
18
- onChange: React.ChangeEventHandler<HTMLInputElement>;
19
- onKeyDown: React.KeyboardEventHandler<HTMLInputElement>;
20
- onFocus: () => void;
21
- onPaste: React.ClipboardEventHandler<HTMLInputElement>;
22
- renderChip: (chip: TypeaheadOption<T>, index: number) => ReactNode;
23
- } & Pick<
24
- TypeaheadProps<T>,
25
- 'id' | 'name' | 'autoFocus' | 'multiple' | 'placeholder' | 'maxHeight' | 'onFocus'
26
- >;
27
-
28
- type TypeaheadInputState = {
29
- inputWidth: number;
30
- };
31
-
32
- export default class TypeaheadInput<T> extends Component<
33
- TypeaheadInputProps<T>,
34
- TypeaheadInputState
35
- > {
36
- inputRef = createRef<HTMLInputElement>();
37
- sizerRef = createRef<HTMLDivElement>();
38
-
39
- constructor(props: TypeaheadInputProps<T>) {
40
- super(props);
12
+ export default class TypeaheadInput extends Component {
13
+ constructor() {
14
+ super();
41
15
  this.state = {
42
16
  inputWidth: DEFAULT_INPUT_MIN_WIDTH,
43
17
  };
@@ -46,11 +20,11 @@ export default class TypeaheadInput<T> extends Component<
46
20
  componentDidMount() {
47
21
  const { autoFocus } = this.props;
48
22
  if (autoFocus) {
49
- this.inputRef.current?.focus();
23
+ this.inputRef.focus();
50
24
  }
51
25
  }
52
26
 
53
- componentDidUpdate(previousProps: TypeaheadInputProps<T>) {
27
+ componentDidUpdate(previousProps) {
54
28
  if (previousProps.value !== this.props.value && this.props.multiple) {
55
29
  this.recalculateWidth();
56
30
  }
@@ -59,7 +33,7 @@ export default class TypeaheadInput<T> extends Component<
59
33
  recalculateWidth = () => {
60
34
  requestAnimationFrame(() => {
61
35
  this.setState({
62
- inputWidth: Math.max(DEFAULT_INPUT_MIN_WIDTH, this.sizerRef.current?.scrollWidth ?? 0 + 10),
36
+ inputWidth: Math.max(DEFAULT_INPUT_MIN_WIDTH, this.sizerRef.scrollWidth + 10),
63
37
  });
64
38
  });
65
39
  };
@@ -67,12 +41,12 @@ export default class TypeaheadInput<T> extends Component<
67
41
  renderInput = () => {
68
42
  const {
69
43
  typeaheadId,
70
- autoFocus = false,
44
+ autoFocus,
71
45
  multiple,
72
46
  name,
73
- optionsShown = false,
74
- placeholder = '',
75
- selected = [],
47
+ optionsShown,
48
+ placeholder,
49
+ selected,
76
50
  value,
77
51
  onChange,
78
52
  onKeyDown,
@@ -85,7 +59,9 @@ export default class TypeaheadInput<T> extends Component<
85
59
  const hasPlaceholder = !multiple || selected.length === 0;
86
60
  return (
87
61
  <Input
88
- ref={this.inputRef}
62
+ ref={(reference) => {
63
+ this.inputRef = reference;
64
+ }}
89
65
  className={classnames(multiple && 'typeahead__input')}
90
66
  name={name}
91
67
  id={`input-${typeaheadId}`}
@@ -109,14 +85,14 @@ export default class TypeaheadInput<T> extends Component<
109
85
  };
110
86
 
111
87
  render() {
112
- const { multiple, selected = [], value, maxHeight = null, renderChip } = this.props;
88
+ const { multiple, selected, value, maxHeight, renderChip } = this.props;
113
89
 
114
90
  return multiple ? (
115
91
  <div
116
92
  className="form-control typeahead__input-container"
117
- style={{ maxHeight: maxHeight ?? undefined }}
93
+ style={maxHeight && { maxHeight }}
118
94
  onClick={() => {
119
- this.inputRef.current?.focus();
95
+ this.inputRef.focus();
120
96
  }}
121
97
  >
122
98
  <div className="typeahead__input-wrapper">
@@ -125,7 +101,12 @@ export default class TypeaheadInput<T> extends Component<
125
101
  {this.renderInput()}
126
102
  <div className="typeahead__input-aligner" />
127
103
  </div>
128
- <div ref={this.sizerRef} className="sizer form-control typeahead__input">
104
+ <div
105
+ ref={(reference) => {
106
+ this.sizerRef = reference;
107
+ }}
108
+ className="sizer form-control typeahead__input"
109
+ >
129
110
  {value}
130
111
  </div>
131
112
  </div>
@@ -134,3 +115,30 @@ export default class TypeaheadInput<T> extends Component<
134
115
  );
135
116
  }
136
117
  }
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,18 +1,11 @@
1
1
  /* eslint-disable jsx-a11y/anchor-is-valid */
2
2
  import classNames from 'classnames';
3
+ import PropTypes from 'prop-types';
3
4
 
4
- import { TypeaheadOption } from '../Typeahead';
5
5
  import highlight from '../util/highlight';
6
6
 
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;
7
+ const Option = (props) => {
8
+ const { option, selected, onClick, query } = props;
16
9
  const { label, note, secondary } = option;
17
10
 
18
11
  return (
@@ -34,4 +27,21 @@ const Option = <T,>(props: TypeaheadOptionProps<T>) => {
34
27
  );
35
28
  };
36
29
 
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
+
37
47
  export default Option;
@@ -1,4 +1,4 @@
1
- export default function highlight(value: string, query: string) {
1
+ export default function highlight(value, query) {
2
2
  if (value && query) {
3
3
  const highlightStart = value.toUpperCase().indexOf(query.trim().toUpperCase());
4
4
  const highlightEnd = highlightStart + query.trim().length;