import { Meta, StoryObj } from '@storybook/react-webpack5'; import { Search as SearchIcon } from '@transferwise/icons'; import { userEvent, within, fn } from 'storybook/test'; import { useState } from 'react'; import { Sentiment, Size } from '../common'; import { Input } from '../inputs/Input'; import { Field } from '../field/Field'; import Button from '../button'; import Modal from '../modal'; import { InlinePromptProps } from '../prompt'; import Typeahead, { type TypeaheadOption } from './Typeahead'; // needed for SB to display correct name in the docs (Typeahead as React.FC).displayName = 'Typeahead'; type Story = StoryObj; /** * Checks if provided TypeaheadOption contains an HTML5-compliant email address * @see https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address */ const validateOptionAsEmail = (option: TypeaheadOption) => { 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( option.label, ); }; const meta: Meta = { component: Typeahead, title: 'Forms/Typeahead', args: { allowNew: false, autoFillOnBlur: true, autoFocus: false, chipSeparators: [',', ' '], clearable: true, inputAutoComplete: 'new-password', minQueryLength: 3, multiple: false, searchDelay: 200, showSuggestions: true, showNewEntry: true, alert: undefined, size: Size.MEDIUM, initialValue: [], id: 'myTypeahead', name: 'typeahead-input-name', placeholder: 'placeholder', onChange: fn(), onBlur: fn(), onFocus: fn(), onInputChange: fn(), onSearch: fn(), }, argTypes: { size: { control: 'inline-radio', options: [Size.MEDIUM, Size.LARGE], }, alert: { description: 'Displays an [InlinePrompt](?path=/docs/prompts-inlineprompt--docs) below the input when provided.
**⚠️ DEPRECATED:** Please use [<Field />](?path=/docs/forms-field--docs) component and its `message` and `sentiment` props instead. `error`, `info` and `success` alert types are no longer supported and will be soon removed.
', control: 'object', table: { type: { summary: '{ message: ReactNode; type?: "negative" | "neutral" | "positive" | "warning" | "proposition" }', }, }, }, }, }; export default meta; export const Basic: Story = { render: function Render(args) { const [options, setOptions] = useState([ { label: 'A thing', note: 'with a note', }, { label: 'Another thing', secondary: 'with secondary text this time', }, { label: 'Profile', }, { label: 'Globe', }, { label: 'British pound', }, { label: 'Euro', }, { label: 'Something else', }, ]); const validateChipWhenMultiple = () => args.multiple && args.allowNew ? (option: TypeaheadOption) => validateOptionAsEmail(option) : undefined; return ( } options={options} onSearch={() => { setTimeout(() => setOptions(options), 1500); }} /> ); }, }; export const Creatable: Story = { render: (args) => ( } options={[]} /> ), play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.type(canvas.getByRole('combobox'), 'chip{Enter}hello@wise.com{Enter}'); }, }; type Result = | { type: 'action'; value: string; } | { type: 'search'; value: string; }; type SearchState = 'success' | 'idle' | 'error' | 'loading'; /** * @FIXME This story feels incomplete. It ignores bunch of props * and seems very opinionated. Surely we can do better? */ export const Search: Story = { render: function Render(args) { const [results, setResults] = useState([]); const [state, setState] = useState('idle'); const [filledValue, setFilledValue] = useState(null); const handleInputChange = (query: string) => { args?.onInputChange?.(query); if (query === 'loading' || query === 'error' || query === 'nothing') { setState(query === 'nothing' ? 'success' : query); setResults([]); return; } setState('success'); setResults(getResults(query)); }; const handleResultSelected = (option: Result) => { if (option.type === 'search') { setResults([ { type: 'action', value: `${option.value} Result #1` }, { type: 'action', value: `${option.value} Result #2` }, { type: 'action', value: `${option.value} Result #3` }, ]); } if (option.type === 'action') { setFilledValue(option.value); } }; const handleChange = (values: TypeaheadOption[]) => { args?.onChange?.(values); if (values.length > 0) { const [updatedValue] = values; if (updatedValue.value) { handleResultSelected(updatedValue.value); } } }; const getResults = (query: string): Result[] => { return [ { type: 'action', value: `${query} Result #1` }, { type: 'action', value: `${query} Result #2` }, { type: 'action', value: `${query} Result #3` }, { type: 'search', value: `Search for more: '${query}'` }, ]; }; const renderFooter = (options: Result[]) => { let output = null; if (state === 'loading') { output = 'Loading…'; } if (state === 'success' && options.length === 0) { output = 'No results found'; } if (state === 'error' && options.length === 0) { output = 'Something went wrong'; } return

{output}

; }; return ( <> {...args} initialValue={undefined} footer={renderFooter(results)} addon={} options={results.map((option) => ({ value: option, label: option.value, keepFocusOnSelect: option.type === 'search', clearQueryOnSelect: option.type === 'action', }))} onChange={handleChange} onInputChange={handleInputChange} /> {filledValue != null ? : null} ); }, }; export const WithField: Story = { args: { autoFillOnBlur: undefined, chipSeparators: undefined, clearable: undefined, initialValue: undefined, inputAutoComplete: undefined, minQueryLength: undefined, onBlur: undefined, onFocus: undefined, onInputChange: undefined, onSearch: undefined, placeholder: undefined, searchDelay: undefined, showSuggestions: undefined, showNewEntry: undefined, size: undefined, }, render: function Render(args) { const options = [{ label: 'Option 1' }]; const alertTypes: InlinePromptProps['sentiment'][] = [ Sentiment.NEGATIVE, Sentiment.WARNING, Sentiment.NEUTRAL, Sentiment.POSITIVE, 'proposition', ]; return ( <> {alertTypes.map((sentiment) => ( ))} ); }, decorators: (Story) => (
), }; /** * > **⚠️ DEPRECATED:** Please use [<Field />](?path=/docs/forms-field--docs) component and its `message` and `sentiment` props instead.
`error`, `info` and `success` alert types are no longer supported and will be soon removed. */ export const WithAlert: Story = { name: 'With Alert (deprecated)', args: { autoFillOnBlur: undefined, chipSeparators: undefined, clearable: undefined, initialValue: undefined, inputAutoComplete: undefined, minQueryLength: undefined, onBlur: undefined, onFocus: undefined, onInputChange: undefined, onSearch: undefined, placeholder: undefined, searchDelay: undefined, showSuggestions: undefined, showNewEntry: undefined, size: undefined, }, parameters: { docs: { canvas: { sourceState: 'hidden', }, }, }, render: function Render(args) { const options = [{ label: 'Option 1' }]; const alertTypes: InlinePromptProps['sentiment'][] = [ Sentiment.NEGATIVE, Sentiment.WARNING, Sentiment.NEUTRAL, Sentiment.POSITIVE, 'proposition', ]; return ( <> {alertTypes.map((sentiment) => ( ))} ); }, decorators: (Story) => (
), };