انتخابگر بازه تاریخ (DateRangePicker)

کامپوننت انتخاب بازه تاریخ با پشتیبانی از تقویم شمسی و میلادی

معرفی

کامپوننت Date Range Picker برای انتخاب بازه تاریخ (از تاریخ شروع تا تاریخ پایان) استفاده می‌شود. این کامپوننت از تقویم شمسی (جلالی) و میلادی پشتیبانی می‌کند و به طور کامل RTL است.

DateRangePicker تنها کنترل بازهٔ زمانی سیستم طراحی است: بازهٔ کل صفحه (period در DashboardPage و DetailPage)، فیلتر تاریخ نوارابزار و «از … تا»ی فرم. مانند Google Analytics، وقتی باز می‌شود کنار تقویم فهرست بازه‌های آماده را دارد و کاربر همچنان می‌تواند بازهٔ دلخواهش را روی تقویم انتخاب کند.

چه زمانی استفاده کنیم:

  • برای فیلتر کردن گزارش‌ها و داشبوردها بر اساس بازه زمانی دلخواه کاربر
  • در فرم‌هایی که تاریخ شروع و پایان به هم وابسته‌اند (مانند بازه اجرای کمپین تخفیف فصلی یا رزرو اقامت)
  • زمانی که کاربر باید بازه دقیقی را روی تقویم شمسی یا میلادی انتخاب کند و دیدن روزهای بین شروع و پایان به تصمیم او کمک می‌کند

چه زمانی استفاده نکنیم:

  • برای انتخاب فقط یک تاریخ (مانند تاریخ انتشار یک گزارش) — از DatePicker استفاده کنید
  • برای نمایش دائمی تقویم در صفحه بدون popover و دکمه trigger — از Calendar با mode="range" به صورت مستقیم استفاده کنید
  • برای پنجرهٔ زمانی یک کارت نمودار در سرِ همان کارت (actions در DashboardChart یا ChartCard) — از PeriodSelector استفاده کنید؛ این تنها جای PeriodSelector است و بازهٔ صفحه، نوارابزار و فیلترها همیشه DateRangePicker است

زمین بازی

با تغییر تنظیمات زیر، پیش‌نمایش زنده را مشاهده کنید.

زمین بازی
تنظیمات
محتوا
ظاهر
حالت
کد این نمونه به‌صورت خودکار قابل تولید نیست — برای کد آماده‌ی copy/paste به بخش «استفاده» در بالای صفحه مراجعه کنید.

استفاده پایه

import { DateRangePicker } from '@partodata/ui'
import { useState } from 'react'
import { DateRange } from 'react-day-picker'

export default function MyComponent() {
  const [dateRange, setDateRange] = useState<DateRange | undefined>()

  return (
    <DateRangePicker value={dateRange} onChange={setDateRange} label="بازه تاریخ" placeholder="انتخاب بازه تاریخ" />
  )
}

بازه‌های آماده (پیش‌فرض)

با باز شدن انتخابگر، فهرست بازه‌های آماده در سمت آغاز خط (در RTL سمت راست) کنار تقویم می‌آید و در موبایل بالای تقویم: «7 روز اخیر»، «14 روز اخیر»، «ماه گذشته» (30 روز)، «3 ماه گذشته»، «6 ماه گذشته»، «1 سال اخیر» و در آخر «بازهٔ دلخواه». بازه‌ها امروز را هم در بر دارند و هر دو سرشان نیمه‌شب محلی است؛ «1 سال اخیر» یک سال تقویم شمسی است.

preset: '30d'

import { DateRangePicker, resolveDateRangePreset, type DateRangeValue } from '@partodata/ui'

// صفحهٔ تازه با «ماه گذشته» شروع می‌شود، مگر محصول بازهٔ دیگری لازم داشته باشد.
const [period, setPeriod] = React.useState<DateRangeValue | undefined>(() => resolveDateRangePreset('30d'))

