@transferwise/components 46.52.0 → 46.52.2

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 (47) hide show
  1. package/build/i18n/pt.json +2 -0
  2. package/build/i18n/pt.json.js +2 -0
  3. package/build/i18n/pt.json.js.map +1 -1
  4. package/build/i18n/pt.json.mjs +2 -0
  5. package/build/i18n/pt.json.mjs.map +1 -1
  6. package/build/i18n/zh-CN.json +2 -0
  7. package/build/i18n/zh-CN.json.js +2 -0
  8. package/build/i18n/zh-CN.json.js.map +1 -1
  9. package/build/i18n/zh-CN.json.mjs +2 -0
  10. package/build/i18n/zh-CN.json.mjs.map +1 -1
  11. package/build/inputs/SelectInput.js +4 -0
  12. package/build/inputs/SelectInput.js.map +1 -1
  13. package/build/inputs/SelectInput.mjs +4 -0
  14. package/build/inputs/SelectInput.mjs.map +1 -1
  15. package/build/main.css +1 -0
  16. package/build/styles/inputs/InputGroup.css +1 -0
  17. package/build/styles/main.css +1 -0
  18. package/build/typeahead/Typeahead.js +63 -59
  19. package/build/typeahead/Typeahead.js.map +1 -1
  20. package/build/typeahead/Typeahead.messages.js +12 -0
  21. package/build/typeahead/Typeahead.messages.js.map +1 -0
  22. package/build/typeahead/Typeahead.messages.mjs +10 -0
  23. package/build/typeahead/Typeahead.messages.mjs.map +1 -0
  24. package/build/typeahead/Typeahead.mjs +63 -59
  25. package/build/typeahead/Typeahead.mjs.map +1 -1
  26. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  27. package/build/types/typeahead/Typeahead.d.ts +2 -1
  28. package/build/types/typeahead/Typeahead.d.ts.map +1 -1
  29. package/build/types/typeahead/Typeahead.messages.d.ts +9 -0
  30. package/build/types/typeahead/Typeahead.messages.d.ts.map +1 -0
  31. package/package.json +3 -3
  32. package/src/dateInput/DateInput.spec.tsx +9 -9
  33. package/src/dateInput/DateInput.tests.story.tsx +1 -1
  34. package/src/i18n/pt.json +2 -0
  35. package/src/i18n/zh-CN.json +2 -0
  36. package/src/inputs/InputGroup.css +1 -0
  37. package/src/inputs/InputGroup.less +1 -0
  38. package/src/inputs/SelectInput.spec.tsx +5 -5
  39. package/src/inputs/SelectInput.story.tsx +19 -7
  40. package/src/inputs/SelectInput.tsx +4 -0
  41. package/src/main.css +1 -0
  42. package/src/moneyInput/MoneyInput.story.tsx +1 -1
  43. package/src/typeahead/Typeahead.messages.ts +9 -0
  44. package/src/typeahead/Typeahead.rtl.spec.tsx +13 -1
  45. package/src/typeahead/Typeahead.spec.js +12 -10
  46. package/src/typeahead/Typeahead.story.tsx +194 -195
  47. package/src/typeahead/Typeahead.tsx +16 -9
@@ -1,5 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
- import { expect, fn, screen, userEvent, within, type Mock } from '@storybook/test';
2
+ import { expect, fn, type Mock, screen, userEvent, within } from '@storybook/test';
3
3
  import { Calendar, ChevronDown } from '@transferwise/icons';
4
4
  import { Flag } from '@wise/art';
5
5
  import { clsx } from 'clsx';
@@ -8,6 +8,7 @@ import { useState } from 'react';
8
8
  import Button from '../button/Button';
9
9
  import { getMonthNames } from '../common/dateUtils';
10
10
  import Drawer from '../drawer';
11
+ import { Field } from '../field/Field';
11
12
  import Modal from '../modal';
12
13
  import { wait } from '../test-utils/wait';
