문제 풀이 화면의 입력 항 진행 상태 표시 방법 개선
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
import type { FocusEvent, FormEvent } from "react";
|
||||
|
||||
import { DESKTOP_INPUT_DEVICE_QUERY } from "@/lib/input-device";
|
||||
import KatexRenderer from "@/components/math/KatexRenderer";
|
||||
|
||||
export type ActiveAnswerInput = 0 | 1;
|
||||
|
||||
@@ -30,15 +31,16 @@ const KEYPAD_KEYS = [
|
||||
"0",
|
||||
];
|
||||
|
||||
export interface AnswerFormHandle {
|
||||
export interface NumberAnswerFormHandle {
|
||||
focusInput: () => void;
|
||||
}
|
||||
|
||||
interface AnswerFormProps {
|
||||
interface NumberAnswerFormProps {
|
||||
answerA: string;
|
||||
answerB: string;
|
||||
activeInput: ActiveAnswerInput;
|
||||
error: string;
|
||||
pendingAnswerLabels: [string, string];
|
||||
onAnswerAChange: (value: string) => void;
|
||||
onAnswerBChange: (value: string) => void;
|
||||
onActiveInputChange: (input: ActiveAnswerInput) => void;
|
||||
@@ -47,12 +49,16 @@ interface AnswerFormProps {
|
||||
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||
}
|
||||
|
||||
const AnswerForm = forwardRef<AnswerFormHandle, AnswerFormProps>(function AnswerForm(
|
||||
const NumberAnswerForm = forwardRef<
|
||||
NumberAnswerFormHandle,
|
||||
NumberAnswerFormProps
|
||||
>(function NumberAnswerForm(
|
||||
{
|
||||
answerA,
|
||||
answerB,
|
||||
activeInput,
|
||||
error,
|
||||
pendingAnswerLabels,
|
||||
onAnswerAChange,
|
||||
onAnswerBChange,
|
||||
onActiveInputChange,
|
||||
@@ -71,6 +77,7 @@ const AnswerForm = forwardRef<AnswerFormHandle, AnswerFormProps>(function Answer
|
||||
|
||||
const activeAnswer = activeInput === 0 ? answerA : answerB;
|
||||
const activeLabel = activeInput === 0 ? "첫째 항" : "둘째 항";
|
||||
const activeDescription = `${activeLabel}의 답을 입력하세요.`;
|
||||
const activeEnterKeyHint = activeInput === 0 ? "next" : "done";
|
||||
const activeAnswerChange =
|
||||
activeInput === 0 ? onAnswerAChange : onAnswerBChange;
|
||||
@@ -210,62 +217,73 @@ const AnswerForm = forwardRef<AnswerFormHandle, AnswerFormProps>(function Answer
|
||||
className="rounded-md bg-white p-6 shadow-sm ring-1 ring-slate-200 sm:p-8"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="mb-2 block text-sm font-semibold text-slate-700"
|
||||
className="text-sm font-semibold text-slate-500"
|
||||
>
|
||||
{activeLabel}
|
||||
{activeDescription}
|
||||
</label>
|
||||
<input
|
||||
id={inputId}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={activeAnswer}
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
onChange={(event) => updateAnswer(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
handleEnter();
|
||||
}
|
||||
}}
|
||||
autoComplete="off"
|
||||
enterKeyHint={activeEnterKeyHint}
|
||||
inputMode="none"
|
||||
pattern="-?[0-9]*(\.[0-9]*)?"
|
||||
className="h-12 w-full min-w-0 rounded-md border border-slate-300 bg-white px-3 text-lg font-semibold text-slate-950 outline-none transition focus:border-emerald-600 focus:ring-2 focus:ring-emerald-200 sm:px-4"
|
||||
/>
|
||||
<ol
|
||||
className="flex min-w-40 items-center gap-1"
|
||||
aria-label="입력 항 진행 상태"
|
||||
>
|
||||
{answerButtons.map((button) => {
|
||||
const selected = activeInput === button.index;
|
||||
const completed = button.value !== "";
|
||||
const displayValue =
|
||||
button.value || pendingAnswerLabels[button.index];
|
||||
|
||||
return (
|
||||
<li
|
||||
key={button.index}
|
||||
className="min-w-0 flex-1"
|
||||
aria-label={`${button.label} ${
|
||||
completed ? `입력값 ${button.value}` : "미입력"
|
||||
}${selected ? ", 현재 입력 중" : ""}`}
|
||||
>
|
||||
<span
|
||||
className={`block truncate text-center text-xs font-bold ${
|
||||
selected || completed ? "text-emerald-800" : "text-slate-500"
|
||||
}`}
|
||||
>
|
||||
<KatexRenderer latex={displayValue} />
|
||||
</span>
|
||||
<span
|
||||
className={`mt-1 block h-1.5 rounded-full ${
|
||||
completed
|
||||
? "bg-emerald-700"
|
||||
: selected
|
||||
? "bg-emerald-300 ring-1 ring-emerald-700"
|
||||
: "bg-slate-200"
|
||||
}`}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="inline-flex w-fit max-w-full gap-1 rounded-md border border-slate-300 bg-white p-1 shadow-sm"
|
||||
role="group"
|
||||
aria-label="입력 항 선택"
|
||||
>
|
||||
{answerButtons.map((button) => {
|
||||
const selected = activeInput === button.index;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={button.index}
|
||||
type="button"
|
||||
onPointerDown={(event) => event.preventDefault()}
|
||||
onClick={() => moveToInput(button.index, true)}
|
||||
aria-pressed={selected}
|
||||
aria-label={`${button.label} 다시 입력`}
|
||||
className={`inline-flex h-10 min-w-20 items-center justify-center rounded px-3 text-base font-semibold transition focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 ${
|
||||
selected
|
||||
? "bg-emerald-700 text-white"
|
||||
: "text-slate-700 hover:bg-slate-100"
|
||||
}`}
|
||||
>
|
||||
{button.value || "?"}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<input
|
||||
id={inputId}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={activeAnswer}
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
onChange={(event) => updateAnswer(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
handleEnter();
|
||||
}
|
||||
}}
|
||||
autoComplete="off"
|
||||
enterKeyHint={activeEnterKeyHint}
|
||||
inputMode="none"
|
||||
pattern="-?[0-9]*(\.[0-9]*)?"
|
||||
className="h-12 w-full min-w-0 rounded-md border border-slate-300 bg-white px-3 text-lg font-semibold text-slate-950 outline-none transition focus:border-emerald-600 focus:ring-2 focus:ring-emerald-200 sm:px-4"
|
||||
/>
|
||||
|
||||
{keypadOpen && !desktopInputDevice ? (
|
||||
<div
|
||||
@@ -333,4 +351,4 @@ const AnswerForm = forwardRef<AnswerFormHandle, AnswerFormProps>(function Answer
|
||||
);
|
||||
});
|
||||
|
||||
export default AnswerForm;
|
||||
export default NumberAnswerForm;
|
||||
@@ -9,31 +9,33 @@ interface VariableChoiceAnswerFormProps {
|
||||
choices: [VariableTermChoice[], VariableTermChoice[]];
|
||||
selectedChoiceIds: [string | null, string | null];
|
||||
activeTermIndex: 0 | 1;
|
||||
pendingChoiceLabels: [string, string];
|
||||
onSelectChoice: (termIndex: 0 | 1, choiceId: string) => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
export default function VariableChoiceAnswerForm({
|
||||
choices,
|
||||
selectedChoiceIds,
|
||||
activeTermIndex,
|
||||
pendingChoiceLabels,
|
||||
onSelectChoice,
|
||||
onSubmit,
|
||||
}: VariableChoiceAnswerFormProps) {
|
||||
const firstChoiceRef = useRef<HTMLButtonElement>(null);
|
||||
const submitButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const canSubmit = selectedChoiceIds[0] !== null && selectedChoiceIds[1] !== null;
|
||||
const activeChoices = choices[activeTermIndex];
|
||||
const activeChoices = [...choices[activeTermIndex]].sort(
|
||||
(a, b) => a.coefficient - b.coefficient,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
firstChoiceRef.current?.focus();
|
||||
}, [activeTermIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (canSubmit) {
|
||||
submitButtonRef.current?.focus();
|
||||
}
|
||||
}, [canSubmit]);
|
||||
function getSelectedChoice(index: 0 | 1) {
|
||||
const selectedChoiceId = selectedChoiceIds[index];
|
||||
|
||||
if (!selectedChoiceId) return null;
|
||||
|
||||
return choices[index].find((choice) => choice.id === selectedChoiceId) ?? null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md bg-white p-6 shadow-sm ring-1 ring-slate-200">
|
||||
@@ -42,32 +44,48 @@ export default function VariableChoiceAnswerForm({
|
||||
<p className="text-sm font-semibold text-slate-500">
|
||||
{activeTermIndex === 0 ? "첫째 항" : "둘째 항"}을 고르세요.
|
||||
</p>
|
||||
<p className="mt-1 text-base text-slate-700">
|
||||
괄호를 풀었을 때의 부호를 선택합니다.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="inline-flex gap-1 rounded-md border border-slate-300 bg-slate-50 p-1"
|
||||
role="group"
|
||||
<ol
|
||||
className="flex min-w-40 items-center gap-1"
|
||||
aria-label="입력 항 진행 상태"
|
||||
>
|
||||
{selectedChoiceIds.map((choiceId, index) => {
|
||||
const selected = activeTermIndex === index;
|
||||
const selectedChoice = getSelectedChoice(index as 0 | 1);
|
||||
const completed = choiceId !== null && selectedChoice !== null;
|
||||
|
||||
return (
|
||||
<span
|
||||
<li
|
||||
key={index}
|
||||
className={`inline-flex h-9 min-w-16 items-center justify-center rounded px-3 text-sm font-bold ${
|
||||
selected
|
||||
? "bg-emerald-700 text-white"
|
||||
: "bg-white text-slate-700"
|
||||
}`}
|
||||
className="min-w-0 flex-1"
|
||||
aria-label={`${index + 1}항 ${
|
||||
completed ? "선택 완료" : "미선택"
|
||||
}${selected ? ", 현재 선택 중" : ""}`}
|
||||
>
|
||||
{choiceId ? `${index + 1}항 완료` : `${index + 1}항`}
|
||||
</span>
|
||||
<span
|
||||
className={`block truncate text-center text-xs font-bold ${
|
||||
selected || completed ? "text-emerald-800" : "text-slate-500"
|
||||
}`}
|
||||
>
|
||||
{completed ? (
|
||||
<KatexRenderer latex={selectedChoice.latex} />
|
||||
) : (
|
||||
<KatexRenderer latex={pendingChoiceLabels[index]} />
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`mt-1 block h-1.5 rounded-full ${
|
||||
completed
|
||||
? "bg-emerald-700"
|
||||
: selected
|
||||
? "bg-emerald-300 ring-1 ring-emerald-700"
|
||||
: "bg-slate-200"
|
||||
}`}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -96,16 +114,6 @@ export default function VariableChoiceAnswerForm({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
ref={submitButtonRef}
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
disabled={!canSubmit}
|
||||
className="mt-5 inline-flex h-11 items-center justify-center rounded-md bg-slate-950 px-5 text-base font-semibold text-white transition hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:bg-slate-300"
|
||||
>
|
||||
정답 확인
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user