<DashboardPage title="داشبورد" period={<DateRangePicker value={period} onChange={setPeriod} />} />
  • انتخاب یک بازهٔ آماده آن را همان لحظه اعمال می‌کند (onChange صدا زده می‌شود) و پنجره را می‌بندد. با باز کردن دوباره، روزهای آن بازه روی تقویم دیده می‌شوند.
  • روی تقویم، کلیک اول شروع بازهٔ تازه را انتخاب می‌کند و پنجره باز می‌ماند؛ کلیک دوم پایان بازه را انتخاب می‌کند و پنجره بسته می‌شود. این رفتار بعد از یک بازهٔ قبلی هم همان است؛ تاریخ پایان قبلی وارد بازهٔ تازه نمی‌شود. برای بازهٔ یک‌روزه، همان روز را دو بار انتخاب کنید. انتخاب دستی فهرست را به «بازهٔ دلخواه» می‌برد.
  • تا وقتی یک بازهٔ آماده فعال است دکمه برچسب آن را نشان می‌دهد («3 ماه گذشته»)، وگرنه تاریخ‌ها را.

مدل مقدار: بازهٔ نسبی

مقدار همان DateRange قبلی است به‌اضافهٔ کلید بازهٔ آماده: { from, to, preset: '30d' } (نوع DateRangeValue). بازه‌ای که دستی انتخاب شود preset ندارد. کلید، فیلتر ذخیره‌شده یا پیوند اشتراکی را نسبی نگه می‌دارد: کلید را ذخیره کنید و هر بار نسبت به امروز حلش کنید — انتخابگر هم مقداری با preset را همیشه به تاریخ امروز نشان می‌دهد. کلیدها: '7d'، '14d'، '30d'، '90d'، '180d'، '1y'.

در انتخاب دستی، onChange در کلیک اول مقدار { from, to: undefined } و در کلیک دوم بازهٔ کامل را می‌دهد. اگر فقط بازهٔ کامل را برای دریافت داده لازم دارید، پیش از ارسال درخواست وجود to را بررسی کنید. بدون value، انتخابگر مقدار را خودش نگه می‌دارد؛ برای کنترل از بیرون، value و onChange را بدهید. برای پاک کردن مقدار کنترل‌شده، value={undefined} بدهید.

// در سرور (Server Component یا API): همان تابعی که انتخابگر به کار می‌برد.
import { DEFAULT_DATE_RANGE_PRESETS, resolveDateRangePreset } from '@partodata/ui/server'

const range = resolveDateRangePreset(saved.preset ?? '30d') // { from, to, preset }

فهرست خود صفحه، یا بدون فهرست

presets فهرست خود صفحه را می‌گیرد (DateRangePreset[]: key، برچسب هر زبان و تابع range(today)). presets={false} فقط با دلیل: مثلاً بازهٔ آیندهٔ یک کمپین در فرم، که هیچ بازهٔ آماده‌ای به کارش نمی‌آید چون همه به امروز ختم می‌شوند. دلیل را روی خودِ prop بنویسید؛ قاعدهٔ ESLint parto/date-range-control بدون آن هشدار می‌دهد:

<DateRangePicker
  value={campaign}
  onChange={setCampaign}
  minDate={new Date()}
  // بازهٔ آیندهٔ کمپین: همهٔ بازه‌های آماده به امروز ختم می‌شوند.
  presets={false}
/>

تقویم شمسی (پیش‌فرض)

از نسخهٔ 4.0 تقویم پیش‌فرض شمسی است؛ لازم نیست usePersianCalendar را بدهید (پیش از آن پیش‌فرض میلادی بود):

<DateRangePicker value={dateRange} onChange={setDateRange} label="بازه تاریخ شمسی" placeholder="انتخاب بازه تاریخ" />

DateTimePicker و Calendar هنوز به‌طور پیش‌فرض میلادی‌اند؛ برای آن‌ها usePersianCalendar را صریحاً بدهید.

تقویم میلادی

برای محصولی که تقویم میلادی لازم دارد، usePersianCalendar={false} بدهید. برچسب دکمه هم میلادی می‌شود:

<DateRangePicker value={dateRange} onChange={setDateRange} usePersianCalendar={false} placeholder="Select range" />

