@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.
- package/build/i18n/pt.json +2 -0
- package/build/i18n/pt.json.js +2 -0
- package/build/i18n/pt.json.js.map +1 -1
- package/build/i18n/pt.json.mjs +2 -0
- package/build/i18n/pt.json.mjs.map +1 -1
- package/build/i18n/zh-CN.json +2 -0
- package/build/i18n/zh-CN.json.js +2 -0
- package/build/i18n/zh-CN.json.js.map +1 -1
- package/build/i18n/zh-CN.json.mjs +2 -0
- package/build/i18n/zh-CN.json.mjs.map +1 -1
- package/build/inputs/SelectInput.js +4 -0
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs +4 -0
- package/build/inputs/SelectInput.mjs.map +1 -1
- package/build/main.css +1 -0
- package/build/styles/inputs/InputGroup.css +1 -0
- package/build/styles/main.css +1 -0
- package/build/typeahead/Typeahead.js +63 -59
- package/build/typeahead/Typeahead.js.map +1 -1
- package/build/typeahead/Typeahead.messages.js +12 -0
- package/build/typeahead/Typeahead.messages.js.map +1 -0
- package/build/typeahead/Typeahead.messages.mjs +10 -0
- package/build/typeahead/Typeahead.messages.mjs.map +1 -0
- package/build/typeahead/Typeahead.mjs +63 -59
- package/build/typeahead/Typeahead.mjs.map +1 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/typeahead/Typeahead.d.ts +2 -1
- package/build/types/typeahead/Typeahead.d.ts.map +1 -1
- package/build/types/typeahead/Typeahead.messages.d.ts +9 -0
- package/build/types/typeahead/Typeahead.messages.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/dateInput/DateInput.spec.tsx +9 -9
- package/src/dateInput/DateInput.tests.story.tsx +1 -1
- package/src/i18n/pt.json +2 -0
- package/src/i18n/zh-CN.json +2 -0
- package/src/inputs/InputGroup.css +1 -0
- package/src/inputs/InputGroup.less +1 -0
- package/src/inputs/SelectInput.spec.tsx +5 -5
- package/src/inputs/SelectInput.story.tsx +19 -7
- package/src/inputs/SelectInput.tsx +4 -0
- package/src/main.css +1 -0
- package/src/moneyInput/MoneyInput.story.tsx +1 -1
- package/src/typeahead/Typeahead.messages.ts +9 -0
- package/src/typeahead/Typeahead.rtl.spec.tsx +13 -1
- package/src/typeahead/Typeahead.spec.js +12 -10
- package/src/typeahead/Typeahead.story.tsx +194 -195
- 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
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
156
|
+
await userEvent.click(canvas.getByRole('combobox'));
|
|
157
157
|
},
|
|
158
158
|
};
|
|
159
159
|
|
|
@@ -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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
322
|
+
options,
|
|
321
323
|
});
|
|
322
324
|
|
|
323
325
|
input().simulate('change', { target: { value: text } });
|
|
@@ -1,125 +1,122 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
3
|
+
import { userEvent, within, fn } from '@storybook/test';
|
|
6
4
|
|
|
7
|
-
import {
|
|
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
|
-
|
|
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
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
157
|
-
}
|
|
154
|
+
setResults(getResults(query));
|
|
155
|
+
};
|
|
158
156
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
194
|
+
if (state === 'loading') {
|
|
195
|
+
output = 'Loading…';
|
|
196
|
+
}
|
|
221
197
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
198
|
+
if (state === 'success' && options.length === 0) {
|
|
199
|
+
output = 'No results found';
|
|
200
|
+
}
|
|
225
201
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
202
|
+
if (state === 'error' && options.length === 0) {
|
|
203
|
+
output = 'Something went wrong';
|
|
204
|
+
}
|
|
229
205
|
|
|
230
|
-
|
|
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
|
+
};
|