новый ответ на вопрос получает фокус

This commit is contained in:
Nastya 2025-09-03 20:25:14 +03:00
parent 751d9eb4f3
commit 862ed4f395
16 changed files with 144 additions and 67 deletions

@ -12,7 +12,7 @@ import {
} from "@mui/material";
import { addQuestionVariant, deleteQuestionVariant, setQuestionVariantField, updateQuestion } from "@root/questions/actions";
import { enqueueSnackbar } from "notistack";
import { memo, type ChangeEvent, type FC, type KeyboardEvent, type ReactNode } from "react";
import { memo, useCallback, type ChangeEvent, type FC, type KeyboardEvent, type ReactNode } from "react";
import { Draggable } from "react-beautiful-dnd";
import type { QuestionVariant, QuizQuestionVariant } from "@frontend/squzanswerer";
@ -28,10 +28,13 @@ type AnswerItemProps = {
additionalMobile?: ReactNode;
isOwn: boolean;
ownPlaceholder: string;
shouldAutoFocus?: boolean;
onFocusHandled?: () => void;
onEnterKeyPress?: () => void;
};
const AnswerItem = memo<AnswerItemProps>(
({ index, variant, questionId, largeCheck = false, additionalContent, additionalMobile, disableKeyDown, isOwn, ownPlaceholder }) => {
({ index, variant, questionId, largeCheck = false, additionalContent, additionalMobile, disableKeyDown, isOwn, ownPlaceholder, shouldAutoFocus, onFocusHandled, onEnterKeyPress }) => {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(790));
@ -41,6 +44,26 @@ const AnswerItem = memo<AnswerItemProps>(
});
};
const inputRefCallback = useCallback((element: HTMLInputElement | null) => {
if (element && shouldAutoFocus) {
element.focus();
onFocusHandled?.();
}
}, [shouldAutoFocus, onFocusHandled]);
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (disableKeyDown) {
enqueueSnackbar("100 максимальное количество");
} else if (event.code === "Enter" && !largeCheck) {
if (onEnterKeyPress) {
onEnterKeyPress();
} else {
// Fallback если onEnterKeyPress не передан
addQuestionVariant(questionId);
}
}
};
return (
<Draggable
@ -64,10 +87,11 @@ const AnswerItem = memo<AnswerItemProps>(
}}
>
<TextField
inputRef={inputRefCallback}
value={isOwn ? ownPlaceholder : variant.answer}
fullWidth
focused={false}
placeholder={isOwn ? "Добавьте текст-подсказку для ввода “своего ответа”" : "Добавьте ответ"}
placeholder={isOwn ? "Добавьте текст-подсказку для ввода \"своего ответа\"" : "Добавьте ответ"}
multiline={largeCheck}
onChange={({ target }: ChangeEvent<HTMLInputElement>) => {
if (target.value.length <= 1000) {
@ -79,13 +103,7 @@ const AnswerItem = memo<AnswerItemProps>(
enqueueSnackbar("Превышена длина вводимого текста")
}
}}
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
if (disableKeyDown) {
enqueueSnackbar("100 максимальное количество");
} else if (event.code === "Enter" && !largeCheck) {
addQuestionVariant(questionId);
}
}}
onKeyDown={handleKeyDown}
InputProps={{
startAdornment: (
<>

@ -16,6 +16,9 @@ type Props = Omit<
openImageUploadModal: () => void;
isOwn: boolean;
ownPlaceholder: string;
shouldAutoFocus?: boolean;
onFocusHandled?: () => void;
onEnterKeyPress?: () => void;
};
export default function ImageEditAnswerItem({
@ -31,6 +34,9 @@ export default function ImageEditAnswerItem({
openImageUploadModal,
isOwn,
ownPlaceholder,
shouldAutoFocus,
onFocusHandled,
onEnterKeyPress,
}: Props) {
const addOrEditImageButton = useMemo(() => {
return (
@ -111,6 +117,9 @@ export default function ImageEditAnswerItem({
additionalMobile={addOrEditImageButtonMobile}
isOwn={isOwn}
ownPlaceholder={ownPlaceholder}
shouldAutoFocus={shouldAutoFocus}
onFocusHandled={onFocusHandled}
onEnterKeyPress={onEnterKeyPress}
/>
);
}

@ -1,7 +1,7 @@
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useCallback, useState } from "react";
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
import { useAddAnswer } from "../../../utils/hooks/useAddAnswer";
import { useQuestionVariantsWithFocus } from "../../../utils/questionVariants";
import { AnswerDraggableList } from "../AnswerDraggableList";
import ButtonsOptions from "../QuestionOptions/ButtonsLayout/ButtonsOptions";
import SwitchDropDown from "./switchDropDown";
@ -16,7 +16,7 @@ interface Props {
}
export default function DropDown({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const {onClickAddAnAnswer} = useAddAnswer();
const {addVariantWithFocus, addVariantOnEnter, focusedVariantId, clearFocusedVariant} = useQuestionVariantsWithFocus();
const [switchState, setSwitchState] = useState("setting");
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -50,6 +50,11 @@ export default function DropDown({ question, openBranchingPage, setOpenBranching
disableKeyDown={question.content.variants.length >= 100}
questionId={question.id}
variant={variant}
isOwn={Boolean(variant?.isOwn)}
ownPlaceholder={question.content.ownPlaceholder || ""}
shouldAutoFocus={focusedVariantId === variant.id}
onFocusHandled={clearFocusedVariant}
onEnterKeyPress={() => addVariantOnEnter(question.id)}
/>
))}
/>
@ -71,7 +76,7 @@ export default function DropDown({ question, openBranchingPage, setOpenBranching
mr: "4px",
height: "19px",
}}
onClick={() => onClickAddAnAnswer(question)}
onClick={() => addVariantWithFocus(question)}
>
Добавьте ответ
</Link>

@ -4,7 +4,7 @@ import { EmojiPicker } from "@ui_kit/EmojiPicker";
import { useState } from "react";
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
import type { QuizQuestionEmoji } from "@frontend/squzanswerer";
import { useAddAnswer } from "../../../utils/hooks/useAddAnswer";
import { useQuestionVariantsWithFocus } from "../../../utils/questionVariants";
import { AnswerDraggableList } from "../AnswerDraggableList";
import ButtonsOptions from "../QuestionOptions/ButtonsLayout/ButtonsOptions";
import EmojiAnswerItem from "./EmojiAnswerItem/EmojiAnswerItem";
@ -19,7 +19,7 @@ interface Props {
export default function Emoji({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const [switchState, setSwitchState] = useState<string>("setting");
const {onClickAddAnAnswer} = useAddAnswer();
const {addVariantWithFocus, addVariantOnEnter, focusedVariantId, clearFocusedVariant} = useQuestionVariantsWithFocus();
const [open, setOpen] = useState<boolean>(false);
const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>(null);
const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
@ -48,6 +48,9 @@ export default function Emoji({ question, openBranchingPage, setOpenBranchingPag
setSelectedVariant={setSelectedVariant}
isOwn={Boolean(variant?.isOwn)}
ownPlaceholder={question.content.ownPlaceholder}
shouldAutoFocus={focusedVariantId === variant.id}
onFocusHandled={clearFocusedVariant}
onEnterKeyPress={() => addVariantOnEnter(question.id)}
/>
))}
/>
@ -93,7 +96,7 @@ export default function Emoji({ question, openBranchingPage, setOpenBranchingPag
component="button"
variant="body2"
sx={{ color: theme.palette.brightPurple.main }}
onClick={() => onClickAddAnAnswer(question)}
onClick={() => addVariantWithFocus(question)}
>
Добавьте ответ
</Link>

@ -14,6 +14,9 @@ type Props = Omit<
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
isOwn: boolean;
ownPlaceholder: string;
shouldAutoFocus?: boolean;
onFocusHandled?: () => void;
onEnterKeyPress?: () => void;
};
export default function EmojiAnswerItem({
@ -28,6 +31,9 @@ export default function EmojiAnswerItem({
setOpen,
isOwn,
ownPlaceholder,
shouldAutoFocus,
onFocusHandled,
onEnterKeyPress,
}: Props) {
@ -99,6 +105,9 @@ export default function EmojiAnswerItem({
additionalMobile={addOrEditImageButtonMobile}
isOwn={isOwn}
ownPlaceholder={ownPlaceholder}
shouldAutoFocus={shouldAutoFocus}
onFocusHandled={onFocusHandled}
onEnterKeyPress={onEnterKeyPress}
/>
);
}

@ -4,7 +4,7 @@ import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import { memo } from "react";
import CustomTextField from "@ui_kit/CustomTextField";
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
type SettingEmojiProps = {
question: QuizQuestionEmoji;
@ -17,7 +17,7 @@ type SettingEmojiProps = {
const SettingEmoji = memo<SettingEmojiProps>(function ({ question, questionId, isRequired, isLargeCheck, isMulti, isOwn }) {
const theme = useTheme();
const {switchOwn} = useAddAnswer();
const {switchOwnVariant} = useQuestionVariantsWithFocus();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
@ -92,7 +92,7 @@ const SettingEmoji = memo<SettingEmojiProps>(function ({ question, questionId, i
label={'Вариант "свой ответ"'}
checked={isOwn}
handleChange={({ target }) => {
switchOwn({question, checked:target.checked})
switchOwnVariant({question, checked:target.checked})
}}
/>
{/* <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>

@ -1,9 +1,9 @@
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import {
addQuestionVariant,
clearQuestionImages,
uploadQuestionImage,
} from "@root/questions/actions";
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useEffect, useMemo, useState } from "react";
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
@ -31,6 +31,7 @@ export default function OptionsAndPicture({
const [switchState, setSwitchState] = useState("setting");
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
const [openCropModal, setOpenCropModal] = useState(false);
const {addVariantWithFocus, addVariantOnEnter, focusedVariantId, clearFocusedVariant} = useQuestionVariantsWithFocus();
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(
null,
@ -111,6 +112,9 @@ export default function OptionsAndPicture({
setSelectedVariantId={setSelectedVariantId}
isOwn={Boolean(variant?.isOwn)}
ownPlaceholder={question.content.ownPlaceholder}
shouldAutoFocus={focusedVariantId === variant.id}
onFocusHandled={clearFocusedVariant}
onEnterKeyPress={() => addVariantOnEnter(question.id)}
/>
))}
/>
@ -148,7 +152,7 @@ export default function OptionsAndPicture({
height: "19px",
}}
onClick={() => {
addQuestionVariant(question.id);
addVariantWithFocus(question);
}}
>
Добавьте ответ

@ -1,4 +1,4 @@
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
import type { QuizQuestionVarImg, QuizQuestionVariant } from "@frontend/squzanswerer";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
@ -19,7 +19,7 @@ type SettingOptionsAndPictProps = {
const SettingOptionsAndPict = memo<SettingOptionsAndPictProps>(function ({ question, questionId, ownPlaceholder, isMulti, isLargeCheck, replText, isRequired, isOwn }) {
const theme = useTheme();
const { switchOwn } = useAddAnswer();
const { switchOwnVariant } = useQuestionVariantsWithFocus();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
@ -73,7 +73,7 @@ const SettingOptionsAndPict = memo<SettingOptionsAndPictProps>(function ({ quest
label={'Вариант "свой ответ"'}
checked={isOwn}
handleChange={({ target }) => {
switchOwn({ question, checked: target.checked })
switchOwnVariant({ question, checked: target.checked })
}}
/>
<CustomCheckbox

@ -9,7 +9,7 @@ import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
import type { QuizQuestionVarImg } from "@frontend/squzanswerer/dist-package/model/questionTypes/varimg";
//@/model/questionTypes/images";
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
import { useDisclosure } from "@/utils/useDisclosure";
import { AnswerDraggableList } from "../../AnswerDraggableList";
import ImageEditAnswerItem from "../../AnswerDraggableList/ImageEditAnswerItem";
@ -31,7 +31,7 @@ export default function OptionsPicture({
setOpenBranchingPage,
}: Props) {
const theme = useTheme();
const {onClickAddAnAnswer} = useAddAnswer();
const {addVariantWithFocus, addVariantOnEnter, focusedVariantId, clearFocusedVariant} = useQuestionVariantsWithFocus();
const quizQid = useCurrentQuiz()?.qid;
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
const [openCropModal, setOpenCropModal] = useState(false);
@ -93,6 +93,9 @@ export default function OptionsPicture({
setSelectedVariantId={setSelectedVariantId}
isOwn={Boolean(variant?.isOwn)}
ownPlaceholder={question.content.ownPlaceholder}
shouldAutoFocus={focusedVariantId === variant.id}
onFocusHandled={clearFocusedVariant}
onEnterKeyPress={() => addVariantOnEnter(question.id)}
/>
))}
/>
@ -117,7 +120,7 @@ export default function OptionsPicture({
component="button"
variant="body2"
sx={{ color: theme.palette.brightPurple.main }}
onClick={() => onClickAddAnAnswer(question)}
onClick={() => addVariantWithFocus(question)}
>
Добавьте ответ
</Link>

@ -9,7 +9,7 @@ import ProportionsIcon11 from "@/assets/icons/questionsPage/ProportionsIcon11";
import ProportionsIcon12 from "@/assets/icons/questionsPage/ProportionsIcon12";
import ProportionsIcon21 from "@/assets/icons/questionsPage/ProportionsIcon21";
import CustomTextField from "@ui_kit/CustomTextField";
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
type Proportion = "1:1" | "1:2" | "2:1";
@ -69,7 +69,7 @@ const SettingOptionsPict = memo<SettingOptionsPictProps>(function ({
question.content.ownPlaceholder = replText;
});
};
const {switchOwn} = useAddAnswer();
const {switchOwnVariant} = useQuestionVariantsWithFocus();
return (
<Box
@ -175,7 +175,7 @@ const SettingOptionsPict = memo<SettingOptionsPictProps>(function ({
label={'Вариант "свой ответ"'}
checked={isOwn}
handleChange={({ target }) => {
switchOwn({question, checked:target.checked})
switchOwnVariant({question, checked:target.checked})
}}
/>
{/* <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>

@ -9,7 +9,7 @@ import ProportionsIcon11 from "@/assets/icons/questionsPage/ProportionsIcon11";
import ProportionsIcon12 from "@/assets/icons/questionsPage/ProportionsIcon12";
import ProportionsIcon21 from "@/assets/icons/questionsPage/ProportionsIcon21";
import CustomTextField from "@ui_kit/CustomTextField";
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
type Proportion = "1:1" | "1:2" | "2:1";
@ -69,7 +69,7 @@ const SettingOptionsPict = memo<SettingOptionsPictProps>(function ({
question.content.ownPlaceholder = replText;
});
};
const {switchOwn} = useAddAnswer();
const {switchOwnVariant} = useQuestionVariantsWithFocus();
return (
<Box
@ -175,7 +175,7 @@ const SettingOptionsPict = memo<SettingOptionsPictProps>(function ({
label={'Вариант "свой ответ"'}
checked={isOwn}
handleChange={({ target }) => {
switchOwn({question, checked:target.checked})
switchOwnVariant({question, checked:target.checked})
}}
/>
{/* <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>

@ -2,7 +2,7 @@ import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useEffect, useState } from "react";
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
import type { QuizQuestionVariant } from "@frontend/squzanswerer";
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
import { AnswerDraggableList } from "../../AnswerDraggableList";
import AnswerItem from "../../AnswerDraggableList/AnswerItem";
import ButtonsOptions from "../ButtonsLayout/ButtonsOptions";
@ -15,7 +15,7 @@ interface Props {
}
export default function AnswerOptions({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const {onClickAddAnAnswer} = useAddAnswer();
const {addVariantWithFocus, addVariantOnEnter, focusedVariantId, clearFocusedVariant} = useQuestionVariantsWithFocus();
const [switchState, setSwitchState] = useState("setting");
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -55,6 +55,9 @@ export default function AnswerOptions({ question, openBranchingPage, setOpenBran
variant={variant}
isOwn={Boolean(variant.isOwn)}
ownPlaceholder={question.content.ownPlaceholder}
shouldAutoFocus={focusedVariantId === variant.id}
onFocusHandled={clearFocusedVariant}
onEnterKeyPress={() => addVariantOnEnter(question.id)}
/>
))}
/>
@ -77,7 +80,7 @@ export default function AnswerOptions({ question, openBranchingPage, setOpenBran
mr: "4px",
height: "19px",
}}
onClick={() => onClickAddAnAnswer(question)}
onClick={() => addVariantWithFocus(question)}
>
Добавьте ответ
</Link>

@ -4,7 +4,7 @@ import CustomCheckbox from "@ui_kit/CustomCheckbox";
import type { QuizQuestionVariant } from "@frontend/squzanswerer";
import { memo } from "react";
import CustomTextField from "@ui_kit/CustomTextField";
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
interface Props {
question: QuizQuestionVariant;
@ -21,7 +21,7 @@ const ResponseSettings = memo<Props>(function ({question, questionId, ownPlaceho
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const {switchOwn} = useAddAnswer();
const {switchOwnVariant} = useQuestionVariantsWithFocus();
return (
<Box
@ -84,7 +84,7 @@ const ResponseSettings = memo<Props>(function ({question, questionId, ownPlaceho
label={'Вариант "свой ответ"'}
checked={isOwn}
handleChange={({ target }) => {
switchOwn({question, checked:target.checked})
switchOwnVariant({question, checked:target.checked})
}}
/>
</Box>

@ -327,7 +327,8 @@ export const updateQuestion = async <T = AnyTypedQuizQuestion>(
requestQueue.enqueue(`updateQuestion-${questionId}`, request);
};
export const addQuestionVariant = (questionId: string) => {
export const addQuestionVariant = (questionId: string): string => {
const newVariant = createQuestionVariant();
updateQuestion(questionId, (question) => {
switch (question.type) {
case "variant":
@ -335,12 +336,13 @@ export const addQuestionVariant = (questionId: string) => {
case "select":
case "images":
case "varimg":
question.content.variants.push(createQuestionVariant());
question.content.variants.push(newVariant);
break;
default:
throw new Error(`Cannot add variant to question of type "${question.type}"`);
}
});
return newVariant.id;
};
export const addQuestionOwnVariant = (questionId: string) => {
updateQuestion(questionId, (question) => {

@ -1,26 +0,0 @@
import { QuizQuestionsWithVariants } from "@frontend/squzanswerer";
import { addQuestionOwnVariant, addQuestionVariant, updateQuestion } from "@root/questions/actions";
export const useAddAnswer = () => {
const onClickAddAnAnswer = (question: QuizQuestionsWithVariants) => {
addQuestionVariant(question.id);
};
interface SwitchOwnProps {
question: QuizQuestionsWithVariants;
checked: boolean
}
const switchOwn = ({ question, checked }: SwitchOwnProps) => {
if (!question.content.variants.some(v => v.isOwn) && checked) {
addQuestionOwnVariant(question.id)
}
updateQuestion<QuizQuestionVariant>(question.id, (question) => {
question.content.own = checked;
});
}
return {
onClickAddAnAnswer,
switchOwn
};
};

@ -0,0 +1,47 @@
import { QuizQuestionsWithVariants, QuizQuestionVariant } from "@frontend/squzanswerer";
import { addQuestionOwnVariant, addQuestionVariant, updateQuestion } from "@root/questions/actions";
import { useState } from "react";
/**
* Утилита для управления вариантами ответов с автофокусом
*/
export const useQuestionVariantsWithFocus = () => {
const [focusedVariantId, setFocusedVariantId] = useState<string | null>(null);
const addVariantWithFocus = (question: QuizQuestionsWithVariants) => {
const newVariantId = addQuestionVariant(question.id);
setFocusedVariantId(newVariantId);
};
const addVariantOnEnter = (questionId: string) => {
const newVariantId = addQuestionVariant(questionId);
setFocusedVariantId(newVariantId);
};
const clearFocusedVariant = () => {
setFocusedVariantId(null);
};
interface SwitchOwnProps {
question: QuizQuestionsWithVariants;
checked: boolean
}
const switchOwnVariant = ({ question, checked }: SwitchOwnProps) => {
if (!question.content.variants.some(v => v.isOwn) && checked) {
addQuestionOwnVariant(question.id)
}
updateQuestion<QuizQuestionVariant>(question.id, (question) => {
question.content.own = checked;
});
}
return {
addVariantWithFocus,
addVariantOnEnter,
switchOwnVariant,
focusedVariantId,
clearFocusedVariant
};
};