AssetInput.tsx
1 import { XCircleIcon } from '@heroicons/react/solid'; 2 import { Trans } from '@lingui/macro'; 3 import { 4 Box, 5 BoxProps, 6 Button, 7 CircularProgress, 8 FormControl, 9 IconButton, 10 InputBase, 11 ListItemText, 12 MenuItem, 13 Select, 14 SelectChangeEvent, 15 Typography, 16 useTheme, 17 } from '@mui/material'; 18 import React, { ReactNode } from 'react'; 19 import NumberFormat, { NumberFormatProps } from 'react-number-format'; 20 import { TrackEventProps } from 'src/store/analyticsSlice'; 21 import { useRootStore } from 'src/store/root'; 22 23 import { CapType } from '../caps/helper'; 24 import { AvailableTooltip } from '../infoTooltips/AvailableTooltip'; 25 import { FormattedNumber } from '../primitives/FormattedNumber'; 26 import { TokenIcon } from '../primitives/TokenIcon'; 27 28 interface CustomProps { 29 onChange: (event: { target: { name: string; value: string } }) => void; 30 name: string; 31 value: string; 32 } 33 34 export const NumberFormatCustom = React.forwardRef<NumberFormatProps, CustomProps>( 35 function NumberFormatCustom(props, ref) { 36 const { onChange, ...other } = props; 37 38 return ( 39 <NumberFormat 40 {...other} 41 getInputRef={ref} 42 onValueChange={(values) => { 43 if (values.value !== props.value) 44 onChange({ 45 target: { 46 name: props.name, 47 value: values.value || '', 48 }, 49 }); 50 }} 51 thousandSeparator 52 isNumericString 53 allowNegative={false} 54 /> 55 ); 56 } 57 ); 58 59 export interface Asset { 60 balance?: string; 61 symbol: string; 62 iconSymbol?: string; 63 address?: string; 64 aToken?: boolean; 65 priceInUsd?: string; 66 decimals?: number; 67 } 68 69 export interface AssetInputProps<T extends Asset = Asset> { 70 value: string; 71 usdValue: string; 72 symbol: string; 73 onChange?: (value: string) => void; 74 disabled?: boolean; 75 disableInput?: boolean; 76 onSelect?: (asset: T) => void; 77 assets: T[]; 78 capType?: CapType; 79 maxValue?: string; 80 isMaxSelected?: boolean; 81 inputTitle?: ReactNode; 82 balanceText?: ReactNode; 83 loading?: boolean; 84 event?: TrackEventProps; 85 selectOptionHeader?: ReactNode; 86 selectOption?: (asset: T) => ReactNode; 87 sx?: BoxProps; 88 exchangeRateComponent?: ReactNode; 89 } 90 91 export const AssetInput = <T extends Asset = Asset>({ 92 value, 93 usdValue, 94 symbol, 95 onChange, 96 disabled, 97 disableInput, 98 onSelect, 99 assets, 100 capType, 101 maxValue, 102 isMaxSelected, 103 inputTitle, 104 balanceText, 105 loading = false, 106 event, 107 selectOptionHeader, 108 selectOption, 109 sx = {}, 110 exchangeRateComponent, 111 }: AssetInputProps<T>) => { 112 const theme = useTheme(); 113 const trackEvent = useRootStore((store) => store.trackEvent); 114 const handleSelect = (event: SelectChangeEvent) => { 115 const newAsset = assets.find((asset) => asset.symbol === event.target.value) as T; 116 onSelect && onSelect(newAsset); 117 onChange && onChange(''); 118 }; 119 120 const asset = 121 assets.length === 1 122 ? assets[0] 123 : assets && (assets.find((asset) => asset.symbol === symbol) as T); 124 125 return ( 126 <Box {...sx}> 127 <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> 128 <Typography color="text.secondary"> 129 {inputTitle ? inputTitle : <Trans>Amount</Trans>} 130 </Typography> 131 {capType && <AvailableTooltip capType={capType} />} 132 </Box> 133 134 <Box 135 sx={(theme) => ({ 136 border: `1px solid ${theme.palette.divider}`, 137 borderRadius: '6px', 138 overflow: 'hidden', 139 })} 140 > 141 <Box sx={{ display: 'flex', alignItems: 'center', mb: 0.5, px: 3, py: 2 }}> 142 {loading ? ( 143 <Box sx={{ flex: 1 }}> 144 <CircularProgress color="inherit" size="16px" /> 145 </Box> 146 ) : ( 147 <InputBase 148 sx={{ flex: 1 }} 149 placeholder="0.00" 150 disabled={disabled || disableInput} 151 value={value} 152 autoFocus 153 onChange={(e) => { 154 if (!onChange) return; 155 if (Number(e.target.value) > Number(maxValue)) { 156 onChange('-1'); 157 } else { 158 onChange(e.target.value); 159 } 160 }} 161 inputProps={{ 162 'aria-label': 'amount input', 163 style: { 164 fontSize: '21px', 165 lineHeight: '28,01px', 166 padding: 0, 167 height: '28px', 168 textOverflow: 'ellipsis', 169 whiteSpace: 'nowrap', 170 overflow: 'hidden', 171 }, 172 }} 173 // eslint-disable-next-line 174 inputComponent={NumberFormatCustom as any} 175 /> 176 )} 177 {value !== '' && !disableInput && ( 178 <IconButton 179 sx={{ 180 minWidth: 0, 181 p: 0, 182 left: 8, 183 zIndex: 1, 184 color: 'text.muted', 185 '&:hover': { 186 color: 'text.secondary', 187 }, 188 }} 189 onClick={() => { 190 onChange && onChange(''); 191 }} 192 disabled={disabled} 193 > 194 <XCircleIcon height={16} /> 195 </IconButton> 196 )} 197 {!onSelect || assets.length === 1 ? ( 198 <Box sx={{ display: 'inline-flex', alignItems: 'center' }}> 199 <TokenIcon 200 aToken={asset.aToken} 201 symbol={asset.iconSymbol || asset.symbol} 202 sx={{ mr: 2, ml: 4 }} 203 /> 204 <Typography variant="h3" sx={{ lineHeight: '28px' }} data-cy={'inputAsset'}> 205 {symbol} 206 </Typography> 207 </Box> 208 ) : ( 209 <FormControl> 210 <Select 211 disabled={disabled} 212 value={asset.symbol} 213 onChange={handleSelect} 214 variant="outlined" 215 className="AssetInput__select" 216 data-cy={'assetSelect'} 217 MenuProps={{ 218 sx: { 219 maxHeight: '240px', 220 '.MuiPaper-root': { 221 border: theme.palette.mode === 'dark' ? '1px solid #EBEBED1F' : 'unset', 222 boxShadow: '0px 2px 10px 0px #0000001A', 223 }, 224 }, 225 }} 226 sx={{ 227 p: 0, 228 '&.AssetInput__select .MuiOutlinedInput-input': { 229 p: 0, 230 backgroundColor: 'transparent', 231 pr: '24px !important', 232 }, 233 '&.AssetInput__select .MuiOutlinedInput-notchedOutline': { display: 'none' }, 234 '&.AssetInput__select .MuiSelect-icon': { 235 color: 'text.primary', 236 right: '0%', 237 }, 238 }} 239 renderValue={(symbol) => { 240 const asset = 241 assets.length === 1 242 ? assets[0] 243 : assets && (assets.find((asset) => asset.symbol === symbol) as T); 244 return ( 245 <Box 246 sx={{ display: 'flex', alignItems: 'center' }} 247 data-cy={`assetsSelectedOption_${asset.symbol.toUpperCase()}`} 248 > 249 <TokenIcon 250 symbol={asset.iconSymbol || asset.symbol} 251 aToken={asset.aToken} 252 sx={{ mr: 2, ml: 4 }} 253 /> 254 <Typography variant="main16" color="text.primary"> 255 {symbol} 256 </Typography> 257 </Box> 258 ); 259 }} 260 > 261 {selectOptionHeader ? selectOptionHeader : undefined} 262 {assets.map((asset) => ( 263 <MenuItem 264 key={asset.symbol} 265 value={asset.symbol} 266 data-cy={`assetsSelectOption_${asset.symbol.toUpperCase()}`} 267 > 268 {selectOption ? ( 269 selectOption(asset) 270 ) : ( 271 <> 272 <TokenIcon 273 aToken={asset.aToken} 274 symbol={asset.iconSymbol || asset.symbol} 275 sx={{ fontSize: '22px', mr: 1 }} 276 /> 277 <ListItemText sx={{ mr: 6 }}>{asset.symbol}</ListItemText> 278 {asset.balance && <FormattedNumber value={asset.balance} compact />} 279 </> 280 )} 281 </MenuItem> 282 ))} 283 </Select> 284 </FormControl> 285 )} 286 </Box> 287 288 <Box sx={{ display: 'flex', alignItems: 'center', height: '16px', px: 3, py: 2, mb: 1 }}> 289 {loading ? ( 290 <Box sx={{ flex: 1 }} /> 291 ) : ( 292 <FormattedNumber 293 value={isNaN(Number(usdValue)) ? 0 : Number(usdValue)} 294 compact 295 symbol="USD" 296 variant="secondary12" 297 color="text.muted" 298 symbolsColor="text.muted" 299 flexGrow={1} 300 /> 301 )} 302 303 {asset.balance && onChange && ( 304 <> 305 <Typography component="div" variant="secondary12" color="text.secondary"> 306 {balanceText && balanceText !== '' ? balanceText : <Trans>Balance</Trans>}{' '} 307 <FormattedNumber 308 value={asset.balance} 309 compact 310 variant="secondary12" 311 color="text.secondary" 312 symbolsColor="text.disabled" 313 /> 314 </Typography> 315 {!disableInput && ( 316 <Button 317 size="small" 318 sx={{ minWidth: 0, ml: '7px', p: 0 }} 319 onClick={() => { 320 if (event) { 321 trackEvent(event.eventName, { ...event.eventParams }); 322 } 323 324 onChange('-1'); 325 }} 326 disabled={disabled || isMaxSelected} 327 > 328 <Trans>Max</Trans> 329 </Button> 330 )} 331 </> 332 )} 333 </Box> 334 {exchangeRateComponent && ( 335 <Box 336 sx={{ 337 background: theme.palette.background.surface, 338 borderTop: `1px solid ${theme.palette.divider}`, 339 px: 3, 340 py: 2, 341 }} 342 > 343 {exchangeRateComponent} 344 </Box> 345 )} 346 </Box> 347 </Box> 348 ); 349 };