Перейти до вмісту
tatetUI

DateRangePicker

Період із пресетами над таблицею чи звітом — два місяці на десктопі, один на телефоні.

«Період: 01.08 — 17.08» над таблицею чи звітом. Найчастіший фільтр у CRM і єдиний, де користувач майже ніколи не хоче гортати календар: у дев'яти випадках із десяти йому потрібні «останні 30 днів».

Пресети ліворуч, а на телефоні — смугою над сіткою

Обрано
1 серп. 2026 р. — 17 серп. 2026 р.
Останній пресет
—

Об'єкт, а не кортеж

v-model — це { start, end } | null. Кортеж [Date, Date] компактніший, але range[0] у шаблоні не читається, а range.start читається. Конверсія до кортежа, з яким працює UiCalendar, — один рядок в обидва боки.

Половинчастого стану не буває: поки другий кінець невідомий, модель лишається попередньою. Тому v-if="period" завжди чесний, і споживачу не доводиться відповідати на питання «чи вважати порожнім об'єкт, у якого заповнено лише start».

Пресети рахуються від «сьогодні»

DateRangePreset.range — це функція від сьогоднішньої дати, а не готовий кортеж. Пресети, обчислені під час імпорту модуля, назавжди застигли б на даті збірки; на прередереному сайті «Останні 7 днів» стали б неправильними вже наступного дня.

Пресет, який не влазить у min/max, зі списку зникає. Запропонувати «Останні 30 днів», коли min — десять днів тому, означає запропонувати період, який сам календар відхилить.

Подія presetSelect існує окремо від update:modelValue, бо з самого значення факт «користувач обрав пресет» не відновити: вручну зведений однаковий період виглядає так само.

Два місяці — рішенням JS, а не класом

Кількість місяців перемикає matchMedia, а не md:hidden.

Прихований класом другий місяць лишається в DOM. Це 42 зайві gridcell для скрінрідера і — головне — другий елемент із tabindex="0", який руйнує roving tabindex першого: Tab починає зупинятись у календарі двічі.

Типове значення — один місяць. Прередер віддає мобільний варіант, і найвужчий клієнт отримує розмітку без стрибка після гідрації.

Нижче md пресети стають горизонтальною смугою над сіткою: колонка пресетів (≈160px) поруч із місяцем (≈300px) не влазить у 375px екрана. Смуга — ряд пігулок із межею, що прокручується вбік і виходить під краї панелі: обрізаний край читається як «далі є ще». Ширину панелі обмежує вікно — без цього max-content смуги (усі пресети в рядок) робив панель 690px завширшки на 375px телефоні.

Значення у формі

З name компонент кладе у форму прихований інпут зі значенням 2026-09-24/2026-09-30 — календарні дні за локальним часом. toISOString() тут не годиться: він переводить локальну опівніч в UTC, і для будь-якого поясу на схід від Гринвіча дата з'їжджає на вчорашню — сервер, що бере перші десять символів, отримав би 2026-09-23.

API

Props

НазваТипТиповоОпис
modelValueDateRangenullОбраний період або `null`. Через `v-model`. Об'єкт, а не кортеж: `range.start` у шаблоні читається, `range[0]` — ні. Половинчастого стану тут не буває: поки другий кінець невідомий, модель лишається попередньою.
presetsDateRangePreset[]defaultDateRangePresetsПресети ліворуч від календаря. Порожній масив ховає колонку.
displayFormatshortlong"short"Формат періоду в тригері.
placementbottom-startbottom-endtop-starttop-end"bottom-start"Куди відкривати панель відносно поля.
weekStartsOn0 | 11Перший день тижня. `1` — понеділок (uk).
localestring"uk-UA"Локаль підписів.
sizeFieldSize"md"Висота поля. На мобільному кожен розмір вищий за десктопний.
placeholderstring"Оберіть період"—
minDate—Найраніша доступна дата включно.
maxDate—Найпізніша доступна дата включно.
disabledDate(date: Date) => boolean—Які дати недоступні. Проксується в `UiCalendar`.
todayDate—Що вважати «сьогодні» — для детермінізму прередеру й тестів.
errorstring—Текст помилки. Стан помилки вмикає САМА наявність тексту.
hintstring—Підказка під полем. Ховається, коли показано помилку.
labelstring——
idstring——
namestring—Ім'я поля форми. Значення — локальні дати `YYYY-MM-DD/YYYY-MM-DD`.
clearablebooleantrueХрестик, що скидає період.
autoClosebooleantrueЗакривати панель, щойно обрано обидва кінці.
disabledboolean——
requiredboolean——

Події

НазваPayloadОпис
update:modelValue[value: DateRange]Обраний період або `null`. Використовуйте через `v-model`.
presetSelect[preset: DateRangePreset]Обрано готовий пресет. Із самого значення це відновити неможливо - вручну зведений однаковий період виглядає так само.
open[]Панель відкрито.
close[]Панель закрито.

Слоти

НазваОпис
triggerВласний тригер. `text` — уже відформатований період або плейсхолдер.
presetВласний рендер пресету.
footerРядок під календарем: «Скасувати», «Застосувати».

Доступно через ref

НазваТипОпис
open() => voidВідкриває панель.
close(returnFocus?: boolean) => voidЗакриває панель.
focus() => voidСтавить фокус на тригер.

Коли використовувати

  • Фільтр періоду над таблицею, звітом або дашбордом.
  • Будь-де, де «останні 30 днів» — типова відповідь.
  • Порівняння періодів, якщо потрібні два таких поля поруч.

Коли НЕ використовувати

  • Для однієї дати — UiDatePicker або UiCalendar у режимі single.
  • Коли календар має бути видимий завжди — беріть UiCalendar напряму.
  • Коли періодів лише кілька фіксованих: тоді це UiToggleGroup без календаря взагалі.

Доступність

Тригер — кнопка з aria-haspopup="dialog", aria-expanded і aria-controls. Панель має role="dialog" і власну назву.

Пастки фокуса тут немає навмисно. Панель немодальна, тієї самої родини, що UiPopover і UiMenu; пастка зламала б Tab до наступного поля форми. Замість неї — закриття по виходу фокуса за межі тригера й панелі, по кліку зовні та по Escape із поверненням фокуса на тригер.

Відкриття переносить фокус у календар — на обрану дату, початок діапазону чи сьогодні. Без цього панель була недосяжна з клавіатури: вона телепортована в кінець <body>, тож Tab із тригера вів до наступного поля форми, а вихід фокуса закривав панель. На краях панелі Tab повертає фокус на тригер: Shift+Tab із першого елемента зупиняється на ньому, а Tab з останнього йде далі вже від тригера — до наступного поля, а не за межі сторінки. Після вибору періоду чи пресету фокус так само повертається на тригер.

На телефоні пресети — смуга, що гортається вбік. Tab докручує її до пресету цілим: браузер докручує лише пресет, схований повністю, а частково видимий лишав обрізаним краєм панелі. Тап смугу не зсуває — пресет не тікає з-під пальця.

Escape обробляється на самому компоненті зі stopPropagation. Слухач на document не міг зупинити слухача UiModal на тому ж document, і всередині модалки один Escape закривав і панель, і модалку.

Z-index береться з getOverlayChildZIndex, тож панель коректно лягає поверх модалки, якщо фільтр живе всередині неї.

Уся клавіатура сітки — з UiCalendar.