import { useCallback, useMemo, useState } from 'react'; import { Args, ArgTypes, Meta, Story } from '@storybook/react'; import { AvatarProps } from '../../avatar'; import AvatarListItem from './components/avatar-list-item'; import { Autocomplete } from './autocomplete'; import type { AutocompleteProps, AutocompleteOptionProps } from './types'; import type { DropDownMenuProps } from '../../dropdown-menu'; import { stringToColor } from '../../../utils'; const mockItems: AutocompleteOptionProps[] = []; for (let i = 1; i <= 20; i++) { const randomNum = Math.random() * 10; const randomStr = randomNum.toString(36).substring(2); const randomText = randomNum < 5 ? ' - ' + randomStr.substring(Math.floor(randomNum)) : ''; mockItems.push({ name: `Item${i}${randomText}`, selected: false }); } const autocompleteItems = mockItems; const argTypes: Partial> = { size: { description: 'Set the size of the input.', options: ['small', 'medium'], control: { type: 'select' }, defaultValue: { summary: 'medium' } }, placeholder: { description: 'Placeholder to be displayed when no value is selected.', table: { defaultValue: { summary: '' } } }, multiple: { description: 'Enable multiple selection functionality (i.e chips).', control: { type: 'boolean' } }, defaultLimitTags: { description: 'Maximum number of tags to be seen on focus.', table: { defaultValue: { summary: 2 } } }, showLimitTagsTooltip: { description: 'Whether to show tooltip with a list of tags or not (works only when `multiple` is enabled).', control: { type: 'boolean' } }, freeSolo: { description: 'Enable text input (i.e chips creation, works only when `multiple` is enabled).', control: { type: 'boolean' } }, options: { description: 'Array of options (each option is of type `AutocompleteOptionProps`).', control: { type: 'array' } }, handleClear: { description: 'Callback to be fired on field clear icon click. Will affect only when `multiple` is provided.' }, handleCreate: { description: 'Callback to be fired when the user enters a new value. Will affect only when `multiple` and `freeSolo` are provided.' }, handleDelete: { description: 'Callback to be fired on tag clear icon click. Will affect only when `multiple` is provided. @param {AutocompleteOptionProps[]} newValue The new value that should be set.' }, handleTextChangeCB: { description: 'Callback which is called on text change when `multiple` set to `true`. Should be used to filter `displayOptions`.' }, handleMenuClick: { description: 'Callback to be fired on menu item click.' }, renderInput: { description: 'Render the input. @param {object} params. @returns {ReactNode}' } }; export default { component: Autocomplete, title: 'Forms/Autocomplete/Chips Grouping', argTypes } as Meta; const Template: Story = args => { const { defaultValue, options } = args; const selectedOptions = useMemo(() => { if (!defaultValue) return options; const defaultValues = ( Array.isArray(defaultValue) ? defaultValue : [defaultValue] ) as AutocompleteOptionProps[]; return options?.map(option => ({ ...option, selected: !!defaultValues?.find(value => option?.name === value?.name) })); }, [defaultValue, options]); // Autocomplete single const [autocompleteOptionsSingle, setAutocompleteOptionsSingle] = useState(selectedOptions); const handleAutocompleteItemClickSingle: NonNullable = v => () => { setAutocompleteOptionsSingle( autocompleteOptionsSingle.map(o => ({ ...o, selected: v === o.name ? !o.selected : false })) ); }; const autocompleteValueSingle = useMemo( () => autocompleteOptionsSingle.find(o => o.selected) || ({} as AutocompleteOptionProps), [autocompleteOptionsSingle] ); // Autocomplete multiple const [autocompleteOptionsMultiple, setAutocompleteOptionsMultiple] = useState(selectedOptions); const handleAutocompleteItemClickMultiple: NonNullable = v => () => { setAutocompleteOptionsMultiple( autocompleteOptionsMultiple.map(o => ({ ...o, selected: v === o.name ? !o.selected : o.selected })) ); }; const autocompleteValueMultiple = useMemo( () => autocompleteOptionsMultiple.filter(o => o.selected), [autocompleteOptionsMultiple] ); const [textMultiple, setTextMultiple] = useState(''); const displayOptionsMultiple = useMemo(() => { if (!textMultiple) { return autocompleteOptionsMultiple; } return autocompleteOptionsMultiple.filter(o => { const normalizedName = o.name.toString().toLowerCase(); const words = [normalizedName]; if (o.additionalProps?.subtext) words.push((o.additionalProps?.subtext as string).toString().toLowerCase()); return words.some(word => word.includes(textMultiple)); }); }, [autocompleteOptionsMultiple, textMultiple]); const handleAutocompleteMultipleTextChange: AutocompleteProps['handleTextChangeCB'] = useCallback( e => { setTextMultiple(e.target.value); }, [] ); // Autocomplete freeSolo const handleAutocompleteClear: AutocompleteProps['handleClear'] = useCallback(() => { setAutocompleteOptionsMultiple( autocompleteOptionsMultiple.map(option => ({ ...option, selected: false })) ); }, [autocompleteOptionsMultiple]); const handleAutocompleteCreate: AutocompleteProps['handleCreate'] = useCallback( (e, v) => { const value = v.slice(-1)[0]?.toString(); value && setAutocompleteOptionsMultiple( autocompleteOptionsMultiple.concat([ { name: value, selected: true } ]) ); }, [autocompleteOptionsMultiple] ); const handleAutocompleteDelete: AutocompleteProps['handleDelete'] = useCallback((e, v) => { setAutocompleteOptionsMultiple(v); }, []); return ( ); }; export const ChipsWithLimitTags = Template.bind({}); ChipsWithLimitTags.args = { size: 'medium', placeholder: '', multiple: true, defaultLimitTags: 2, showLimitTagsTooltip: false, freeSolo: false, options: autocompleteItems, defaultValue: autocompleteItems.slice(0, 12), handleDelete: () => alert('Will delete a single chip') }; export const ActiveWithoutCollapsing = Template.bind({}); ActiveWithoutCollapsing.args = { size: 'medium', placeholder: '', multiple: true, defaultLimitTags: -1, alwaysExpand: true, showLimitTagsTooltip: true, freeSolo: true, options: autocompleteItems, defaultValue: autocompleteItems.slice(0, 12), handleDelete: () => alert('Will delete a single chip') }; export const ActiveWithCollapsing = Template.bind({}); ActiveWithCollapsing.args = { size: 'medium', placeholder: 'Type text', multiple: true, defaultLimitTags: 2, showLimitTagsTooltip: true, freeSolo: true, alwaysExpand: false, options: [] }; export const AutocompleteWithAvatar = Template.bind({}); const avatarMapFN = (item: AutocompleteOptionProps) => { const { name, additionalProps } = item; const avatarProps: AvatarProps = { backgroundColor: stringToColor(name.toString()), customSize: additionalProps?.type === 'person' ? 24 : 32, variant: additionalProps?.type === 'person' ? 'circular' : 'rounded', src: additionalProps?.src as string }; return { id: name, name: name, textNode: ( ) }; }; AutocompleteWithAvatar.args = { options: [ { name: 'Dima Kurdon', additionalProps: { subtext: '123@kurdon.com', type: 'person' } }, { name: 'Tal Dangur', additionalProps: { subtext: 'tal@dangur.com', type: 'person' } }, { name: 'Blumshapiro', additionalProps: { subtext: 'Blumshapiro', type: 'company', src: 'https://i.pinimg.com/originals/e5/a9/e8/e5a9e877bcacdc5713d2a8f98412762d.png' } }, { name: 'Something', additionalProps: { subtext: 'Something Something', type: 'company' } } ], showSearchIcon: true, listItemsMapFN: avatarMapFN };