diff --git a/src/components/Componentes/question-number.test.tsx b/src/components/Componentes/question-number.test.tsx
new file mode 100644
index 0000000..c9f56e6
--- /dev/null
+++ b/src/components/Componentes/question-number.test.tsx
@@ -0,0 +1,80 @@
+import { render, screen, fireEvent, cleanup } from "@testing-library/react";
+import { describe, expect, it, afterEach } from "vitest";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import QuestionNumber, { normalizeNumberString } from "./question-number";
+import { QuestionAnswersProvider } from "./question-answer-storage";
+import type { QuestionField } from "@/lib/schema-adapter";
+
+describe("normalizeNumberString", () => {
+ it("should convert Persian digits to English digits", () => {
+ expect(normalizeNumberString("۱۲۳۴۵۶۷۸۹۰")).toBe("1234567890");
+ expect(normalizeNumberString("۵")).toBe("5");
+ expect(normalizeNumberString("۳٫۵")).toBe("3.5");
+ });
+
+ it("should convert Arabic digits to English digits", () => {
+ expect(normalizeNumberString("١٢٣٤٥٦٧٨٩٠")).toBe("1234567890");
+ expect(normalizeNumberString("٤")).toBe("4");
+ });
+
+ it("should preserve English digits", () => {
+ expect(normalizeNumberString("12345")).toBe("12345");
+ });
+});
+
+describe("QuestionNumber Component", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+
+ const mockQuestion: QuestionField = {
+ id: "q1_number_of_siblings",
+ title: "Number of Siblings",
+ type: "number",
+ order: 1,
+ required: true,
+ isVisible: true,
+ description: "",
+ tooltip: "",
+ extras: { placeHolder: "e.g. 3", range: [0, 20] },
+ options: [],
+ ui_config: {},
+ };
+
+ it("should allow entering Persian digits without clearing input", () => {
+ render(
+
+
+
+
+ ,
+ );
+
+ const input = screen.getByPlaceholderText("e.g. 3") as HTMLInputElement;
+ fireEvent.change(input, { target: { value: "۴" } });
+
+ expect(input.value).toBe("4");
+ });
+
+ it("should allow entering English digits", () => {
+ render(
+
+
+
+
+ ,
+ );
+
+ const input = screen.getByPlaceholderText("e.g. 3") as HTMLInputElement;
+ fireEvent.change(input, { target: { value: "5" } });
+
+ expect(input.value).toBe("5");
+ });
+});
diff --git a/src/components/Componentes/question-number.tsx b/src/components/Componentes/question-number.tsx
index 5baf1bf..ddb07c6 100644
--- a/src/components/Componentes/question-number.tsx
+++ b/src/components/Componentes/question-number.tsx
@@ -18,6 +18,33 @@ type QuestionNumberProps = {
derivedFromQuestionIndex?: number;
};
+const PERSIAN_DIGITS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"];
+const ARABIC_DIGITS = ["٠", "١", "٢", "٣", "٤", "٥", "٦", "٧", "٨", "٩"];
+
+export function normalizeNumberString(val: string): string {
+ if (!val) return "";
+ let result = "";
+ for (let i = 0; i < val.length; i++) {
+ const char = val[i];
+ const pIdx = PERSIAN_DIGITS.indexOf(char);
+ if (pIdx !== -1) {
+ result += String(pIdx);
+ continue;
+ }
+ const aIdx = ARABIC_DIGITS.indexOf(char);
+ if (aIdx !== -1) {
+ result += String(aIdx);
+ continue;
+ }
+ if (char === "٫") {
+ result += ".";
+ continue;
+ }
+ result += char;
+ }
+ return result;
+}
+
const NUMBER_INPUT_PATTERN = /^-?\d*\.?\d*$/;
export default function QuestionNumber({
@@ -51,12 +78,17 @@ export default function QuestionNumber({
]);
useEffect(() => {
- if (
- typeof value === "string" &&
- value.length > 0 &&
- !NUMBER_INPUT_PATTERN.test(value)
- ) {
- setAnswerValue(question, null);
+ if (typeof value === "string" && value.length > 0) {
+ const normalized = normalizeNumberString(value);
+ if (!NUMBER_INPUT_PATTERN.test(normalized)) {
+ setAnswerValue(question, null);
+ } else if (normalized !== value) {
+ const parsed = parseFloat(normalized);
+ setAnswerValue(
+ question,
+ Number.isNaN(parsed) ? normalized : parsed,
+ );
+ }
}
}, [question, setAnswerValue, value]);
@@ -66,7 +98,7 @@ export default function QuestionNumber({
typeof value === "number"
? value
: typeof value === "string"
- ? parseFloat(value)
+ ? parseFloat(normalizeNumberString(value))
: NaN;
const isOutOfRange = useMemo(() => {
if (Number.isNaN(numValue)) return false;
@@ -76,8 +108,9 @@ export default function QuestionNumber({
}, [numValue, min, max]);
const rawInputValue = value == null ? "" : String(value);
- const inputValue = NUMBER_INPUT_PATTERN.test(rawInputValue)
- ? rawInputValue
+ const normalizedRaw = normalizeNumberString(rawInputValue);
+ const inputValue = NUMBER_INPUT_PATTERN.test(normalizedRaw)
+ ? normalizedRaw
: "";
const isMonthlyIncome = question.ui_config?.currency_enabled === true;
@@ -174,22 +207,24 @@ export default function QuestionNumber({
value={localTextValue}
onChange={(event) => {
const nextValue = event.target.value;
- const cleanValue = nextValue.replace(/,/g, "");
+ const normalized = normalizeNumberString(nextValue);
+ const cleanValue = normalized.replace(/,/g, "");
if (
cleanValue !== "" &&
+ cleanValue !== "-" &&
!NUMBER_INPUT_PATTERN.test(cleanValue)
) {
return;
}
const formatted = formatNumberWithCommas(cleanValue);
- const finalFormatted = nextValue.endsWith(".")
+ const finalFormatted = normalized.endsWith(".")
? `${formatted}.`
: formatted;
setLocalTextValue(finalFormatted);
- if (cleanValue === "") {
+ if (cleanValue === "" || cleanValue === "-") {
setAnswerValue(question, null);
} else {
const parsed = parseFloat(cleanValue);
@@ -278,29 +313,31 @@ export default function QuestionNumber({
>
{
- const nextValue = event.target.value;
- if (!NUMBER_INPUT_PATTERN.test(nextValue)) {
- event.currentTarget.value = inputValue;
+ const raw = event.target.value;
+ const normalized = normalizeNumberString(raw);
+ const cleaned = normalized.replace(/[^0-9.-]/g, "");
+
+ if (cleaned === "" || cleaned === "-") {
+ setAnswerValue(question, null);
return;
}
- if (nextValue === "") {
- setAnswerValue(question, null);
- } else {
- const parsed = parseFloat(nextValue);
- setAnswerValue(
- question,
- Number.isNaN(parsed) ? nextValue : parsed,
- );
+ if (!NUMBER_INPUT_PATTERN.test(cleaned)) {
+ return;
}
+
+ const parsed = parseFloat(cleaned);
+ setAnswerValue(
+ question,
+ Number.isNaN(parsed) ? cleaned : parsed,
+ );
}}
className={[
"h-[54px] w-full rounded-[16px] border px-4.5 text-[15px] font-medium text-[#181818] outline-none transition-all placeholder:text-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472]",