Files
clinicpro/assets/admin/components/ui/PersianDatePicker.tsx
T
hamedandClaude Opus 5 f06efe26c0 fix(holidays): honest dates and reachable fields on the holidays page
The table had two date columns for one date: "تاریخ" printed the raw
1405-05-13 string in Latin digits, and the column labelled "میلادی" ran the
same day through formatDate — which returns Jalali. One date, twice, under a
label that lied. It is now a single formatted Jalali column.

The closure form used a native <input type="date">: Gregorian, an English
mm/dd/yyyy placeholder in an RTL Persian panel, and a white box in dark mode
because a native control does not follow the theme. It is the shared Persian
picker now.

That picker turned out to be a div with an onClick — no role, no tab stop, no
accessible name, and its clear button was a span. Since every page that picks
a date goes through it, it gained role/tabIndex/Enter-Space, an ariaLabel
prop, and a real button for clear. The page passes labels for the year select
and both form fields, and the global topbar search got an aria-label, which
takes the runtime accessibility probe on this page to clean.

useHolidays now returns an error, so a failed request reads as an error
instead of an empty year — previously indistinguishable.

The page had no test file; it has eight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:25:56 +03:30

86 lines
3.5 KiB
TypeScript

import { useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { CalendarDaysIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { formatDate } from '../../lib/utils';
import PersianCalendar from './PersianCalendar';
interface Props {
value: string; // YYYY-MM-DD میلادی
onChange: (v: string) => void;
placeholder?: string;
height?: number;
minWidth?: number;
/** انتخاب سال→ماه→روز را فعال می‌کند (پیش‌فرض خاموش). */
enableYearPicker?: boolean;
/**
* نام دسترس‌پذیر. تریگر یک `div` است نه `input`، پس `<label htmlFor>` رویش نمی‌نشیند
* و بدون این، screen reader فقط «button» می‌شنود. پیش‌فرض روی placeholder می‌افتد.
*/
ariaLabel?: string;
}
export default function PersianDatePicker({ value, onChange, placeholder = 'انتخاب تاریخ', height = 38, minWidth = 148, enableYearPicker = false, ariaLabel }: Props) {
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLDivElement>(null);
const [pos, setPos] = useState<{ top: number; right: number } | null>(null);
// تقویم را به body پورت می‌کنیم تا داخل مودال‌های overflow-scroll زیر کادر نرود.
const toggle = () => {
const r = triggerRef.current?.getBoundingClientRect();
if (r) setPos({ top: r.bottom, right: window.innerWidth - r.right });
setOpen((o) => !o);
};
return (
<div ref={triggerRef} style={{ position: 'relative', display: 'inline-block' }}>
<div
role="button"
tabIndex={0}
aria-haspopup="dialog"
aria-expanded={open}
aria-label={ariaLabel ?? placeholder}
onClick={toggle}
// با موس باز می‌شد و با کیبورد نه؛ Enter/Space همان کاری را می‌کنند که کلیک.
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
}}
style={{
display: 'flex', alignItems: 'center', gap: 7,
height, padding: '0 10px', borderRadius: 'var(--r-sm)',
border: '1px solid var(--border)', background: 'var(--surface)',
cursor: 'pointer', fontSize: 13, color: value ? 'var(--text)' : 'var(--text-3)',
userSelect: 'none', minWidth, whiteSpace: 'nowrap',
}}
>
<CalendarDaysIcon style={{ width: 15, height: 15, color: 'var(--text-3)', flexShrink: 0 }} />
<span style={{ flex: 1 }}>{value ? formatDate(value) : placeholder}</span>
{value && (
<button
type="button"
aria-label="پاک کردن تاریخ"
onClick={(e) => { e.stopPropagation(); onChange(''); }}
style={{
display: 'flex', alignItems: 'center', cursor: 'pointer', color: 'var(--text-3)',
background: 'none', border: 0, padding: 0,
}}
>
<XMarkIcon style={{ width: 13, height: 13 }} />
</button>
)}
</div>
{open && pos && createPortal(
<div style={{ position: 'fixed', top: pos.top, right: pos.right, zIndex: 3000 }}>
<PersianCalendar
value={value}
onChange={(v) => { onChange(v); setOpen(false); }}
onClose={() => setOpen(false)}
enableYearPicker={enableYearPicker}
/>
</div>,
document.body,
)}
</div>
);
}