import { EmojiType, IEmojiIcon } from '@vev/utils'; import { isEqual } from 'lodash'; import React, { Children, KeyboardEvent, ReactNode, useMemo, useRef, useState } from 'react'; import { useFormField, useUniqId } from '../../hooks'; import { BoxSize, BoxSpace, SilkeBox } from '../silke-box'; import { FormValidator } from '../silke-form'; import { SilkeIcon, SilkeIcons } from '../silke-icon'; import { PopoverOrigin } from '../silke-popover'; import { SilkeText, SilkeTextSmall } from '../silke-text'; import { SilkeTextFieldItem, SilkeTextFieldProps } from '../silke-text-field'; import { SilkeTextFieldOutline } from '../silke-text-field/silke-text-field-outline'; import { useTextFieldContext } from '../silke-text-field/text-field-context'; import { AutocompleteRenderItemProps } from './autocomplete-item'; import AutocompleteTags, { AutocompleteTagsProps } from './autocomplete-tags'; import { Data, Entry, MultiProps, SingleProps } from './autocomplete-types'; import { SilkeAutocompletePopup } from './silke-autocomplete-popup'; import styles from './silke-autocomplete-field.scss'; /** * Props for autocomplete field */ export type SilkeAutocompleteFieldProps = any> = ( | MultiProps | SingleProps ) & Omit & { disableSearch?: boolean; disableAddFromList?: boolean; noReset?: boolean; hideListArrow?: boolean; isVariable?: boolean; maxDisplayedItems?: number; maxDisplayedItemsLabel?: React.ReactNode; showAddIcon?: boolean; selectedVariableKey?: string; /** What point of the target should be used for centering default center-center */ targetOrigin?: PopoverOrigin; /** What point of the anchor should be used for centering default center-center */ anchorOrigin?: PopoverOrigin; dropDownMinWidth?: number; dropDownMaxWidth?: number; /** * Possible values */ items: U[]; /** * Minimum number of characters in search for results to show */ minChars?: number; buttonRight?: ReactNode; /** Cypress tag */ 'data-cy'?: string; /** * If a function is passed, an "Add item" entry will be shown in the list if nothing matches text search. This function will be the callback. * @param value The value entered */ onAdd?: (value: string) => void; /** * Use custom component for rendering the autocomplete items */ renderItem?: (props: AutocompleteRenderItemProps) => React.ReactElement | null; /** * Use custom component for rendering autocomplete tags */ renderTags?: (props: AutocompleteTagsProps) => React.ReactElement | null; /** * Custom sort of items (default sorts by label) */ sort?: (one: Entry, other: Entry) => number; onSearch?: (value: string) => void; /** * Called when dropdown closes for any reason */ onClose?: () => void; }; const autocompleteValidator: FormValidator = (props, value) => { if (props.multiple) { const len = value?.length || 0; if (len > (props.maxItems || 1000)) return `To many item, max ${props.maxItems}`; if (len < (props.minItems || 0)) return `To few item, min ${props.minItems}`; } if (props.required && !value && value !== 0) return 'Required'; return undefined; }; /** * @deprecated Use SilkeAutocompleteFieldNative instead. This component will be removed in a future version. */ export function SilkeAutocompleteField(props: SilkeAutocompleteFieldProps) { const { hideListArrow, isVariable, items, placeholder, renderTags: TagsRender = AutocompleteTags, label, inline, flex, disabled, tabIndex, onClick, onMouseDown, hAlign, onClose, autoFocus, ...form } = useFormField(props, autocompleteValidator); const context = useTextFieldContext(); const size = form.size || context.size || 'base'; const uid = useUniqId('silke-input'); const inputId = form.id || uid; const inputField = useRef(); const isSelected = (value: T): boolean => { if (form.multiple) { for (const v of Array.isArray(form.value) ? form.value : []) { if (isEqual(v, value)) return true; } return false; } return isEqual(value, form.value); }; /** This list contains all *data* entries, dividers are stripped */ const dataItems = items.filter((entry) => !entry.divider) as Data[]; const selectedDataItems = dataItems.filter((item) => isSelected(item.value)); const [selectedLabel, selectedIcon, selectedImage] = useMemo((): [ label?: string, icon?: SilkeIcons | IEmojiIcon, image?: string, ] => { if (form.multiple) return []; const entry = items.find((entry) => !entry.divider && isEqual(entry.value, form.value)); return [entry?.label || (form.value && form.value.toString()) || '', entry?.icon, entry?.image]; }, [items, form.multiple, form.value]); const containerRef = useRef(null); const [open, setOpen] = useState(false); const before: ReactNode[] = []; const after: ReactNode[] = []; Children.forEach(form.children, (child) => { if (React.isValidElement(child) && child.type === SilkeTextFieldItem && child.props.before) { before.push(child); } else { after.push(child); } }); const handleReset = (e?: React.MouseEvent) => { e?.stopPropagation(); if (form.multiple) form.onChange([]); else form.onChange(''); onClose && onClose(); setOpen(false); }; const handleRemoveSingle = (remove: T) => { if (form.multiple && form.value) { const value = Array.isArray(form.value) ? form.value.filter((v: T) => remove !== v) : []; if (!isEqual(value, form.value)) form.onChange(value); } }; const displayReset = props.multiple && !form.noReset && Array.isArray(form.value) && form.value.length > 0; const dataCy = props['data-cy'] || undefined; const kind = props?.kind || context.kind || 'default'; const boxSize: BoxSize = size === 's' ? 's' : 'base'; let dropDownSize = size; if (kind !== 'default') dropDownSize = 's'; else if (size === 's') dropDownSize = 's'; let cl = styles.root; if (open) cl += ' ' + styles.open; if (styles[kind]) cl += ' ' + styles[kind]; if (disabled) cl += ' ' + styles.disabled; if (isVariable) cl += ' ' + styles.variable; return ( <> ) => { e.preventDefault(); e.stopPropagation(); onMouseDown?.(e); if (disabled) return; !context.readOnly && setOpen((wasOpen) => { return !wasOpen; }); }} > {before} { if ((e.key === 'Enter' || e.key === 'ArrowDown') && !open && !disabled) { e.preventDefault(); setOpen(true); } }} onFocus={() => { if (!open && !disabled) setOpen(true); }} > {form.multiple && Array.isArray(form.value) && form.value.length > 0 ? ( ) : ( <> {selectedImage && ( )} {selectedIcon && ( )} {selectedLabel && size === 's' && ( {selectedLabel} )} {selectedLabel && size !== 's' && {selectedLabel}} {placeholder && !selectedLabel && ( {placeholder} )} )} {(displayReset || !hideListArrow) && ( {(() => { return displayReset ? ( ) : !hideListArrow ? ( ) : null; })()} )} {after} {open && ( setOpen(false)} {...props} /> )} ); }