181 lines
5.4 KiB
TypeScript
181 lines
5.4 KiB
TypeScript
import React, { useState, useRef } from 'react';
|
||
import {
|
||
Box,
|
||
InputBase,
|
||
IconButton,
|
||
Menu,
|
||
MenuItem,
|
||
Paper,
|
||
Popper,
|
||
Grow,
|
||
ClickAwayListener,
|
||
MenuList,
|
||
useTheme,
|
||
FormHelperText
|
||
} from '@mui/material';
|
||
import ArrowDownIcon from "@/assets/icons/ArrowDownIcon";
|
||
|
||
interface AgeInputWithSelectProps {
|
||
value: string;
|
||
onChange: (value: string) => void;
|
||
onErrorChange?: (isError: boolean) => void;
|
||
}
|
||
|
||
const AgeInputWithSelect = ({ value, onChange, onErrorChange }: AgeInputWithSelectProps) => {
|
||
const theme = useTheme();
|
||
const [open, setOpen] = useState(false);
|
||
const anchorRef = useRef<HTMLDivElement>(null);
|
||
const [errorType, setErrorType] = useState<'format' | 'range' | false>(false);
|
||
|
||
// Валидация: только число или диапазон число-число, и диапазон 0-150
|
||
const validate = (val: string) => {
|
||
if (!val) return false;
|
||
// Число (только положительное)
|
||
if (/^-?\d+$/.test(val)) {
|
||
const num = Number(val);
|
||
if (num < 0 || num > 150) return 'range';
|
||
return false;
|
||
}
|
||
// Диапазон (только положительные числа)
|
||
const rangeMatch = val.match(/^(-?\d+)-(-?\d+)$/);
|
||
if (rangeMatch) {
|
||
const left = Number(rangeMatch[1]);
|
||
const right = Number(rangeMatch[2]);
|
||
if (left < 0 || left > 150 || right < 0 || right > 150 || left > right) return 'range';
|
||
return false;
|
||
}
|
||
return 'format';
|
||
};
|
||
|
||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const filtered = e.target.value.replace(/[^\d-]/g, '');
|
||
onChange(filtered);
|
||
const err = validate(filtered);
|
||
setErrorType(err);
|
||
if (onErrorChange) onErrorChange(!!err);
|
||
};
|
||
|
||
const handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||
const trimmed = e.target.value.replace(/\s+/g, '');
|
||
onChange(trimmed);
|
||
const err = validate(trimmed);
|
||
setErrorType(err);
|
||
if (onErrorChange) onErrorChange(!!err);
|
||
};
|
||
|
||
const handleSelectItemClick = (selectedValue: string) => {
|
||
onChange(selectedValue);
|
||
setErrorType(false);
|
||
setOpen(false);
|
||
};
|
||
|
||
const handleToggle = () => {
|
||
setOpen((prevOpen) => !prevOpen);
|
||
};
|
||
|
||
const handleClose = (event: Event) => {
|
||
if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement)) {
|
||
return;
|
||
}
|
||
setOpen(false);
|
||
};
|
||
|
||
return (
|
||
<Box
|
||
ref={anchorRef}
|
||
sx={{
|
||
position: 'relative',
|
||
mt: "17px",
|
||
height: "48px",
|
||
maxWidth: "420px",
|
||
width: "100%",
|
||
borderRadius: "8px",
|
||
border: "1px solid #9A9AAF",
|
||
'&:hover': {
|
||
borderColor: '#B0B0B0',
|
||
},
|
||
'&:focus-within': {
|
||
borderColor: '#7E2AEA',
|
||
}
|
||
}}
|
||
>
|
||
<InputBase
|
||
value={value}
|
||
onChange={handleInputChange}
|
||
onBlur={handleInputBlur}
|
||
fullWidth
|
||
placeholder="Введите возраст"
|
||
inputProps={{ inputMode: 'numeric', pattern: '[0-9-]*' }}
|
||
sx={{
|
||
height: "100%",
|
||
padding: "10px 20px",
|
||
'& input': {
|
||
height: "100%",
|
||
width: "100%",
|
||
}
|
||
}}
|
||
/>
|
||
{errorType === 'format' && (
|
||
<FormHelperText error sx={{ position: 'absolute', left: 0, top: '100%', mt: '2px', ml: '10px' }}>
|
||
можно только число или диапазон
|
||
</FormHelperText>
|
||
)}
|
||
{errorType === 'range' && (
|
||
<FormHelperText error sx={{ position: 'absolute', left: 0, top: '100%', mt: '2px', ml: '10px' }}>
|
||
таких возрастов нет
|
||
</FormHelperText>
|
||
)}
|
||
<IconButton
|
||
onClick={handleToggle}
|
||
sx={{
|
||
position: 'absolute',
|
||
right: 0,
|
||
top: '50%',
|
||
transform: `translateY(-50%) rotate(${open ? 180 : 0}deg)`,
|
||
cursor: 'pointer',
|
||
color: theme.palette.brightPurple.main,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
transition: 'transform 0.2s',
|
||
padding: '8px'
|
||
}}
|
||
>
|
||
<ArrowDownIcon style={{ width: "18px", height: "18px" }} />
|
||
</IconButton>
|
||
|
||
<Popper
|
||
open={open}
|
||
anchorEl={anchorRef.current}
|
||
role={undefined}
|
||
placement="bottom-end"
|
||
transition
|
||
disablePortal
|
||
sx={{ zIndex: 1300 }}
|
||
>
|
||
{({ TransitionProps, placement }) => (
|
||
<Grow
|
||
{...TransitionProps}
|
||
style={{
|
||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||
}}
|
||
>
|
||
<Paper elevation={8}>
|
||
<ClickAwayListener onClickAway={handleClose}>
|
||
<MenuList autoFocusItem={open} id="menu-list-grow">
|
||
<MenuItem onClick={() => handleSelectItemClick('')}>Выберите возраст</MenuItem>
|
||
<MenuItem onClick={() => handleSelectItemClick('18-24')}>18-24</MenuItem>
|
||
<MenuItem onClick={() => handleSelectItemClick('25-34')}>25-34</MenuItem>
|
||
<MenuItem onClick={() => handleSelectItemClick('35-44')}>35-44</MenuItem>
|
||
<MenuItem onClick={() => handleSelectItemClick('45-54')}>45-54</MenuItem>
|
||
<MenuItem onClick={() => handleSelectItemClick('55+')}>55+</MenuItem>
|
||
</MenuList>
|
||
</ClickAwayListener>
|
||
</Paper>
|
||
</Grow>
|
||
)}
|
||
</Popper>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
export default AgeInputWithSelect;
|