замена инпут на текстареа

This commit is contained in:
Nastya 2025-10-05 15:41:26 +03:00
parent 426449d9bf
commit 70cc2e4e5b
8 changed files with 299 additions and 219 deletions

@ -1,3 +1,4 @@
1.0.7 _ 2025-10-05 _ замена инпут на текстареа
1.0.6 _ 2025-09-19 _ логика включения таймера 1.0.6 _ 2025-09-19 _ логика включения таймера
1.0.5 _ 2025-09-18 _ особые условия для вывода картинок 1.0.5 _ 2025-09-18 _ особые условия для вывода картинок
1.0.4 _ 2025-09-14 _ особые условия для вывода картинок 1.0.4 _ 2025-09-14 _ особые условия для вывода картинок

@ -39,22 +39,31 @@ const AnswerItem = memo<AnswerItemProps>(
const isTablet = useMediaQuery(theme.breakpoints.down(790)); const isTablet = useMediaQuery(theme.breakpoints.down(790));
const setOwnPlaceholder = (replText: string) => { const setOwnPlaceholder = (replText: string) => {
updateQuestion(questionId, (question) => { updateQuestion<QuizQuestionVariant>(questionId, (question) => {
question.content.ownPlaceholder = replText; question.content.ownPlaceholder = replText;
}); });
}; };
const inputRefCallback = useCallback((element: HTMLInputElement | null) => { const inputRefCallback = useCallback((element: HTMLInputElement | HTMLTextAreaElement | null) => {
if (element && shouldAutoFocus) { if (element && shouldAutoFocus) {
element.focus(); element.focus();
onFocusHandled?.(); onFocusHandled?.();
} }
}, [shouldAutoFocus, onFocusHandled]); }, [shouldAutoFocus, onFocusHandled]);
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (disableKeyDown) { // Shift+Enter — новая строка, ничего не делаем (даём браузеру вставить перенос)
enqueueSnackbar("100 максимальное количество"); if (event.key === "Enter" && event.shiftKey) {
} else if (event.code === "Enter" && !largeCheck) { return;
}
// Enter — добавить новый вариант
if (event.key === "Enter") {
event.preventDefault();
if (disableKeyDown) {
enqueueSnackbar("100 максимальное количество");
return;
}
if (onEnterKeyPress) { if (onEnterKeyPress) {
onEnterKeyPress(); onEnterKeyPress();
} else { } else {
@ -92,8 +101,9 @@ const AnswerItem = memo<AnswerItemProps>(
fullWidth fullWidth
focused={false} focused={false}
placeholder={isOwn ? "Добавьте текст-подсказку для ввода \"своего ответа\"" : "Добавьте ответ"} placeholder={isOwn ? "Добавьте текст-подсказку для ввода \"своего ответа\"" : "Добавьте ответ"}
multiline={largeCheck} multiline
onChange={({ target }: ChangeEvent<HTMLInputElement>) => { rows={1}
onChange={({ target }: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (target.value.length <= 1000) { if (target.value.length <= 1000) {
isOwn ? isOwn ?
setOwnPlaceholder(target.value || " ") setOwnPlaceholder(target.value || " ")
@ -148,6 +158,9 @@ const AnswerItem = memo<AnswerItemProps>(
}, },
"& textarea.MuiInputBase-input": { "& textarea.MuiInputBase-input": {
marginTop: "1px", marginTop: "1px",
resize: "none",
// удерживаем стартовую высоту визуально как у однострочного
lineHeight: "21px",
}, },
"& .MuiOutlinedInput-notchedOutline": { "& .MuiOutlinedInput-notchedOutline": {
border: "none", border: "none",

@ -51,7 +51,7 @@ export default function DropDown({ question, openBranchingPage, setOpenBranching
questionId={question.id} questionId={question.id}
variant={variant} variant={variant}
isOwn={Boolean(variant?.isOwn)} isOwn={Boolean(variant?.isOwn)}
ownPlaceholder={question.content.ownPlaceholder || ""} ownPlaceholder={""}
shouldAutoFocus={focusedVariantId === variant.id} shouldAutoFocus={focusedVariantId === variant.id}
onFocusHandled={clearFocusedVariant} onFocusHandled={clearFocusedVariant}
onEnterKeyPress={() => addVariantOnEnter(question.id)} onEnterKeyPress={() => addVariantOnEnter(question.id)}
@ -59,47 +59,56 @@ export default function DropDown({ question, openBranchingPage, setOpenBranching
))} ))}
/> />
)} )}
<Box <Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-start", marginBottom: "20px" }}>
sx={{ <Box sx={{ display: "flex", alignItems: "center" }}>
display: "flex", <Link
alignItems: "center", component="button"
marginBottom: "20px", variant="body2"
}} sx={{
> color: theme.palette.brightPurple.main,
<Link fontWeight: "400",
component="button" fontSize: "16px",
variant="body2" mr: "4px",
sx={{ height: "19px",
color: theme.palette.brightPurple.main, }}
fontWeight: "400", onClick={() => addVariantWithFocus(question)}
fontSize: "16px", >
mr: "4px", Добавьте ответ
height: "19px", </Link>
}} {isMobile ? null : (
onClick={() => addVariantWithFocus(question)} <>
> <Typography
Добавьте ответ sx={{
</Link> fontWeight: 400,
lineHeight: "21.33px",
color: theme.palette.grey2.main,
fontSize: "16px",
}}
>
или нажмите Enter
</Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)}
</Box>
{isMobile ? null : ( {isMobile ? null : (
<> <Typography
<Typography sx={{
sx={{ fontWeight: 400,
fontWeight: 400, lineHeight: "21.33px",
lineHeight: "21.33px", color: theme.palette.grey2.main,
color: theme.palette.grey2.main, fontSize: "16px",
fontSize: "16px", mt: "4px",
}} }}
> >
или нажмите Enter нажмите shift + enter для переноса строки
</Typography> </Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)} )}
</Box> </Box>
</Box> </Box>
@ -110,6 +119,7 @@ export default function DropDown({ question, openBranchingPage, setOpenBranching
questionContentId={question.content.id} questionContentId={question.content.id}
questionType={question.type} questionType={question.type}
questionHasParent={question.content.rule.parentId?.length !== 0} questionHasParent={question.content.rule.parentId?.length !== 0}
openBranchingPage={openBranchingPage}
setOpenBranchingPage={setOpenBranchingPage} setOpenBranchingPage={setOpenBranchingPage}
/> />
<SwitchDropDown <SwitchDropDown

@ -47,7 +47,7 @@ export default function Emoji({ question, openBranchingPage, setOpenBranchingPag
setOpen={setOpen} setOpen={setOpen}
setSelectedVariant={setSelectedVariant} setSelectedVariant={setSelectedVariant}
isOwn={Boolean(variant?.isOwn)} isOwn={Boolean(variant?.isOwn)}
ownPlaceholder={question.content.ownPlaceholder} ownPlaceholder={question.content.ownPlaceholder || ""}
shouldAutoFocus={focusedVariantId === variant.id} shouldAutoFocus={focusedVariantId === variant.id}
onFocusHandled={clearFocusedVariant} onFocusHandled={clearFocusedVariant}
onEnterKeyPress={() => addVariantOnEnter(question.id)} onEnterKeyPress={() => addVariantOnEnter(question.id)}
@ -84,42 +84,50 @@ export default function Emoji({ question, openBranchingPage, setOpenBranchingPag
}} }}
/> />
</Popover> </Popover>
<Box <Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-start", marginBottom: isMobile ? "17px" : "20px" }}>
sx={{ <Box sx={{ display: "flex", alignItems: "center" }}>
display: "flex", <Link
alignItems: "center", component="button"
gap: "10px", variant="body2"
marginBottom: isMobile ? "17px" : "20px", sx={{ color: theme.palette.brightPurple.main }}
}} onClick={() => addVariantWithFocus(question)}
> >
<Link Добавьте ответ
component="button" </Link>
variant="body2" {!isTablet && (
sx={{ color: theme.palette.brightPurple.main }} <>
onClick={() => addVariantWithFocus(question)} <Typography
> sx={{
Добавьте ответ fontWeight: 400,
</Link> lineHeight: "21.33px",
color: theme.palette.grey2.main,
fontSize: "16px",
}}
>
или нажмите Enter
</Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)}
</Box>
{!isTablet && ( {!isTablet && (
<> <Typography
<Typography sx={{
sx={{ fontWeight: 400,
fontWeight: 400, lineHeight: "21.33px",
lineHeight: "21.33px", color: theme.palette.grey2.main,
color: theme.palette.grey2.main, fontSize: "16px",
fontSize: "16px", mt: "4px",
}} }}
> >
или нажмите Enter для переноса строки нажмите shift + enter
</Typography> </Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)} )}
</Box> </Box>
</Box> </Box>
@ -130,6 +138,7 @@ export default function Emoji({ question, openBranchingPage, setOpenBranchingPag
questionContentId={question.content.id} questionContentId={question.content.id}
questionType={question.type} questionType={question.type}
questionHasParent={question.content.rule.parentId?.length !== 0} questionHasParent={question.content.rule.parentId?.length !== 0}
openBranchingPage={openBranchingPage}
setOpenBranchingPage={setOpenBranchingPage} setOpenBranchingPage={setOpenBranchingPage}
/> />
<SwitchEmoji <SwitchEmoji

@ -134,49 +134,58 @@ export default function OptionsAndPicture({
selfClose={() => setOpenCropModal(false)} selfClose={() => setOpenCropModal(false)}
setPictureUploading={setPictureUploading} setPictureUploading={setPictureUploading}
/> />
<Box <Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-start", marginBottom: "17px" }}>
sx={{ <Box sx={{ display: "flex", alignItems: "center" }}>
display: "flex", <Link
alignItems: "center", component="button"
marginBottom: "17px", variant="body2"
}} sx={{
> color: theme.palette.brightPurple.main,
<Link fontWeight: "400",
component="button" fontSize: "16px",
variant="body2" mr: "4px",
sx={{ height: "19px",
color: theme.palette.brightPurple.main, }}
fontWeight: "400", onClick={() => {
fontSize: "16px", addVariantWithFocus(question);
mr: "4px", }}
height: "19px", >
}} Добавьте ответ
onClick={() => { </Link>
addVariantWithFocus(question); {isMobile ? null : (
}} <>
> <Typography
Добавьте ответ sx={{
</Link> fontWeight: 400,
lineHeight: "21.33px",
color: theme.palette.grey2.main,
fontSize: "16px",
}}
>
или нажмите Enter
</Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)}
</Box>
{isMobile ? null : ( {isMobile ? null : (
<> <Typography
<Typography sx={{
sx={{ fontWeight: 400,
fontWeight: 400, lineHeight: "21.33px",
lineHeight: "21.33px", color: theme.palette.grey2.main,
color: theme.palette.grey2.main, fontSize: "16px",
fontSize: "16px", mt: "4px",
}} }}
> >
или нажмите Enter для переноса строки нажмите shift + enter
</Typography> </Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)} )}
</Box> </Box>
</Box> </Box>

@ -87,12 +87,12 @@ export default function OptionsPicture({
largeCheck={question.content.largeCheck} largeCheck={question.content.largeCheck}
variant={variant} variant={variant}
isMobile={isMobile} isMobile={isMobile}
openCropModal={() => {setOpenCropModal(true)}} openCropModal={() => { setOpenCropModal(true); return Promise.resolve(); }}
openImageUploadModal={openImageUploadModal} openImageUploadModal={openImageUploadModal}
pictureUploding={pictureUploding} pictureUploding={pictureUploding}
setSelectedVariantId={setSelectedVariantId} setSelectedVariantId={setSelectedVariantId}
isOwn={Boolean(variant?.isOwn)} isOwn={Boolean(variant?.isOwn)}
ownPlaceholder={question.content.ownPlaceholder} ownPlaceholder={question.content.ownPlaceholder || ""}
shouldAutoFocus={focusedVariantId === variant.id} shouldAutoFocus={focusedVariantId === variant.id}
onFocusHandled={clearFocusedVariant} onFocusHandled={clearFocusedVariant}
onEnterKeyPress={() => addVariantOnEnter(question.id)} onEnterKeyPress={() => addVariantOnEnter(question.id)}
@ -105,45 +105,60 @@ export default function OptionsPicture({
handleImageChange={handleImageUpload} handleImageChange={handleImageUpload}
/> />
<CropModalInit <CropModalInit
originalImageUrl={variant?.originalImageUrl} originalImageUrl={variant?.originalImageUrl ?? ""}
editedUrlImagesList={variant?.editedUrlImagesList} editedUrlImagesList={(variant?.editedUrlImagesList as any) ?? undefined}
questionId={question.id.toString()} questionId={question.id.toString()}
questionType={question.type} questionType={question.type as any}
quizId={quizQid} quizId={quizQid ?? ""}
variantId={variant?.id} variantId={variant?.id ?? ""}
open={openCropModal} open={openCropModal}
selfClose={() => setOpenCropModal(false)} selfClose={() => setOpenCropModal(false)}
setPictureUploading={setPictureUploading} setPictureUploading={setPictureUploading as any}
/> />
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}> <Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-start" }}>
<Link <Box sx={{ display: "flex", alignItems: "center" }}>
component="button" <Link
variant="body2" component="button"
sx={{ color: theme.palette.brightPurple.main }} variant="body2"
onClick={() => addVariantWithFocus(question)} sx={{ color: theme.palette.brightPurple.main }}
> onClick={() => addVariantWithFocus(question)}
Добавьте ответ >
</Link> Добавьте ответ
</Link>
{isMobile ? null : (
<>
<Typography
sx={{
fontWeight: 400,
lineHeight: "21.33px",
color: theme.palette.grey2.main,
fontSize: "16px",
}}
>
или нажмите Enter
</Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)}
</Box>
{isMobile ? null : ( {isMobile ? null : (
<> <Typography
<Typography sx={{
sx={{ fontWeight: 400,
fontWeight: 400, lineHeight: "21.33px",
lineHeight: "21.33px", color: theme.palette.grey2.main,
color: theme.palette.grey2.main, fontSize: "16px",
fontSize: "16px", mt: "4px",
}} }}
> >
или нажмите Enter для переноса строки нажмите shift + enter
</Typography> </Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)} )}
</Box> </Box>
</Box> </Box>
@ -154,11 +169,12 @@ export default function OptionsPicture({
questionContentId={question.content.id} questionContentId={question.content.id}
questionType={question.type} questionType={question.type}
questionHasParent={question.content.rule.parentId?.length !== 0} questionHasParent={question.content.rule.parentId?.length !== 0}
openBranchingPage={openBranchingPage}
setOpenBranchingPage={setOpenBranchingPage} setOpenBranchingPage={setOpenBranchingPage}
/> />
<SwitchAnswerOptionsPict <SwitchAnswerOptionsPict
switchState={switchState} switchState={switchState}
question={question} question={question as any}
/> />
</> </>
); );

@ -54,7 +54,7 @@ export default function AnswerOptions({ question, openBranchingPage, setOpenBran
questionId={question.id} questionId={question.id}
variant={variant} variant={variant}
isOwn={Boolean(variant.isOwn)} isOwn={Boolean(variant.isOwn)}
ownPlaceholder={question.content.ownPlaceholder} ownPlaceholder={question.content.ownPlaceholder || ""}
shouldAutoFocus={focusedVariantId === variant.id} shouldAutoFocus={focusedVariantId === variant.id}
onFocusHandled={clearFocusedVariant} onFocusHandled={clearFocusedVariant}
onEnterKeyPress={() => addVariantOnEnter(question.id)} onEnterKeyPress={() => addVariantOnEnter(question.id)}
@ -63,47 +63,56 @@ export default function AnswerOptions({ question, openBranchingPage, setOpenBran
/> />
)} )}
<Box <Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-start", marginBottom: "17px" }}>
sx={{ <Box sx={{ display: "flex", alignItems: "center" }}>
display: "flex", <Link
alignItems: "center", component="button"
marginBottom: "17px", variant="body2"
}} sx={{
> color: theme.palette.brightPurple.main,
<Link fontWeight: "400",
component="button" fontSize: "16px",
variant="body2" mr: "4px",
sx={{ height: "19px",
color: theme.palette.brightPurple.main, }}
fontWeight: "400", onClick={() => addVariantWithFocus(question)}
fontSize: "16px", >
mr: "4px", Добавьте ответ
height: "19px", </Link>
}} {isMobile ? null : (
onClick={() => addVariantWithFocus(question)} <>
> <Typography
Добавьте ответ sx={{
</Link> fontWeight: 400,
lineHeight: "21.33px",
color: theme.palette.grey2.main,
fontSize: "16px",
}}
>
или нажмите Enter
</Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)}
</Box>
{isMobile ? null : ( {isMobile ? null : (
<> <Typography
<Typography sx={{
sx={{ fontWeight: 400,
fontWeight: 400, lineHeight: "21.33px",
lineHeight: "21.33px", color: theme.palette.grey2.main,
color: theme.palette.grey2.main, fontSize: "16px",
fontSize: "16px", mt: "4px",
}} }}
> >
или нажмите Enter для переноса строки нажмите shift + enter
</Typography> </Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)} )}
</Box> </Box>
</Box> </Box>

@ -17,12 +17,12 @@ interface CustomTextFieldProps {
value?: string; value?: string;
error?: string; error?: string;
emptyError?: boolean; emptyError?: boolean;
onChange?: (event: ChangeEvent<HTMLInputElement>) => void; onChange?: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void; onKeyDown?: (event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
onBlur?: (event: FocusEvent<HTMLInputElement>) => void; onBlur?: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
onFocus?: (event: FocusEvent<HTMLInputElement>) => void; onFocus?: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
onClick?: (event: MouseEvent<HTMLInputElement>) => void; onClick?: (event: MouseEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
onPaste?: (event: ClipboardEvent<HTMLInputElement>) => void; onPaste?: (event: ClipboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
text?: string; text?: string;
maxLength?: number; maxLength?: number;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
@ -32,7 +32,7 @@ interface CustomTextFieldProps {
rows?: number; rows?: number;
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
inputRef?: Ref<HTMLInputElement>; inputRef?: Ref<HTMLInputElement | HTMLTextAreaElement>;
} }
export default function CustomTextField({ export default function CustomTextField({
@ -67,7 +67,9 @@ export default function CustomTextField({
setInputValue(value); setInputValue(value);
}, [value]); }, [value]);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
if (event.target.value.length <= maxLength) { if (event.target.value.length <= maxLength) {
const inputValue = event.target.value; const inputValue = event.target.value;
@ -85,12 +87,16 @@ export default function CustomTextField({
} }
}; };
const handleInputFocus = (event: React.FocusEvent<HTMLInputElement>) => { const handleInputFocus = (
event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setIsInputActive(true); setIsInputActive(true);
if (onFocus) onFocus(event); if (onFocus) onFocus(event);
}; };
const handleInputBlur = (event: React.FocusEvent<HTMLInputElement>) => { const handleInputBlur = (
event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setIsInputActive(false); setIsInputActive(false);
if (onBlur) { if (onBlur) {
@ -98,6 +104,12 @@ export default function CustomTextField({
} }
}; };
const mergedInputElementProps = {
...(InputProps?.inputProps as any),
onClick,
onPaste,
};
return ( return (
<FormControl <FormControl
fullWidth fullWidth
@ -126,14 +138,13 @@ export default function CustomTextField({
onFocus={handleInputFocus} onFocus={handleInputFocus}
onBlur={handleInputBlur} onBlur={handleInputBlur}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onClick={onClick} multiline
onPaste={onPaste} rows={rows > 0 ? rows : 1}
multiline={rows > 0}
rows={rows}
disabled={disabled} disabled={disabled}
disableUnderline disableUnderline
inputRef={inputRef} inputRef={inputRef}
{...InputProps} {...InputProps}
inputProps={mergedInputElementProps}
sx={{ sx={{
maxLength: maxLength, maxLength: maxLength,
borderRadius: "10px", borderRadius: "10px",
@ -143,6 +154,8 @@ export default function CustomTextField({
border: `${isInputActive ? "black 2px" : "#9A9AAF 1px"} solid`, border: `${isInputActive ? "black 2px" : "#9A9AAF 1px"} solid`,
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
height: "48px", height: "48px",
// Prevent resize handle to keep visuals unchanged
'& textarea': { resize: 'none' },
...sx, ...sx,
}} }}
data-cy="textfield" data-cy="textfield"