goldenChat base source add
This commit is contained in:
@@ -0,0 +1,536 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Autocomplete, TextField, CircularProgress, Box, SxProps, Theme, IconButton, Typography, Divider } from '@mui/material';
|
||||
import { Star, StarBorder } from '@mui/icons-material';
|
||||
import upbitApiService, { UpbitMarket, UpbitTicker } from '../services/upbitApi';
|
||||
import { toggleFavorite, getAllFavorites } from '../services/cryptoFavoriteApi';
|
||||
|
||||
interface CryptoNameComboBoxProps {
|
||||
value: string;
|
||||
onChange: (symbol: string, cryptoInfo?: UpbitMarket) => void;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
helperText?: string;
|
||||
size?: 'small' | 'medium';
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
type SortKey = 'favorite' | 'korean_name' | 'trade_price' | 'signed_change_rate' | 'acc_trade_price_24h';
|
||||
type SortOrder = 'asc' | 'desc';
|
||||
|
||||
const HEADER_ROW_HEIGHT = 30;
|
||||
|
||||
const formatPrice = (price?: number): string => {
|
||||
if (price == null) return '-';
|
||||
if (price >= 1000) return price.toLocaleString();
|
||||
if (price >= 1) return price.toLocaleString(undefined, { maximumFractionDigits: 4 });
|
||||
return price.toLocaleString(undefined, { maximumFractionDigits: 8 });
|
||||
};
|
||||
|
||||
const formatChangeRate = (rate?: number): string => {
|
||||
if (rate == null) return '-';
|
||||
const percent = rate * 100;
|
||||
const sign = percent > 0 ? '+' : '';
|
||||
return `${sign}${percent.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
const formatTradeValue = (value?: number): string => {
|
||||
if (value == null) return '-';
|
||||
if (value >= 1_0000_0000_0000) return `${(value / 1_0000_0000_0000).toFixed(2)}조`;
|
||||
if (value >= 1_0000_0000) return `${(value / 1_0000_0000).toFixed(1)}억`;
|
||||
if (value >= 1_0000) return `${(value / 1_0000).toFixed(1)}만`;
|
||||
return value.toLocaleString();
|
||||
};
|
||||
|
||||
const compareNumber = (a?: number, b?: number): number => {
|
||||
const av = a ?? Number.NEGATIVE_INFINITY;
|
||||
const bv = b ?? Number.NEGATIVE_INFINITY;
|
||||
return av - bv;
|
||||
};
|
||||
|
||||
const compareString = (a?: string, b?: string): number => (a ?? '').localeCompare(b ?? '', 'ko');
|
||||
|
||||
|
||||
const CryptoNameComboBox: React.FC<CryptoNameComboBoxProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
label = '암호화폐명',
|
||||
placeholder = '암호화폐명을 입력하거나 선택하세요',
|
||||
helperText,
|
||||
size = 'medium',
|
||||
sx,
|
||||
}) => {
|
||||
const [cryptos, setCryptos] = useState<UpbitMarket[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [selectedCrypto, setSelectedCrypto] = useState<UpbitMarket | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isUserTyping, setIsUserTyping] = useState(false);
|
||||
const [favorites, setFavorites] = useState<Record<string, boolean>>({});
|
||||
const [tickerMap, setTickerMap] = useState<Record<string, UpbitTicker>>({});
|
||||
const [sortKey, setSortKey] = useState<SortKey>('favorite');
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
|
||||
const [dropdownWidth, setDropdownWidth] = useState<number>(520);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const comboRootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchTickers = useCallback(async (markets: string[]) => {
|
||||
if (markets.length === 0) return;
|
||||
const chunkSize = 80;
|
||||
const chunks: string[][] = [];
|
||||
for (let i = 0; i < markets.length; i += chunkSize) {
|
||||
chunks.push(markets.slice(i, i + chunkSize));
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await Promise.all(chunks.map((chunk) => upbitApiService.getCurrentPrice(chunk)));
|
||||
const next: Record<string, UpbitTicker> = {};
|
||||
results.flat().forEach((ticker) => {
|
||||
if (ticker?.market) next[ticker.market] = ticker;
|
||||
});
|
||||
setTickerMap(next);
|
||||
} catch (error) {
|
||||
console.error('티커 로드 실패:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 컴포넌트 마운트 시 암호화폐 목록 로드
|
||||
useEffect(() => {
|
||||
const loadAllCryptos = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const activeCryptos = await upbitApiService.getActiveCryptos();
|
||||
const convertedCryptos = activeCryptos.map(crypto => ({
|
||||
market: crypto.symbol,
|
||||
korean_name: crypto.koreanName,
|
||||
english_name: crypto.englishName
|
||||
}));
|
||||
setCryptos(convertedCryptos);
|
||||
await fetchTickers(convertedCryptos.map((c) => c.market));
|
||||
|
||||
// 즐겨찾기 목록 한 번에 로드
|
||||
try {
|
||||
const favList = await getAllFavorites();
|
||||
const favMap: Record<string, boolean> = {};
|
||||
favList.forEach(fav => {
|
||||
favMap[fav.symbol] = true;
|
||||
});
|
||||
setFavorites(favMap);
|
||||
} catch (error) {
|
||||
console.error('즐겨찾기 목록 로드 실패:', error);
|
||||
setFavorites({});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('암호화폐 목록 로드 실패:', error);
|
||||
const defaultCryptos = [
|
||||
{ market: 'KRW-BTC', korean_name: '비트코인', english_name: 'Bitcoin' },
|
||||
{ market: 'KRW-ETH', korean_name: '이더리움', english_name: 'Ethereum' },
|
||||
{ market: 'KRW-XRP', korean_name: '리플', english_name: 'Ripple' },
|
||||
{ market: 'KRW-ADA', korean_name: '에이다', english_name: 'Cardano' },
|
||||
{ market: 'KRW-SOL', korean_name: '솔라나', english_name: 'Solana' },
|
||||
];
|
||||
setCryptos(defaultCryptos);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadAllCryptos();
|
||||
}, [fetchTickers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || cryptos.length === 0) return;
|
||||
void fetchTickers(cryptos.map((c) => c.market));
|
||||
}, [isOpen, cryptos, fetchTickers]);
|
||||
|
||||
// 부모에서 종목이 바뀌면(알림·FAB 등) 검색 중 플래그 해제 → 아래 동기화 effect가 한글명 반영
|
||||
useEffect(() => {
|
||||
setIsUserTyping(false);
|
||||
}, [value]);
|
||||
|
||||
// 외부 value 변경 시 선택 상태 동기화
|
||||
useEffect(() => {
|
||||
// 사용자가 타이핑 중이거나 드롭다운이 열려있을 때는 동기화하지 않음
|
||||
if (isUserTyping || isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value && cryptos.length > 0) {
|
||||
const crypto = cryptos.find(c => c.market === value);
|
||||
if (crypto && crypto !== selectedCrypto) {
|
||||
setSelectedCrypto(crypto);
|
||||
setInputValue(crypto.korean_name);
|
||||
}
|
||||
} else if (!value) {
|
||||
setSelectedCrypto(null);
|
||||
setInputValue('');
|
||||
}
|
||||
}, [value, cryptos, selectedCrypto, isUserTyping, isOpen]);
|
||||
|
||||
// 필터링된 종목 목록
|
||||
// - 드롭다운이 열릴 때 (isOpen=true) 사용자가 타이핑 중일 때만 필터링
|
||||
// - 목록을 펼칠 때는 모든 종목 표시
|
||||
// - 즐겨찾기를 먼저 표시하도록 정렬
|
||||
const getFilteredOptions = useCallback(() => {
|
||||
let filteredList: UpbitMarket[];
|
||||
|
||||
// 사용자가 타이핑 중일 때만 필터링
|
||||
if (isUserTyping && inputValue) {
|
||||
const lowerCaseInput = inputValue.toLowerCase();
|
||||
filteredList = cryptos.filter(crypto => {
|
||||
if (!crypto || !crypto.korean_name || !crypto.english_name || !crypto.market) return false;
|
||||
return crypto.korean_name.toLowerCase().includes(lowerCaseInput) ||
|
||||
crypto.english_name.toLowerCase().includes(lowerCaseInput) ||
|
||||
crypto.market.toLowerCase().includes(lowerCaseInput);
|
||||
});
|
||||
} else {
|
||||
// 목록을 펼칠 때는 모든 종목 표시
|
||||
filteredList = cryptos;
|
||||
}
|
||||
|
||||
return [...filteredList].sort((a, b) => {
|
||||
const tA = tickerMap[a.market];
|
||||
const tB = tickerMap[b.market];
|
||||
let cmp = 0;
|
||||
|
||||
if (sortKey === 'favorite') {
|
||||
cmp = Number(!!favorites[a.market]) - Number(!!favorites[b.market]);
|
||||
} else if (sortKey === 'korean_name') {
|
||||
cmp = compareString(a.korean_name, b.korean_name);
|
||||
} else if (sortKey === 'trade_price') {
|
||||
cmp = compareNumber(tA?.trade_price, tB?.trade_price);
|
||||
} else if (sortKey === 'signed_change_rate') {
|
||||
cmp = compareNumber(tA?.signed_change_rate, tB?.signed_change_rate);
|
||||
} else if (sortKey === 'acc_trade_price_24h') {
|
||||
cmp = compareNumber(tA?.acc_trade_price_24h, tB?.acc_trade_price_24h);
|
||||
}
|
||||
|
||||
if (cmp === 0) {
|
||||
cmp = compareString(a.korean_name, b.korean_name);
|
||||
}
|
||||
return sortOrder === 'asc' ? cmp : -cmp;
|
||||
});
|
||||
}, [cryptos, inputValue, isUserTyping, favorites, tickerMap, sortKey, sortOrder]);
|
||||
|
||||
const handleSort = useCallback((key: SortKey) => {
|
||||
if (sortKey === key) {
|
||||
setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc'));
|
||||
return;
|
||||
}
|
||||
setSortKey(key);
|
||||
setSortOrder(key === 'korean_name' ? 'asc' : 'desc');
|
||||
}, [sortKey]);
|
||||
|
||||
// 드롭다운 열림/닫힘 핸들러
|
||||
const handleOpen = () => {
|
||||
const inputWidth = comboRootRef.current?.clientWidth ?? 0;
|
||||
// 입력란 좌측 기준으로 붙이고, 너무 좁지 않게 최소 폭 보장
|
||||
setDropdownWidth(Math.max(520, inputWidth));
|
||||
setIsOpen(true);
|
||||
setIsUserTyping(false); // 목록을 펼칠 때는 필터링 비활성화
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
setIsUserTyping(false);
|
||||
// 닫힐 때 입력값이 비어있거나 매칭되는 종목이 없으면 선택된 종목으로 복원
|
||||
if (selectedCrypto && !inputValue) {
|
||||
setInputValue(selectedCrypto.korean_name);
|
||||
} else if (!selectedCrypto && !inputValue) {
|
||||
// 선택된 종목도 없고 입력값도 없으면 그대로 유지
|
||||
setInputValue('');
|
||||
}
|
||||
};
|
||||
|
||||
// 종목 선택 핸들러 — 목록에서 고를 때만 부모 onChange(차트 갱신). Clear는 검색칸만 비움.
|
||||
const handleChange = (event: React.SyntheticEvent, newValue: UpbitMarket | null) => {
|
||||
if (newValue) {
|
||||
setSelectedCrypto(newValue);
|
||||
setIsUserTyping(false);
|
||||
setInputValue(newValue.korean_name);
|
||||
onChange(newValue.market, newValue);
|
||||
return;
|
||||
}
|
||||
// Clear(X): 실제 선택·부모 symbol·차트는 유지, 입력만 비워 다음 종목 검색 가능
|
||||
const keep =
|
||||
(value && cryptos.length > 0 ? cryptos.find((c) => c.market === value) : null) ?? selectedCrypto;
|
||||
if (keep) {
|
||||
setSelectedCrypto(keep);
|
||||
setInputValue('');
|
||||
setIsUserTyping(true);
|
||||
} else {
|
||||
setSelectedCrypto(null);
|
||||
setInputValue('');
|
||||
setIsUserTyping(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 입력 변경 핸들러
|
||||
const handleInputChange = (event: React.SyntheticEvent, newInputValue: string, reason: string) => {
|
||||
setInputValue(newInputValue);
|
||||
|
||||
// 사용자가 키보드로 직접 입력하거나 clear할 때 필터링 활성화
|
||||
if (reason === 'input' || reason === 'clear') {
|
||||
setIsUserTyping(true);
|
||||
} else if (reason === 'reset') {
|
||||
// 선택이 완료되었을 때만 타이핑 상태 해제
|
||||
setIsUserTyping(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 키 입력 핸들러 - 엔터키 처리
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter' && isUserTyping) {
|
||||
const filteredOptions = getFilteredOptions();
|
||||
// 필터링된 결과가 하나이면 해당 종목 선택
|
||||
if (filteredOptions.length === 1) {
|
||||
const crypto = filteredOptions[0];
|
||||
setSelectedCrypto(crypto);
|
||||
setInputValue(crypto.korean_name);
|
||||
setIsUserTyping(false);
|
||||
setIsOpen(false);
|
||||
onChange(crypto.market, crypto);
|
||||
} else if (filteredOptions.length > 1) {
|
||||
// 필터링된 결과 중 정확히 일치하는 종목이 있으면 선택
|
||||
const exactMatch = filteredOptions.find(c =>
|
||||
c.korean_name.toLowerCase() === inputValue.toLowerCase() ||
|
||||
c.english_name.toLowerCase() === inputValue.toLowerCase() ||
|
||||
c.market.toLowerCase() === inputValue.toLowerCase()
|
||||
);
|
||||
if (exactMatch) {
|
||||
setSelectedCrypto(exactMatch);
|
||||
setInputValue(exactMatch.korean_name);
|
||||
setIsUserTyping(false);
|
||||
setIsOpen(false);
|
||||
onChange(exactMatch.market, exactMatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 즐겨찾기 토글 핸들러
|
||||
const handleToggleFavorite = async (e: React.MouseEvent, option: UpbitMarket) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
try {
|
||||
const result = await toggleFavorite({
|
||||
symbol: option.market,
|
||||
koreanName: option.korean_name,
|
||||
englishName: option.english_name,
|
||||
});
|
||||
setFavorites(prev => ({ ...prev, [option.market]: result }));
|
||||
} catch (error) {
|
||||
console.error('즐겨찾기 토글 실패:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box ref={comboRootRef}>
|
||||
<Autocomplete
|
||||
options={getFilteredOptions()}
|
||||
getOptionLabel={(option) => option.korean_name}
|
||||
isOptionEqualToValue={(option, val) => option.market === val.market}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
value={selectedCrypto}
|
||||
open={isOpen}
|
||||
onOpen={handleOpen}
|
||||
onClose={handleClose}
|
||||
onChange={handleChange}
|
||||
inputValue={inputValue}
|
||||
onInputChange={handleInputChange}
|
||||
filterOptions={(options) => options} // 커스텀 필터링 사용
|
||||
size={size}
|
||||
freeSolo={false} // 목록에서만 선택 가능
|
||||
clearOnBlur={false} // 포커스를 잃어도 입력값 유지
|
||||
selectOnFocus={false} // 포커스 시 자동 선택 안함
|
||||
handleHomeEndKeys={false}
|
||||
disableClearable={false} // X 버튼 표시
|
||||
sx={sx}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
inputRef={inputRef}
|
||||
label={label || undefined}
|
||||
placeholder={placeholder}
|
||||
helperText={helperText || undefined}
|
||||
fullWidth
|
||||
size={size}
|
||||
onKeyDown={handleKeyDown}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<React.Fragment>
|
||||
{loading ? <CircularProgress color="inherit" size={20} /> : null}
|
||||
{params.InputProps.endAdornment}
|
||||
</React.Fragment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
renderOption={(props, option, state) => {
|
||||
const isFav = favorites[option.market] || false;
|
||||
const ticker = tickerMap[option.market];
|
||||
const options = getFilteredOptions();
|
||||
const currentIndex = state.index;
|
||||
const isFirstRow = currentIndex === 0;
|
||||
|
||||
// 현재 항목이 즐겨찾기이고, 다음 항목이 일반 항목인지 확인
|
||||
const nextOption = options[currentIndex + 1];
|
||||
const isLastFavorite = isFav && nextOption && !favorites[nextOption.market];
|
||||
|
||||
return (
|
||||
<React.Fragment key={option.market}>
|
||||
{isFirstRow && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '28px minmax(120px, 1.2fr) minmax(92px, 0.9fr) minmax(82px, 0.8fr) minmax(82px, 0.8fr)',
|
||||
alignItems: 'center',
|
||||
px: 1.5,
|
||||
height: HEADER_ROW_HEIGHT,
|
||||
bgcolor: 'background.paper',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none' }}
|
||||
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleSort('favorite'); }}
|
||||
>
|
||||
즐겨
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ cursor: 'pointer', userSelect: 'none' }}
|
||||
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleSort('korean_name'); }}
|
||||
>
|
||||
한글명 {sortKey === 'korean_name' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ textAlign: 'right', cursor: 'pointer', userSelect: 'none' }}
|
||||
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleSort('trade_price'); }}
|
||||
>
|
||||
현재가 {sortKey === 'trade_price' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ textAlign: 'right', cursor: 'pointer', userSelect: 'none' }}
|
||||
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleSort('signed_change_rate'); }}
|
||||
>
|
||||
전일대비 {sortKey === 'signed_change_rate' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ textAlign: 'right', cursor: 'pointer', userSelect: 'none' }}
|
||||
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleSort('acc_trade_price_24h'); }}
|
||||
>
|
||||
거래대금 {sortKey === 'acc_trade_price_24h' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
component="li"
|
||||
{...props}
|
||||
sx={{
|
||||
display: 'grid !important',
|
||||
gridTemplateColumns: '28px minmax(120px, 1.2fr) minmax(92px, 0.9fr) minmax(82px, 0.8fr) minmax(82px, 0.8fr) !important',
|
||||
alignItems: 'center !important',
|
||||
gap: 1,
|
||||
padding: '8px 12px !important',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => handleToggleFavorite(e, option)}
|
||||
sx={{
|
||||
color: isFav ? '#fbbf24' : 'action.disabled',
|
||||
padding: '2px',
|
||||
}}
|
||||
>
|
||||
{isFav ? <Star fontSize="small" /> : <StarBorder fontSize="small" />}
|
||||
</IconButton>
|
||||
|
||||
{/* 한글명 */}
|
||||
<Box sx={{ minWidth: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography sx={{ fontWeight: 'bold', fontSize: '14px' }}>
|
||||
{option.korean_name}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '12px', color: 'text.secondary' }}>
|
||||
{option.market.replace('KRW-', '')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* 현재가 */}
|
||||
<Typography sx={{ textAlign: 'right', fontWeight: 600, fontSize: '13px' }}>
|
||||
{formatPrice(ticker?.trade_price)}
|
||||
</Typography>
|
||||
|
||||
{/* 전일대비 */}
|
||||
<Typography
|
||||
sx={{
|
||||
textAlign: 'right',
|
||||
fontWeight: 600,
|
||||
fontSize: '13px',
|
||||
color:
|
||||
(ticker?.signed_change_rate ?? 0) > 0
|
||||
? 'error.main'
|
||||
: (ticker?.signed_change_rate ?? 0) < 0
|
||||
? 'info.main'
|
||||
: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
{formatChangeRate(ticker?.signed_change_rate)}
|
||||
</Typography>
|
||||
|
||||
{/* 거래대금 */}
|
||||
<Typography sx={{ textAlign: 'right', fontSize: '12px', color: 'text.secondary' }}>
|
||||
{formatTradeValue(ticker?.acc_trade_price_24h)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* 즐겨찾기와 일반 종목 사이 divider */}
|
||||
{isLastFavorite && (
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}}
|
||||
ListboxProps={{
|
||||
sx: {
|
||||
width: '100%',
|
||||
maxHeight: '70vh',
|
||||
pt: 0,
|
||||
},
|
||||
}}
|
||||
componentsProps={{
|
||||
popper: {
|
||||
style: {
|
||||
width: `${dropdownWidth}px`,
|
||||
},
|
||||
placement: 'bottom-start',
|
||||
},
|
||||
paper: {
|
||||
sx: {
|
||||
maxHeight: '72vh',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default CryptoNameComboBox;
|
||||
Reference in New Issue
Block a user