اگر fromYear/toYear را با سال میلادی می‌دهید، usePersianCalendar={false} را هم بدهید؛ این دو سال همیشه سال تقویم فعال‌اند (بخش «انتخابگر سال و ماه» در ادامه).

اندازه و عرض

دکمهٔ باز کننده روی همان نردبان اندازهٔ کنترل‌ها (xs تا xl) قرار دارد؛ پیش‌فرض md (38 پیکسل) است، اندازهٔ فیلد فرم، و در نوار ابزار، FilterBar و سرِ کارت‌ها و سربرگ‌ها ردیف آن را sm (30 پیکسل) می‌کند، تا با دکمه‌ها، ورودی‌ها و Select هم‌ردیف خود هم‌قد باشد. به‌طور پیش‌فرض دکمهٔ DateRangePicker تمام عرض ظرفش را با حداقل 256 پیکسل پر می‌کند، و دکمهٔ DateRangePickerInline به اندازهٔ متنش است با همان حداقل 256 پیکسل. در نوار ابزار و ردیف فیلتر با autoWidth عرض دکمه دقیقاً به اندازهٔ متنش می‌شود:

<div className="flex items-center gap-2">
  <Input size="xs" placeholder="جست‌وجو" className="w-60" />
  <DateRangePicker size="xs" autoWidth value={dateRange} onChange={setDateRange} />
  <Button size="xs" variant="default">
    اعمال
  </Button>
</div>

نام autoWidth است و نه block، چون جهتش برعکس است: Button به‌طور پیش‌فرض به اندازهٔ متنش است و block آن را تمام‌عرض می‌کند، اما این انتخابگرها به‌طور پیش‌فرض پهن‌اند و autoWidth آن‌ها را به اندازهٔ متن برمی‌گرداند.

ویژگی‌ها

تعداد ماه‌ها

می‌توانید تعداد ماه‌های نمایش داده شده را تغییر دهید:

<DateRangePicker numberOfMonths={1} value={dateRange} onChange={setDateRange} />

محدودیت تاریخ

می‌توانید حداقل و حداکثر تاریخ قابل انتخاب را مشخص کنید:

<DateRangePicker
  value={dateRange}
  onChange={setDateRange}
  minDate={new Date(2024, 0, 1)}
  maxDate={new Date(2024, 11, 31)}
/>

روزهای خارج از این بازه غیرفعال می‌شوند (قابل کلیک نیستند و onChange را صدا نمی‌زنند) و حرکت بین ماه‌ها هم به همین بازه محدود می‌شود.

تا نسخه 3.0.0 این دو prop بی‌اثر بودند

minDate/maxDate به fromDate/toDate تقویم منتقل می‌شدند، اما react-day-picker نسخه 9 این دو prop را حذف کرده است (نه تغییر نام). به همین دلیل محدودیت تاریخ در عمل اعمال نمی‌شد و کاربر می‌توانست هر تاریخی را انتخاب کند. اگر روی نسخه‌های قدیمی‌تر برای این محدودیت راه‌حل جانبی نوشته‌اید، اکنون می‌توانید آن را حذف کنید.

تقویم برچسب دکمه

متن روی دکمه‌ی trigger همیشه با همان تقویمی نوشته می‌شود که در پنل باز می‌شود: به‌طور پیش‌فرض تاریخ شمسی («1 فروردین - 1 اردیبهشت 1403») و با usePersianCalendar={false} تاریخ میلادی (Jan 01, 2024 - Jan 31, 2024). ارقام با حروف لاتین در DOM نوشته می‌شوند و فونت آن‌ها را با قابلیت ss01 به شکل فارسی نمایش می‌دهد؛ بنابراین کپی/جست‌وجو و خواندن با صفحه‌خوان سالم می‌ماند. DatePicker از نسخهٔ 4.0 همین برچسب را نشان می‌دهد.

انتخابگر سال و ماه

برای نمایش dropdown برای انتخاب سال و ماه، captionLayout="dropdown" بدهید. fromYear و toYear سال‌های تقویم فعال‌اند: در تقویم شمسی (پیش‌فرض) سال شمسی، و با usePersianCalendar={false} سال میلادی. فهرست سال‌ها از fromYear تا خود toYear است.

