/* eslint-disable react/jsx-props-no-spreading */ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import Select from 'react-select'; import AsyncSelect from 'react-select/async'; import CreatableSelect from 'react-select/creatable'; import styled, { css } from 'styled-components'; import { Controller } from 'react-hook-form'; import useTranslation from 'next-translate/useTranslation'; import get from 'lodash.get'; import { InputFieldWrapper, InputLabel, InputError, Row } from './styles'; import { validationErrorMessage } from '../../utils/helper'; import Help from './Help'; import RequiredIndicator from './Required/Indicator'; import { theme } from '../../styles'; const reactSelectStyles = { default: { container: (base) => ({ ...base, width: '100%', }), control: (base) => ({ ...base, minHeight: '4.4rem', borderRadius: '0.2rem', }), singleValue: (base) => ({ ...base, color: theme.colors.lightGray, }), }, rounded: { container: (base) => ({ ...base, width: '100%', }), control: (base) => ({ ...base, minHeight: '4.4rem', borderRadius: `${theme.metrics.baseRadius}rem`, }), singleValue: (base) => ({ ...base, color: theme.colors.lightGray, }), indicatorSeparator: () => ({ display: 'none', }), dropdownIndicator: (base) => ({ ...base, color: theme.colors.secondary, }), }, gray: { container: (base) => ({ ...base, width: '100%', }), control: (base) => ({ ...base, backgroundColor: theme.colors.lightGray4, borderColor: theme.colors.lightGray4, minHeight: '4.4rem', borderRadius: `${theme.metrics.baseRadius}rem`, }), singleValue: (base) => ({ ...base, color: theme.colors.lightGray, }), indicatorSeparator: () => ({ display: 'none', }), dropdownIndicator: (base) => ({ ...base, color: theme.colors.secondary, }), }, lightRounded: { container: (base) => ({ ...base, width: '100%', }), control: (base) => ({ ...base, minHeight: '4.4rem', borderRadius: `${theme.metrics.baseRadius}rem`, borderColor: theme.colors.lightGray4, }), singleValue: (base) => ({ ...base, color: theme.colors.darkGray, }), indicatorSeparator: () => ({ display: 'none', }), placeholder: (base) => ({ ...base, color: theme.colors.lightGray3, }), }, }; const styles = css` width: 100%; margin: 0.5rem 0; font-size: 1.4rem; `; const StyledSelect = styled(Select)` ${styles} `; const StyledCreatable = styled(CreatableSelect)` ${styles} `; const StyledAsync = styled(AsyncSelect)` ${styles} `; const Hint = styled.span` ${({ theme: { colors } }) => css` color: ${colors.lightGray2}; margin-bottom: 1rem; display: inline-block; `} `; const SelectField = ({ name, form, label, help, options, defaultOptions, validation, creatable, onCreate, isMulti, callback, wrapperCss, variant, isHidden, isLoading, instanceId, isAsync, onChange, ...selectProps }) => { const { t } = useTranslation(['error']); const [needsUpdate, setNeedsUpdate] = useState(true); const [internalIsLoading, setInternalIsLoading] = useState(false); const [selectOptions, setSelectOptions] = useState(options); const { formState: { errors } = {}, control, watch, setValue, getValues } = form; const errorObject = get(errors, name); const hasError = typeof errorObject !== 'undefined'; let selectedValue = watch(name); if (selectedValue) { selectedValue = Array.isArray(selectedValue) ? selectedValue.map((value) => `${value}`) : selectedValue; selectedValue = Array.isArray(selectedValue) && !isMulti ? selectedValue[0] : selectedValue; selectedValue = !Array.isArray(selectedValue) && typeof selectedValue !== 'object' ? `${selectedValue}` : selectedValue; } /** * Compares each option's label with each other in order to sort them later. * * @param {object} firstOption The first option * @param {object} secondOption The second option * @returns {number} */ function compareOptions(firstOption, secondOption) { const { label: firstLabel } = firstOption; const { label: secondLabel } = secondOption; return firstLabel.localeCompare(secondLabel); } // update the select options whenever options prop changes useEffect(() => { const useOptionsFrom = isAsync ? defaultOptions : options; setSelectOptions(useOptionsFrom.sort(compareOptions)); }, [options, defaultOptions, isAsync]); /** * React-select expects value to be in { value: '', label: '' } shape so we run a useEffect * to ensure it's in the right format. This allows this component to be intialized just with the value. */ useEffect(() => { if (!needsUpdate) { return; } if ( (!options || options.length === 0) && (!defaultOptions || defaultOptions.length === 0) ) { return; } const useOptionsFrom = isAsync ? defaultOptions : options; if (!selectedValue) { setNeedsUpdate(false); return; } if (isMulti) { setValue( name, selectedValue.map((value) => useOptionsFrom.find((option) => `${option.value}` === `${value}`), ), { shouldDirty: true }, ); } else if (typeof selectedValue === 'object') { setValue( name, useOptionsFrom.find((option) => `${option.value}` === `${selectedValue.value}`), { shouldDirty: true }, ); } else { setValue( name, useOptionsFrom.find((option) => `${option.value}` === `${selectedValue}`), { shouldDirty: true }, ); } setNeedsUpdate(false); }, [selectedValue, options, defaultOptions, name, setValue, isMulti, isAsync, needsUpdate]); /** * Handles creating a new element in the select field. * * Only called if `creatable` is true. * * @param {string} inputValue The inserted input value. * */ const onCreateOption = async (inputValue) => { setInternalIsLoading(true); const newOption = await onCreate(inputValue); setInternalIsLoading(false); setSelectOptions([...options, newOption]); const currentValue = getValues(name) || []; if (isMulti) { setValue(name, [...currentValue, newOption], { shouldDirty: true }); } else { setValue(name, newOption, { shouldDirty: true }); } return newOption; }; // eslint-disable-next-line no-nested-ternary const Component = creatable ? StyledCreatable : isAsync ? StyledAsync : StyledSelect; return ( <InputFieldWrapper hasError={hasError} customCss={wrapperCss} isHidden={isHidden}> {label && ( <InputLabel htmlFor={name}> {label} {validation.required && <RequiredIndicator />} </InputLabel> )} <Row> <Controller control={control} rules={validation} name={name} render={({ field }) => ( <Component id={name} className="react-select-container" classNamePrefix="react-select" aria-label={label} aria-required={validation.required} options={selectOptions} defaultOptions={selectOptions} isMulti={isMulti} onCreateOption={creatable ? onCreateOption : null} isDisabled={internalIsLoading || isLoading || isHidden} isLoading={internalIsLoading || isLoading} styles={reactSelectStyles[variant]} instanceId={instanceId} {...selectProps} {...field} onChange={(selectedValues) => { if (typeof callback === 'function') callback(selectedValues); if (typeof onChange === 'function') return field.onChange(onChange(selectedValues)); return field.onChange(selectedValues); }} /> )} /> {help && <Help id={name} label={label} HelpComponent={help} />} </Row> {creatable && ( <Hint> É possível adicionar novas opções neste campo. Basta digitar a opção e pressionar a tecla Enter. </Hint> )} {hasError && Object.keys(errors).length ? ( <InputError>{validationErrorMessage(errors, name, t)}</InputError> ) : null} </InputFieldWrapper> ); }; SelectField.propTypes = { name: PropTypes.string.isRequired, label: PropTypes.string, creatable: PropTypes.bool, onCreate: PropTypes.func, isMulti: PropTypes.bool, form: PropTypes.shape({ formState: PropTypes.shape({ errors: PropTypes.shape({}) }), control: PropTypes.shape({}), watch: PropTypes.func, setValue: PropTypes.func, getValues: PropTypes.func, }), help: PropTypes.node, /** * @see https://react-hook-form.com/api#register */ validation: PropTypes.shape({ required: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), }), options: PropTypes.arrayOf( PropTypes.shape({ label: PropTypes.string, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }), ), defaultOptions: PropTypes.arrayOf( PropTypes.shape({ label: PropTypes.string, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }), ), callback: PropTypes.func, wrapperCss: PropTypes.arrayOf(PropTypes.string), variant: PropTypes.oneOf(['default', 'rounded', 'gray', 'lightRounded']), isHidden: PropTypes.bool, isLoading: PropTypes.bool, instanceId: PropTypes.string, isAsync: PropTypes.bool, onChange: PropTypes.func, }; SelectField.defaultProps = { label: '', form: {}, creatable: false, onCreate: () => {}, isMulti: false, validation: {}, options: [], defaultOptions: [], help: null, callback: null, wrapperCss: [], variant: 'default', isHidden: false, isLoading: false, instanceId: '', isAsync: false, onChange: null, }; export default SelectField;