feat: add DigitInput component; force English digits for phone & national code
New reusable DigitInput normalizes Persian/Arabic digits to English on input, strips non-digits, enforces maxDigits, and is always LTR + numeric keyboard. Use it for the phone and national-code fields on the appointment create page (phone previously kept raw Persian digits; national code lost Persian digits to the \D strip). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import DigitInput from './DigitInput';
|
||||||
|
|
||||||
|
describe('DigitInput — ورودی عددی با تبدیل رقم فارسی', () => {
|
||||||
|
it('converts Persian/Arabic digits to English (happy path)', () => {
|
||||||
|
const onChange = vi.fn();
|
||||||
|
render(<DigitInput value="" onChange={onChange} placeholder="p" />);
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('p'), { target: { value: '۰۹۱۲٣٤' } });
|
||||||
|
expect(onChange).toHaveBeenCalledWith('091234');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips non-digit characters (error/garbage input)', () => {
|
||||||
|
const onChange = vi.fn();
|
||||||
|
render(<DigitInput value="" onChange={onChange} placeholder="p" />);
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('p'), { target: { value: '0a9 1-2b' } });
|
||||||
|
expect(onChange).toHaveBeenCalledWith('0912');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enforces maxDigits and handles empty input (boundary)', () => {
|
||||||
|
const onChange = vi.fn();
|
||||||
|
render(<DigitInput value="" onChange={onChange} maxDigits={10} placeholder="p" />);
|
||||||
|
const input = screen.getByPlaceholderText('p');
|
||||||
|
fireEvent.change(input, { target: { value: '۱۲۳۴۵۶۷۸۹۰۱۲' } });
|
||||||
|
expect(onChange).toHaveBeenCalledWith('1234567890');
|
||||||
|
expect(input).toHaveAttribute('dir', 'ltr');
|
||||||
|
expect(input).toHaveAttribute('inputmode', 'numeric');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { toEnglishDigits } from '../../lib/utils';
|
||||||
|
|
||||||
|
interface DigitInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'type'> {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
/** حداکثر تعداد رقم (مثلاً ۱۱ برای موبایل، ۱۰ برای کد ملی). */
|
||||||
|
maxDigits?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ورودیِ فقط-عددی که ارقام فارسی/عربی را همان لحظه به انگلیسی تبدیل میکند و هر
|
||||||
|
* کاراکتر غیررقمی را حذف میکند؛ همیشه LTR و کیبورد عددی. مناسب شماره تماس و کد ملی.
|
||||||
|
*/
|
||||||
|
export default function DigitInput({ value, onChange, maxDigits, ...rest }: DigitInputProps) {
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
let digits = toEnglishDigits(e.target.value).replace(/\D/g, '');
|
||||||
|
if (maxDigits) digits = digits.slice(0, maxDigits);
|
||||||
|
onChange(digits);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
{...rest}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
dir="ltr"
|
||||||
|
inputMode="numeric"
|
||||||
|
maxLength={maxDigits}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import type { ApiResponse } from '../lib/api';
|
|||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import PersianDateInput from '../components/ui/PersianDateInput';
|
import PersianDateInput from '../components/ui/PersianDateInput';
|
||||||
import PriceInput from '../components/ui/PriceInput';
|
import PriceInput from '../components/ui/PriceInput';
|
||||||
|
import DigitInput from '../components/ui/DigitInput';
|
||||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||||
import { WalletChargeLink } from '../components/AppointmentActions';
|
import { WalletChargeLink } from '../components/AppointmentActions';
|
||||||
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
|
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
|
||||||
@@ -218,8 +219,8 @@ export default function AppointmentCreatePage() {
|
|||||||
</div>
|
</div>
|
||||||
<label style={label}>کد ملی</label>
|
<label style={label}>کد ملی</label>
|
||||||
<div className="field" style={{ marginTop: 6, height: 44 }}>
|
<div className="field" style={{ marginTop: 6, height: 44 }}>
|
||||||
<input value={nationalCode} onChange={e => setNationalCode(e.target.value.replace(/\D/g, '').slice(0, 10))}
|
<DigitInput value={nationalCode} onChange={setNationalCode} maxDigits={10}
|
||||||
placeholder="کد ملی مراجعه کننده" dir="ltr" inputMode="numeric" maxLength={10} />
|
placeholder="کد ملی مراجعه کننده" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -246,14 +247,14 @@ export default function AppointmentCreatePage() {
|
|||||||
<div>
|
<div>
|
||||||
<label style={label}>شماره تماس</label>
|
<label style={label}>شماره تماس</label>
|
||||||
<div className="field" style={{ marginTop: 6, height: 44 }}>
|
<div className="field" style={{ marginTop: 6, height: 44 }}>
|
||||||
<input value={mobile} onChange={e => setMobile(e.target.value)} placeholder="شماره تماس مراجعه کننده" dir="ltr" />
|
<DigitInput value={mobile} onChange={setMobile} maxDigits={11} placeholder="شماره تماس مراجعه کننده" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={label}>کد ملی</label>
|
<label style={label}>کد ملی</label>
|
||||||
<div className="field" style={{ marginTop: 6, height: 44 }}>
|
<div className="field" style={{ marginTop: 6, height: 44 }}>
|
||||||
<input value={nationalCode} onChange={e => setNationalCode(e.target.value.replace(/\D/g, '').slice(0, 10))}
|
<DigitInput value={nationalCode} onChange={setNationalCode} maxDigits={10}
|
||||||
placeholder="کد ملی مراجعه کننده" dir="ltr" inputMode="numeric" maxLength={10} />
|
placeholder="کد ملی مراجعه کننده" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user