13
14
  import {
@@ -73,12 +74,12 @@ export const Months: Story<Month | null> = {
73
74
  const canvas = within(canvasElement);
74
75
 
75
76
  await step('renders placeholder', async () => {
76
- const triggerButton = canvas.getByRole('button');
77
+ const triggerButton = canvas.getByRole('combobox');
77
78
  await expect(triggerButton).toHaveTextContent('Month');
78
79
  });
79
80
 
80
81
  await step('selects option via mouse', async () => {
81
- const triggerButton = canvas.getByRole('button');
82
+ const triggerButton = canvas.getByRole('combobox');
82
83
 
83
84
  await userEvent.click(triggerButton);
84
85
  await userEvent.unhover(triggerButton);
@@ -195,7 +196,7 @@ export const Currencies: Story<Currency> = {
195
196
  await expect(within(screen.getByRole('listbox')).queryAllByRole('option')).toHaveLength(8);
196
197
  await expect(screen.getByText(/^Can’t find it?/u)).toBeInTheDocument();
197
198
 
198
- const input = screen.getByRole('searchbox');
199
+ const input = screen.getByRole('combobox');
199
200
 
200
201
  await wait(0); // TODO: Remove
201
202
  await userEvent.type(input, 'huf');
@@ -234,7 +235,7 @@ export const MultipleCurrencies: Story<Currency, true> = {
234
235
  const canvas = within(canvasElement);
235
236
 
236
237
  await step('selects multiple options via mouse', async () => {
237
- const triggerButton = canvas.getByRole('button');
238
+ const triggerButton = canvas.getByRole('combobox');
238
239
 
239
240
  await userEvent.click(triggerButton);
240
241
  await userEvent.unhover(triggerButton);
@@ -272,7 +273,7 @@ export const CustomTrigger: Story<Month> = {
272
273
  play: async ({ canvasElement }) => {
273
274
  const canvas = within(canvasElement);
274
275
 
275
- const triggerButton = canvas.getByRole('button');
276
+ const triggerButton = canvas.getByRole('combobox');
276
277
  await userEvent.click(triggerButton);
277
278
  },
278
279
  };
@@ -317,7 +318,7 @@ export const Advanced: Story<Month> = {
317
318
  play: async ({ canvasElement }) => {
318
319
  const canvas = within(canvasElement);
319
320
 
320
- const triggerButton = canvas.getByRole('button');
321
+ const triggerButton = canvas.getByRole('combobox');
321
322
  await userEvent.click(triggerButton);
322
323
  },
323
324
  };
@@ -342,6 +343,17 @@ export const ManyItems: Story<string, true> = {
342
343
  },
343
344
  };
344
345
 
346
+ export const WithinField = {
347
+ args: Months.args,
348
+ decorators: [
349
+ (Story) => (
350
+ <Field message="Something went wrong" sentiment="negative">
351
+ <Story />
352
+ </Field>
353
+ ),
354
+ ],
355
+ } satisfies Story<Month | null>;
356
+
345
357
  export const WithinDrawer: Story<Currency> = {
346
358
  args: CurrenciesArgs,
347
359
  decorators: [
@@ -430,6 +430,7 @@ export function SelectInputTriggerButton<T extends SelectInputTriggerButtonEleme
430
430
  <ListboxBase.Button
431
431
  ref={ref}
432
432
  as={PolymorphicWithOverrides}
433
+ role="combobox"
433
434
  __overrides={{ as, ...interactionProps }}
434
435
  {...mergeProps({ onClick, onKeyDown }, restProps)}
435
436
  />
@@ -608,9 +609,12 @@ function SelectInputOptions<T = string>({
608
609
  <div className="np-select-input-query-container">
609
610
  <SearchInput
610
611
  ref={searchInputRef}
612
+ role="combobox"
611
613
  shape="rectangle"
612
614
  placeholder={filterPlaceholder}
613
615
  defaultValue={filterQuery}
616
+ aria-autocomplete="list"
617
+ aria-expanded
614
618
  aria-controls={listboxId}
615
619
  aria-describedby={showStatus ? statusId : undefined}
616
620
  onKeyDown={(event) => {
package/src/main.css CHANGED
@@ -2387,6 +2387,7 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
2387
2387
  }
2388
2388
  .np-input-group {
2389
2389
  display: inline-grid;
2390
+ width: 100%;
2390
2391
  grid-auto-columns: minmax(0, 1fr);
2391
2392
  /* Prevent unwanted `group-hover/input` triggers */
2392
2393
  border-radius: 9999px;
@@ -153,7 +153,7 @@ export const OpenedInput: Story = {
153
153
  ...MultipleCurrencies,
154
154
  play: async ({ canvasElement }) => {
155
155
  const canvas = within(canvasElement);
156
- await userEvent.click(canvas.getByRole('button'));
156
+ await userEvent.click(canvas.getByRole('combobox'));
157
157
  },
158
158
  };
159
159
 
@@ -0,0 +1,9 @@
1
+ import { defineMessages } from 'react-intl';
2
+
3
+ export default defineMessages({
4
+ clearLabel: {
5
+ id: 'neptune.ClearButton.ariaLabel',
6
+ defaultMessage: 'Clear',
7
+ description: 'Description of clear button',
8
+ },
9
+ });
@@ -1,14 +1,26 @@
1
1
  import { Field } from '../field/Field';
2
2
  import { mockMatchMedia, render, screen } from '../test-utils';
3
3
  import Typeahead from './Typeahead';
4
+ import { createIntl, createIntlCache } from 'react-intl';
5
+ import messages from '../i18n';
6
+ import { DEFAULT_LANG, DEFAULT_LOCALE } from '../common';
4
7
 
5
8
  mockMatchMedia();
6
9
 
10
+ const cache = createIntlCache();
11
+ const intl = createIntl({ locale: DEFAULT_LOCALE, messages: messages[DEFAULT_LANG] }, cache);
12
+
7
13
  describe('Typeahead', () => {
8
14
  it('supports `Field` for labeling', () => {
9
15
  render(
10
16
  <Field id="test" label="Tags">
11
- <Typeahead id="test" name="test" options={[{ label: 'Test' }]} onChange={() => {}} />
17
+ <Typeahead
18
+ id="test"
19
+ name="test"
20
+ options={[{ label: 'Test' }]}
21
+ intl={intl}
22
+ onChange={() => {}}
23
+ />
12
24
  </Field>,
13
25
  );
14
26
  expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Tags/);
@@ -8,15 +8,17 @@ import { fakeEvent, fakeKeyDownEventForKey } from '../common/fakeEvents';
8
8
  import Typeahead from './Typeahead';
9
9
 
10
10
  const defaultLocale = 'en-GB';
11
-
12
- jest.mock('react-intl', () => ({
13
- injectIntl: (Component) =>
14
- function (props) {
15
- return <Component {...props} intl={{ locale: defaultLocale }} />;
16
- },
17
- useIntl: () => ({ locale: defaultLocale, formatMessage: (id) => `${id}` }),
18
- defineMessages: (translations) => translations,
19
- }));
11
+ jest.mock('react-intl', () => {
12
+ const mockedIntl = {
13
+ locale: defaultLocale,
14
+ formatMessage: (id) => String(id),
15
+ };
16
+ return {
17
+ injectIntl: (Component) => (props) => <Component {...props} intl={mockedIntl} />,
18
+ defineMessages: (translations) => translations,
19
+ useIntl: () => mockedIntl,
20
+ };
21
+ });
20
22
 
21
23
  describe('Typeahead', () => {
22
24
  let component;
@@ -317,7 +319,7 @@ describe('Typeahead', () => {
317
319
  onChange: (selections) => {
318
320
  selectedOption = selections[0];
319
321
  },
320
- options: options,
322
+ options,
321
323
  });
322
324
 
323
325
  input().simulate('change', { target: { value: text } });
@@ -1,125 +1,122 @@
1
- import { select, boolean } from '@storybook/addon-knobs';
2
- import { StoryContext } from '@storybook/react';
3
- import { userEvent, within } from '@storybook/test';
1
+ import { Meta, StoryObj } from '@storybook/react';
4
2
  import { Search as SearchIcon } from '@transferwise/icons';
5
- import { useState } from 'react';
3
+ import { userEvent, within, fn } from '@storybook/test';
6
4
 
7
- import { Sentiment } from '../common';
5
+ import Typeahead, { type TypeaheadOption } from './Typeahead';
6
+ import { Size } from '../common';
7
+ import { useState } from 'react';
8
8
  import { Input } from '../inputs/Input';
9
9
 
10
- import Typeahead, { type TypeaheadOption } from './Typeahead';
10
+ type Story = StoryObj<typeof Typeahead>;
11
+
12
+ /**
13
+ * Checks if provided TypeaheadOption contains an HTML5-compliant email address
14
+ * @see https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
15
+ */
16
+ const validateOptionAsEmail = (option: TypeaheadOption) => {
17
+ return /^[\w.!#$%&'*+/=?^`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i.test(
18
+ option.label,
19
+ );
20
+ };
11
21
 
12
22
  export default {
13
23
  component: Typeahead,
14
24
  title: 'Forms/Typeahead',
25
+ args: {
26
+ allowNew: false,
27
+ autoFillOnBlur: true,
28
+ autoFocus: false,
29
+ chipSeparators: [',', ' '],
30
+ clearable: true,
31
+ inputAutoComplete: 'new-password',
32
+ minQueryLength: 3,
33
+ multiple: false,
34
+ searchDelay: 200,
35
+ showSuggestions: true,
36
+ showNewEntry: true,
37
+ size: Size.MEDIUM,
38
+ initialValue: [],
39
+ id: 'myTypeahead',
40
+ name: 'typeahead-input-name',
41
+ placeholder: 'placeholder',
42
+ onChange: fn(),
43
+ onBlur: fn(),
44
+ onFocus: fn(),
45
+ onInputChange: fn(),
46
+ onSearch: fn(),
47
+ },
48
+ argTypes: {
49
+ size: {
50
+ control: 'inline-radio',
51
+ options: [Size.MEDIUM, Size.LARGE],
52
+ },
53
+ },
54
+ } satisfies Meta<typeof Typeahead>;
55
+
56
+ export const Basic: Story = {
57
+ render: function Render(args) {
58
+ const [options, setOptions] = useState([
59
+ {
60
+ label: 'A thing',
61
+ note: 'with a note',
62
+ },
63
+ {
64
+ label: 'Another thing',
65
+ secondary: 'with secondary text this time',
66
+ },
67
+ {
68
+ label: 'Profile',
69
+ },
70
+ {
71
+ label: 'Globe',
72
+ },
73
+ {
74
+ label: 'British pound',
75
+ },
76
+ {
77
+ label: 'Euro',
78
+ },
79
+ {
80
+ label: 'Something else',
81
+ },
82
+ ]);
83
+
84
+ const validateChipWhenMultiple = () =>
85
+ args.multiple && args.allowNew
86
+ ? (option: TypeaheadOption) => validateOptionAsEmail(option)
87
+ : undefined;
88
+
89
+ return (
90
+ <Typeahead
91
+ {...args}
92
+ initialValue={[]}
93
+ validateChip={validateChipWhenMultiple()}
94
+ addon={<SearchIcon size={24} />}
95
+ options={options}
96
+ onSearch={() => {
97
+ setTimeout(() => setOptions(options), 1500);
98
+ }}
99
+ />
100
+ );
101
+ },
15
102
  };
16
103
 
17
- const validateChip = (option: TypeaheadOption) => {
18
- // eslint-disable-next-line unicorn/no-unsafe-regex
19
- 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(
20
- option.label,
21
- );
22
- };
23
-
24
- export const createable = () => {
25
- return (
104
+ export const Creatable: Story = {
105
+ render: (args) => (
26
106
  <Typeahead
27
- id="typeahead"
28
- name="typeahead-input-name"
29
- size="md"
30
- maxHeight={100}
31
- footer={<div>Want a footer? Style it!</div>}
32
- multiple
33
- clearable
107
+ {...args}
34
108
  allowNew
35
- showSuggestions={false}
36
- showNewEntry={false}
37
- placeholder="placeholder"
38
- chipSeparators={[',', ' ']}
39
- validateChip={validateChip}
109
+ multiple
110
+ initialValue={[]}
111
+ validateChip={validateOptionAsEmail}
40
112
  addon={<SearchIcon size={24} />}
41
113
  options={[]}
42
- onChange={() => {}}
43
- onBlur={() => {}}
44
114
  />
45
- );
46
- };
47
-
48
- createable.play = async ({ canvasElement }: StoryContext) => {
49
- const canvas = within(canvasElement);
50
- await userEvent.type(canvas.getByRole('combobox'), 'chip{Enter}chip2{Enter}');
51
- };
52
-
53
- export const Basic = () => {
54
- const [options, setOptions] = useState([
55
- {
56
- label: 'A thing',
57
- note: 'with a note',
58
- },
59
- {
60
- label: 'Another thing',
61
- secondary: 'with secondary text this time',
62
- },
63
- {
64
- label: 'Profile',
65
- },
66
- {
67
- label: 'Globe',
68
- },
69
- {
70
- label: 'British pound',
71
- },
72
- {
73
- label: 'Euro',
74
- },
75
- {
76
- label: 'Something else',
77
- },
78
- ]);
79
-
80
- const validateChipWhenMultiple = () => {
81
- return multiple && allowNew ? (option: TypeaheadOption) => validateChip(option) : undefined;
82
- };
83
-
84
- const multiple = boolean('multiple', false);
85
- const clearable = boolean('clearable', false);
86
- const allowNew = boolean('allowNew', false);
87
- const showSuggestions = boolean('showSuggestions', true);
88
- const showNewEntry = boolean('showNewEntry', true);
89
- const showAlert = boolean('alert', false);
90
- const alertType = select('alert type', [Sentiment.ERROR, Sentiment.WARNING], Sentiment.ERROR);
91
-
92
- return (
93
- <Typeahead
94
- id="typeahead"
95
- name="typeahead-input-name"
96
- size="md"
97
- maxHeight={100}
98
- footer={<div>Want a footer? Style it!</div>}
99
- multiple={multiple}
100
- clearable={clearable}
101
- allowNew={allowNew}
102
- showSuggestions={showSuggestions}
103
- showNewEntry={showNewEntry}
104
- placeholder="placeholder"
105
- chipSeparators={[',', ' ']}
106
- validateChip={validateChipWhenMultiple()}
107
- alert={showAlert ? { message: `Couldn't add item`, type: alertType } : undefined}
108
- addon={<SearchIcon size={24} />}
109
- options={options}
110
- inputAutoComplete="off"
111
- onSearch={() => {
112
- setTimeout(() => setOptions(options), 1500);
113
- }}
114
- onChange={() => {}}
115
- onBlur={() => {}}
116
- />
117
- );
118
- };
119
-
120
- Basic.play = async ({ canvasElement }: StoryContext) => {
121
- const canvas = within(canvasElement);
122
- await userEvent.type(canvas.getByRole('combobox'), 'abc{ArrowDown}');
115
+ ),
116
+ play: async ({ canvasElement }) => {
117
+ const canvas = within(canvasElement);
118
+ await userEvent.type(canvas.getByRole('combobox'), 'chip{Enter}hello@wise.com{Enter}');
119
+ },
123
120
  };
124
121
 
125
122
  type Result =
@@ -134,98 +131,100 @@ type Result =
134
131
 
135
132
  type SearchState = 'success' | 'idle' | 'error' | 'loading';
136
133
 
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);
134
+ /**
135
+ * @FIXME This story feels incomplete. It ignores bunch of props
136
+ * and seems very opinionated. Surely we can do better?
137
+ */
138
+ export const Search: Story = {
139
+ render: function Render(args) {
140
+ const [results, setResults] = useState<Result[]>([]);
141
+ const [state, setState] = useState<SearchState>('idle');
142
+ const [filledValue, setFilledValue] = useState<string | null>(null);
143
+
144
+ const handleInputChange = (query: string) => {
145
+ args?.onInputChange?.(query);
146
+
147
+ if (query === 'loading' || query === 'error' || query === 'nothing') {
148
+ setState(query === 'nothing' ? 'success' : query);
149
+ setResults([]);
150
+ return;
151
+ }
141
152
 
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
153
  setState('success');
155
- setResults([]);
156
- return;
157
- }
154
+ setResults(getResults(query));
155
+ };
158
156
 
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
- };
157
+ const handleResultSelected = (option: Result) => {
158
+ if (option.type === 'search') {
159
+ setResults([
160
+ { type: 'action', value: `${option.value} Result #1` },
161
+ { type: 'action', value: `${option.value} Result #2` },
162
+ { type: 'action', value: `${option.value} Result #3` },
163
+ ]);
164
+ }
165
+ if (option.type === 'action') {
166
+ setFilledValue(option.value);
167
+ }
168
+ };
169
+
170
+ const handleChange = (values: TypeaheadOption<Result>[]) => {
171
+ args?.onChange?.(values);
172
+
173
+ if (values.length > 0) {
174
+ const [updatedValue] = values;
175
+
176
+ if (updatedValue.value) {
177
+ handleResultSelected(updatedValue.value);
178
+ }
179
+ }
180
+ };
181
+
182
+ const getResults = (query: string): Result[] => {
183
+ return [
184
+ { type: 'action', value: `${query} Result #1` },
185
+ { type: 'action', value: `${query} Result #2` },
186
+ { type: 'action', value: `${query} Result #3` },
187
+ { type: 'search', value: `Search for more: '${query}'` },
188
+ ];
189
+ };
190
+
191
+ const renderFooter = (options: Result[]) => {
192
+ let output = null;
216
193
 
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
- }
194
+ if (state === 'loading') {
195
+ output = 'Loading…';
196
+ }
221
197
 
222
- if (state === 'success' && options.length === 0) {
223
- return <p className="m-y-2 m-x-2">No results found</p>;
224
- }
198
+ if (state === 'success' && options.length === 0) {
199
+ output = 'No results found';
200
+ }
225
201
 
226
- if (state === 'error' && options.length === 0) {
227
- return <div className="m-y-2 m-x-2">Something went wrong</div>;
228
- }
202
+ if (state === 'error' && options.length === 0) {
203
+ output = 'Something went wrong';
204
+ }
229
205
 
230
- return null;
231
- }
206
+ return <p className="m-y-2 m-x-2">{output}</p>;
207
+ };
208
+
209
+ return (
210
+ <>
211
+ <Typeahead<Result>
212
+ {...args}
213
+ initialValue={undefined}
214
+ footer={renderFooter(results)}
215
+ addon={<SearchIcon />}
216
+ options={results.map((option) => ({
217
+ value: option,
218
+ label: option.value,
219
+ keepFocusOnSelect: option.type === 'search',
220
+ clearQueryOnSelect: option.type === 'action',
221
+ }))}
222
+ onChange={handleChange}
223
+ onInputChange={handleInputChange}
224
+ />
225
+
226
+ {filledValue != null ? <Input value={filledValue} /> : null}
227
+ </>
228
+ );
229
+ },
230
+ };