<DateRangePicker value={dateRange} onChange={setDateRange} captionLayout="dropdown" fromYear={1380} toYear={1410} />

سال میلادی روی تقویم شمسی

تا 3٫x پیش‌فرض میلادی بود، پس کدی مثل fromYear={2020} toYear={2030} بدون usePersianCalendar سال میلادی می‌داد. اکنون همین اعداد سال شمسی خوانده می‌شوند (سال 2020 شمسی) و تاریخ انتخاب‌شده و امروز در فهرست نیستند. در این کد usePersianCalendar={false} را اضافه کنید یا سال‌ها را شمسی بنویسید.

بازه‌های آماده بیرون از minDate / maxDate

بازهٔ آماده‌ای که پیش از minDate شروع یا پس از maxDate تمام شود در فهرست غیرفعال است (تقویم هم روزهایش را نمی‌پذیرد). مقایسه روزبه‌روز است، پس maxDate امروز (هر ساعتی) همهٔ بازه‌هایی را که امروز تمام می‌شوند باز می‌گذارد.

راهنمای استفاده

بکنید

  • از DateRangePicker برای هر بازهٔ زمانی استفاده کنید: بازهٔ صفحه (period)، فیلتر نوارابزار و «از … تا»ی فرم - بازه‌های آماده را روشن بگذارید و صفحهٔ تازه را با «ماه گذشته» (resolveDateRangePreset('30d')) شروع کنید - در نوارابزار صفحه (filters در ListPage) و در FormRow بدون label: نام فیلتر placeholder است و نام فیلد فرم برچسب ردیف؛ label فقط برای انتخابگری تنها بیرون از این دو - برای محدود کردن بازه قابل انتخاب، از minDate و maxDate استفاده کنید

نکنید

  • برای انتخاب فقط یک تاریخ از DateRangePicker استفاده نکنید — از DatePicker با mode="single" استفاده کنید - برای بازهٔ صفحه یا فیلتر از PeriodSelector یا DatePicker در حالت بازه استفاده نکنید؛ PeriodSelector فقط در سرِ یک کارت نمودار است - presets={false} را بی‌دلیل ننویسید - فراموش نکنید که مقادیر Date همیشه میلادی هستند، حتی وقتی تقویم شمسی فعال است

Props

Prop

Type

مثال‌های کاربردی

فرم رزرو هتل

export default function HotelBookingForm() {
  const [checkInOut, setCheckInOut] = useState<DateRange | undefined>()

  return (
    <form>
      <DateRangePicker
        value={checkInOut}
        onChange={setCheckInOut}
        label="تاریخ ورود و خروج"
        placeholder="انتخاب تاریخ"
        minDate={new Date()}
        numberOfMonths={2}
      />
    </form>
  )
}

فیلتر گزارش با تقویم شمسی

export default function ReportFilter() {
  const [reportRange, setReportRange] = useState<DateRange | undefined>()

  return (
    <div className="flex gap-4">
      <DateRangePicker
        value={reportRange}
        onChange={setReportRange}
        label="بازه گزارش"
        placeholder="از تاریخ - تا تاریخ"
        captionLayout="dropdown"
        fromYear={1400}
        toYear={1404}
      />
      <Button onClick={() => console.log(reportRange)}>مشاهده گزارش</Button>
    </div>
  )
}

نکات مهم

  1. تقویم شمسی: با تقویم شمسی (پیش‌فرض)، تاریخ‌ها به صورت شمسی نمایش داده می‌شوند اما مقادیر Date همچنان میلادی هستند.

  2. بسته شدن خودکار: کلیک اول روی تقویم شروع بازهٔ تازه را انتخاب می‌کند و پنجره باز می‌ماند؛ کلیک دوم پایان را انتخاب می‌کند و پنجره بسته می‌شود. انتخاب یک بازهٔ آماده با همان یک کلیک اعمال می‌شود و پنجره را می‌بندد.

  3. RTL: این کامپوننت به طور کامل از RTL پشتیبانی می‌کند و در محیط راست به چپ به درستی کار می‌کند.

  4. دسترسی: کامپوننت از استانداردهای دسترسی پیروی می‌کند و با کیبورد قابل استفاده است.

  5. بازه برای API: هر روزِ بازه نیمه‌شب محلی (00:00) آن روز است — from و to هر دو؛ یک روز تنها { from: d, to: d } است. APIای که با publishedAt <= to فیلتر می‌کند روز آخر را جا می‌اندازد (و بازهٔ یک‌روزه چیزی پیدا نمی‌کند)، پس from را همان‌طور و to را پایانِ روز آخرش بفرستید:

// روزهای انتخابگر 00:00 هستند: بازه تا پایانِ روز آخرش می‌رود، وگرنه API آن روز را جا می‌اندازد.
const endOfDay = (day: Date) => new Date(day.getFullYear(), day.getMonth(), day.getDate(), 23, 59, 59, 999)

const from = range?.from?.toISOString()
const to = range?.to && endOfDay(range.to).toISOString()

توابع کمکی

برای کار با تاریخ‌های شمسی، می‌توانید از توابع utility استفاده کنید:

import { formatPersianDateRange, toPersianDigits, getPersianMonthName } from '@partodata/ui'

const formattedRange = formatPersianDateRange(new Date(2024, 0, 1), new Date(2024, 0, 15))
// خروجی: "11 - 25 دی 1402"

دسترسی به توابع کمکی

formatPersianDateRange(from: Date, to: Date): string

تبدیل بازه تاریخ به رشته فارسی

toPersianDigits(num: number | string): string

تبدیل اعداد انگلیسی به فارسی

getPersianMonthName(date: Date): string

دریافت نام ماه شمسی

getPersianYear(date: Date): number

دریافت سال شمسی

jalaliToGregorian(year: number, month: number, day: number): Date

تبدیل تاریخ شمسی به میلادی

دسترسی‌پذیری

  • دکمه trigger یک <button> استاندارد است؛ با Enter/Space باز می‌شود و popover تقویم به‌صورت Radix Popover فوکس را trap می‌کند.
  • در تقویم: کلیدهای جهت (← → ↑ ↓) برای جابه‌جایی روز، Page Up/Page Down برای ماه، و Home/End برای ابتدا/انتهای هفته کار می‌کنند.
  • Enter تاریخ زیر فوکس را انتخاب می‌کند؛ پس از انتخاب هر دو تاریخ شروع و پایان، popover خودکار بسته می‌شود.
  • فهرست بازه‌های آماده یک radiogroup با نام «بازه‌های آماده» است: با Tab به گزینهٔ فعال می‌رسید، کلیدهای جهت بین بازه‌ها جابه‌جا می‌شوند (در انتها به ابتدا برمی‌گردند) و Enter یا Space بازه را انتخاب می‌کند؛ گزینهٔ فعال aria-checked="true" دارد.
  • Escape popover را می‌بندد و فوکس به دکمه trigger برمی‌گردد (بازهٔ انتخاب‌شده همان لحظه اعمال شده است).
  • روزهای انتخاب‌شده با aria-selected="true"، روزهای داخل بازه با aria-current متمایز می‌شوند، و بازه با data-range-middle markup می‌شود.
  • label به عنوان label قابل-دسترس به صورت بصری بالای trigger ظاهر می‌شود و screen reader هدف فیلد را اعلام می‌کند.
  • در حالت usePersianCalendar={true}، نام ماه/روز و اعداد به فارسی اعلام می‌شوند ولی مقدار Date همچنان میلادی است — بنابراین i18n value معنادار باقی می‌ماند.

کامپوننت‌های مرتبط

  • DatePicker — اگر نیاز به انتخاب یک تاریخ (نه بازه) دارید، از DatePicker با mode="single" استفاده کنید
  • PeriodSelector — فقط تغییر فشردهٔ پنجرهٔ زمانی در سرِ یک کارت نمودار؛ بازهٔ صفحه، نوارابزار و فیلترها DateRangePicker است
  • Calendar — اگر نیاز به نمایش تقویم بدون popover و input دارید، از Calendar مستقیماً استفاده کنید