@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.
- package/README.md +14 -1
- package/build/index.esm.js +147 -49
- package/build/index.esm.js.map +1 -1
- package/build/index.js +147 -49
- package/build/index.js.map +1 -1
- package/build/mocks.esm.js +40 -0
- package/build/mocks.esm.js.map +1 -0
- package/build/mocks.js +43 -0
- package/build/mocks.js.map +1 -0
- package/build/types/index.d.ts +0 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/mocks.d.ts +9 -0
- package/build/types/mocks.d.ts.map +1 -0
- package/build/types/test-utils/window-mock.d.ts.map +1 -1
- package/build/types/typeahead/Typeahead.d.ts +57 -98
- package/build/types/typeahead/Typeahead.d.ts.map +1 -1
- package/build/types/typeahead/index.d.ts +2 -2
- package/build/types/typeahead/index.d.ts.map +1 -1
- package/build/types/typeahead/typeaheadInput/TypeaheadInput.d.ts +41 -23
- package/build/types/typeahead/typeaheadInput/TypeaheadInput.d.ts.map +1 -1
- package/build/types/typeahead/typeaheadOption/TypeaheadOption.d.ts +17 -9
- package/build/types/typeahead/typeaheadOption/TypeaheadOption.d.ts.map +1 -1
- package/build/types/typeahead/util/highlight.d.ts +1 -2
- package/build/types/typeahead/util/highlight.d.ts.map +1 -1
- package/package.json +23 -10
- package/src/dimmer/Dimmer.spec.js +0 -4
- package/src/index.ts +0 -1
- package/src/mocks.ts +48 -0
- package/src/snackbar/Snackbar.spec.js +0 -4
- package/src/test-utils/window-mock.ts +7 -23
- package/src/typeahead/{Typeahead.tsx → Typeahead.js} +108 -107
- package/src/typeahead/{Typeahead.story.tsx → Typeahead.story.js} +7 -8
- package/src/typeahead/index.js +3 -0
- package/src/typeahead/typeaheadInput/{TypeaheadInput.tsx → TypeaheadInput.js} +51 -43
- package/src/typeahead/typeaheadOption/{TypeaheadOption.tsx → TypeaheadOption.js} +20 -10
- package/src/typeahead/util/{highlight.tsx → highlight.js} +1 -1
- package/src/withNextPortal/withNextPortal.spec.js +0 -4
- 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
|
-
|
|
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
|
-
|
|
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
|
|
9
|
+
import PropTypes from 'prop-types';
|
|
10
|
+
import { Component } from 'react';
|
|
11
11
|
|
|
12
12
|
import Chip from '../chips/Chip';
|
|
13
|
-
import { Size, Sentiment
|
|
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
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
+
|
|
65
|
+
if (onFocus) {
|
|
66
|
+
this.props.onFocus();
|
|
67
|
+
}
|
|
142
68
|
};
|
|
143
69
|
|
|
144
|
-
onOptionSelected = (event
|
|
70
|
+
onOptionSelected = (event, item) => {
|
|
145
71
|
event.preventDefault();
|
|
146
72
|
this.selectItem(item);
|
|
147
73
|
};
|
|
148
74
|
|
|
149
|
-
handleOnChange
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
344
|
-
const valid = this.props.validateChip
|
|
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
|
-
}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 }
|
|
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
|
|
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
|
|
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 }
|
|
118
|
+
Basic.play = async ({ canvasElement }) => {
|
|
120
119
|
const canvas = within(canvasElement);
|
|
121
120
|
await userEvent.type(canvas.getByRole('combobox'), 'abc{ArrowDown}');
|
|
122
121
|
};
|
|
@@ -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
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
23
|
+
this.inputRef.focus();
|
|
50
24
|
}
|
|
51
25
|
}
|
|
52
26
|
|
|
53
|
-
componentDidUpdate(previousProps
|
|
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.
|
|
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
|
|
44
|
+
autoFocus,
|
|
71
45
|
multiple,
|
|
72
46
|
name,
|
|
73
|
-
optionsShown
|
|
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={
|
|
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
|
|
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={
|
|
93
|
+
style={maxHeight && { maxHeight }}
|
|
118
94
|
onClick={() => {
|
|
119
|
-
this.inputRef.
|
|
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
|
|
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
|
-
|
|
8
|
-
option
|
|
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
|
|
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;
|