|
| 1 | +import ReactSelect, { ControlProps, MenuListProps, ValueContainerProps } from 'react-select' |
| 2 | +import { ReactElement, useCallback, useMemo } from 'react' |
| 3 | +import { ReactComponent as ErrorIcon } from '@Icons/ic-warning.svg' |
| 4 | +import { ReactComponent as ICInfoFilledOverride } from '@Icons/ic-info-filled-override.svg' |
| 5 | +import { ComponentSizeType } from '@Shared/constants' |
| 6 | +import { ConditionalWrap } from '@Common/Helper' |
| 7 | +import Tippy from '@tippyjs/react' |
| 8 | +import { getCommonSelectStyle } from './utils' |
| 9 | +import { |
| 10 | + SelectPickerClearIndicator, |
| 11 | + SelectPickerControl, |
| 12 | + SelectPickerDropdownIndicator, |
| 13 | + SelectPickerLoadingIndicator, |
| 14 | + SelectPickerMenuList, |
| 15 | + SelectPickerOption, |
| 16 | + SelectPickerValueContainer, |
| 17 | +} from './common' |
| 18 | +import { SelectPickerOptionType, SelectPickerProps } from './type' |
| 19 | + |
| 20 | +/** |
| 21 | + * Generic component for select picker |
| 22 | + * |
| 23 | + * @example With icon in control |
| 24 | + * ```tsx |
| 25 | + * <SelectPicker ... icon={<CustomIcon />} /> |
| 26 | + * ``` |
| 27 | + * |
| 28 | + * @example Medium menu list width |
| 29 | + * ```tsx |
| 30 | + * <SelectPicker ... menuSize={ComponentSizeType.medium} /> |
| 31 | + * ``` |
| 32 | + * |
| 33 | + * @example Large menu list width |
| 34 | + * ```tsx |
| 35 | + * <SelectPicker ... menuSize={ComponentSizeType.large} /> |
| 36 | + * ``` |
| 37 | + * |
| 38 | + * @example Required label |
| 39 | + * ```tsx |
| 40 | + * <SelectPicker ... required label="Label" /> |
| 41 | + * ``` |
| 42 | + * |
| 43 | + * @example Custom label |
| 44 | + * ```tsx |
| 45 | + * <SelectPicker ... label={<div>Label</div>} /> |
| 46 | + * ``` |
| 47 | + * |
| 48 | + * @example Error state |
| 49 | + * ```tsx |
| 50 | + * <SelectPicker ... error="Something went wrong" /> |
| 51 | + * ``` |
| 52 | + * |
| 53 | + * @example Helper text |
| 54 | + * ```tsx |
| 55 | + * <SelectPicker ... helperText="Help information" /> |
| 56 | + * ``` |
| 57 | + * |
| 58 | + * @example Menu list footer |
| 59 | + * The footer is sticky by default |
| 60 | + * ```tsx |
| 61 | + * <SelectPicker |
| 62 | + * ... |
| 63 | + * renderMenuListFooter={() => ( |
| 64 | + * <div className="px-8 py-6 dc__border-top bcn-50 cn-6"> |
| 65 | + * <div>Foot note</div> |
| 66 | + * </div> |
| 67 | + * )} |
| 68 | + * /> |
| 69 | + * ``` |
| 70 | + * |
| 71 | + * @example Loading state |
| 72 | + * ```tsx |
| 73 | + * <SelectPicker ... isLoading /> |
| 74 | + * ``` |
| 75 | + * |
| 76 | + * @example Disabled state |
| 77 | + * ```tsx |
| 78 | + * <SelectPicker ... isDisabled /> |
| 79 | + * ``` |
| 80 | + * |
| 81 | + * @example Loading & disabled state |
| 82 | + * ```tsx |
| 83 | + * <SelectPicker ... isLoading isDisabled /> |
| 84 | + * ``` |
| 85 | + * |
| 86 | + * @example Hide selected option icon in control |
| 87 | + * ```tsx |
| 88 | + * <SelectPicker ... showSelectedOptionIcon={false} /> |
| 89 | + * ``` |
| 90 | + * |
| 91 | + * @example Selected option clearable |
| 92 | + * ```tsx |
| 93 | + * <SelectPicker ... isClearable /> |
| 94 | + * ``` |
| 95 | + * |
| 96 | + * @example Selected option clearable |
| 97 | + * ```tsx |
| 98 | + * <SelectPicker ... showSelectedOptionsCount /> |
| 99 | + * ``` |
| 100 | + */ |
| 101 | +const SelectPicker = ({ |
| 102 | + error, |
| 103 | + icon, |
| 104 | + renderMenuListFooter, |
| 105 | + helperText, |
| 106 | + placeholder = 'Select a option', |
| 107 | + label, |
| 108 | + showSelectedOptionIcon = true, |
| 109 | + size = ComponentSizeType.medium, |
| 110 | + disabledTippyContent, |
| 111 | + showSelectedOptionsCount = false, |
| 112 | + menuSize, |
| 113 | + ...props |
| 114 | +}: SelectPickerProps) => { |
| 115 | + const { inputId, required, isDisabled } = props |
| 116 | + |
| 117 | + const labelId = `${inputId}-label` |
| 118 | + const errorElementId = `${inputId}-error-msg` |
| 119 | + |
| 120 | + const selectStyles = useMemo( |
| 121 | + () => |
| 122 | + getCommonSelectStyle({ |
| 123 | + error, |
| 124 | + size, |
| 125 | + menuSize, |
| 126 | + }), |
| 127 | + [error, size, menuSize], |
| 128 | + ) |
| 129 | + |
| 130 | + const renderControl = useCallback( |
| 131 | + (controlProps: ControlProps<SelectPickerOptionType>) => ( |
| 132 | + <SelectPickerControl {...controlProps} icon={icon} showSelectedOptionIcon={showSelectedOptionIcon} /> |
| 133 | + ), |
| 134 | + [icon, showSelectedOptionIcon], |
| 135 | + ) |
| 136 | + |
| 137 | + const renderMenuList = useCallback( |
| 138 | + (menuProps: MenuListProps<SelectPickerOptionType>) => ( |
| 139 | + <SelectPickerMenuList {...menuProps} renderMenuListFooter={renderMenuListFooter} /> |
| 140 | + ), |
| 141 | + [], |
| 142 | + ) |
| 143 | + |
| 144 | + const renderValueContainer = useCallback( |
| 145 | + (valueContainerProps: ValueContainerProps<SelectPickerOptionType>) => ( |
| 146 | + <SelectPickerValueContainer {...valueContainerProps} showSelectedOptionsCount={showSelectedOptionsCount} /> |
| 147 | + ), |
| 148 | + [showSelectedOptionsCount], |
| 149 | + ) |
| 150 | + |
| 151 | + const renderDisabledTippy = (children: ReactElement) => ( |
| 152 | + <Tippy content={disabledTippyContent} placement="top" className="default-tt" arrow={false}> |
| 153 | + {children} |
| 154 | + </Tippy> |
| 155 | + ) |
| 156 | + |
| 157 | + return ( |
| 158 | + <div className="flex column left top dc__gap-4"> |
| 159 | + {/* Note: Common out for fields */} |
| 160 | + <div className="flex column left top dc__gap-6 w-100"> |
| 161 | + {label && ( |
| 162 | + <label |
| 163 | + className="fs-13 lh-20 cn-7 fw-4 dc__block mb-0" |
| 164 | + htmlFor={inputId} |
| 165 | + data-testid={`label-${inputId}`} |
| 166 | + id={labelId} |
| 167 | + > |
| 168 | + {typeof label === 'string' ? ( |
| 169 | + <span className={`flex left ${required ? 'dc__required-field' : ''}`}> |
| 170 | + <span className="dc__truncate">{label}</span> |
| 171 | + {required && <span> </span>} |
| 172 | + </span> |
| 173 | + ) : ( |
| 174 | + label |
| 175 | + )} |
| 176 | + </label> |
| 177 | + )} |
| 178 | + <ConditionalWrap condition={isDisabled && !!disabledTippyContent} wrap={renderDisabledTippy}> |
| 179 | + <div className="w-100"> |
| 180 | + <ReactSelect<SelectPickerOptionType, boolean> |
| 181 | + {...props} |
| 182 | + placeholder={placeholder} |
| 183 | + components={{ |
| 184 | + IndicatorSeparator: null, |
| 185 | + LoadingIndicator: SelectPickerLoadingIndicator, |
| 186 | + DropdownIndicator: SelectPickerDropdownIndicator, |
| 187 | + Control: renderControl, |
| 188 | + Option: SelectPickerOption, |
| 189 | + MenuList: renderMenuList, |
| 190 | + ClearIndicator: SelectPickerClearIndicator, |
| 191 | + ValueContainer: renderValueContainer, |
| 192 | + }} |
| 193 | + styles={selectStyles} |
| 194 | + menuPlacement="auto" |
| 195 | + menuPosition="fixed" |
| 196 | + menuShouldScrollIntoView |
| 197 | + backspaceRemovesValue={false} |
| 198 | + aria-errormessage={errorElementId} |
| 199 | + aria-invalid={!!error} |
| 200 | + aria-labelledby={labelId} |
| 201 | + /> |
| 202 | + </div> |
| 203 | + </ConditionalWrap> |
| 204 | + </div> |
| 205 | + {error && ( |
| 206 | + <div className="flex left dc__gap-4 cr-5 fs-11 lh-16 fw-4" id={errorElementId}> |
| 207 | + <ErrorIcon className="icon-dim-16 p-1 form__icon--error dc__no-shrink dc__align-self-start" /> |
| 208 | + <span className="dc__ellipsis-right__2nd-line">{error}</span> |
| 209 | + </div> |
| 210 | + )} |
| 211 | + {/* Note: Common out for input fields */} |
| 212 | + {helperText && ( |
| 213 | + <div className="flex left dc__gap-4 fs-11 lh-16 cn-7"> |
| 214 | + <ICInfoFilledOverride className="icon-dim-16 dc__no-shrink dc__align-self-start" /> |
| 215 | + <span className="dc__ellipsis-right__2nd-line">{helperText}</span> |
| 216 | + </div> |
| 217 | + )} |
| 218 | + </div> |
| 219 | + ) |
| 220 | +} |
| 221 | + |
| 222 | +export default SelectPicker |
0 commit comments