GrTimePicker
Берут, когда время без даты.
Обзор
Когда брать
- время без даты — начало смены, часы работы, напоминание: день здесь либо не важен, либо задан отдельно;
- время удобнее набрать —
editableдаёт ввод с клавиатуры, разбор идёт по локали, включая 12-часовой вид там, где он принят; - нужны секунды —
enableSecondsдобавляет третью колонку; - шаг не минутный —
minuteStepстроит колонку с нужным шагом, и «14:07» там, где приём идёт по получасам, набрать уже нельзя.
Когда взять другое
| Нужно | Берите |
|---|---|
| Дата вместе со временем | GrDateTimePicker |
| Только дата | GrDatePicker |
| Период «с — по» по датам | GrDateRangePicker |
| Ввести длительность, а не момент на часах | GrNumberInput |
| Показать длительность | GrDuration |
| Несколько готовых слотов на выбор | GrSelect |
Длительность — не время. «Полтора часа» это число с единицей, а не момент:
колонки часов и минут для неё врут, потому что 90 минут в них не набираются.
Вводят её числом, а показывают GrDuration.
Примеры 3 у каждого — живое демо и его код
Часы работы — строками самого API
API магазина хранит часы строкой
'08:00', поэтому маленькийvalue-adapterпревращает её вDateдля пикера и обратно — день из компонента не выходит.minute-stepв 15 минут держит часы на сетке, которую показывает карта,editableпозволяет менеджеру набрать «7:45», аminначинает колонку закрытия через полчаса после открытия.Opening hours · Kiosk AltonaShown on the map and in the app · local time, Europe/Berlin- Closed
Open 63 h 30 min a weekSent to the shop API{ "mon": ["08:00", "19:00"], "tue": ["08:00", "19:00"], "wed": ["08:00", "19:00"], "thu": ["08:00", "20:30"], "fri": ["08:00", "19:00"], "sat": ["09:00", "16:00"], "sun": null }Обратный звонок в ближайший слот
Время — единственный вопрос на этом шаге, поэтому список остаётся открытым (
inline).min— ближайший свободный слот,max— последний: прошедшее и нерабочее время не предлагается вовсе. Подвал получаетselectиcanSelect, и «As soon as possible» проходит те же правила и округляется вверх до сетки — 11:10 становится 11:30.0001020304050607080910111213141516171819202122230030Call me backBilling support · today, Tuesday 13 October- Number
- +49 40 1234 5678
- Topic
- Invoice INV-2041
- Time
- 14:30 – 15:00
Инцидент с точностью до секунды
enable-secondsдобавляет третью колонку и секунды в поле,editableпринимает время, вставленное из логов. Значение — ISO-строка (value-adapter="isoDateTime") с днём изtoday,use-12-hoursследует переключателю часов, «Now» в подвале встаёт на сетку как любой другой выбор, аGrDurationпревращает шаги в цифры, которых ждёт разбор.INC-2041 · Payments webhooks delayedTimeline · today, 13 October- Time to acknowledge
- Time to resolve
- Still open
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
modelValuev-model | TValue | undefined | undefined | Время (`v-model`) — `Date`, у которого значимо время суток. Тип задаёт `valueAdapter`; `null` — ничего не выбрано. |
valueAdapter |
| undefined | Как значение уходит наружу и приходит обратно: имя готового адаптера (`date`, `isoDate`, `isoDateTime`, `timestamp`) либо свой. |
min | Date | undefined | undefined | Нижняя граница. Учитывается только время суток. |
max | Date | undefined | undefined | Верхняя граница. Учитывается только время суток. |
minuteStep | number | undefined | 1 | Шаг колонки минут в минутах. |
secondStep | number | undefined | 1 | Шаг колонки секунд в секундах. |
enableSeconds | boolean | undefined | false | Колонка секунд в панели и секунды в поле. По умолчанию выключена. |
use12Hours | boolean | undefined | undefined | 12-часовой вид. Не задан — из локали через `Intl`. |
today | Date | undefined | undefined | Дата, к которой привязывается выбранное время, когда значения ещё нет. Задаётся ради воспроизводимых тестов и снимков. |
format | Intl.DateTimeFormatOptions | undefined | undefined | Вид значения в поле — опциями `Intl`, а не строкой-паттерном. |
placeholder | string | undefined | undefined | Подсказка в пустом поле. |
editable | boolean | undefined | false | Время можно набрать руками: `9:30`, `09:30:45`, `3:30 PM`. Маски здесь нет намеренно — разделитель у времени один, и дорисовывать нечего. |
applyOnBlur | boolean | undefined | true | Разобранный текст уходит наружу на уходе фокуса, а не только по `Enter`. |
clearable | boolean | undefined | undefined | Кнопка очистки в поле: снимает значение и шлёт `clear`. Не задан — из `GrConfigProvider`, иначе `false`. |
openv-model | boolean | undefined | undefined | Контролируемое состояние панели (`v-model:open`). |
inline | boolean | undefined | false | Панель рисуется на месте: ни поля, ни поповера. Модель, адаптер и `name` остаются пикеровскими — этим `inline` и отличается от голого `GrCalendar`, который говорит кортежами. |
placement | Placement | undefined | undefined | Сторона поля, у которой раскрывается панель. Не задан — из `GrConfigProvider`, иначе `bottom-start`. |
teleportTo | string | HTMLElement | undefined | undefined | Точечное переопределение точки монтирования панели. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Высота и кегль поля по шкале контролов. По умолчанию `md`; берётся из `GrConfigProvider`. |
locale | string | undefined | undefined | Локаль показа. Не задана — из адаптера i18n приложения. |
id | string | undefined | undefined | Собственный `id` поля. Не задан — берётся из `GrFormField`. |
name | string | undefined | undefined | Имя для нативной формы: сериализованное значение уходит скрытым полем. |
disabled | boolean | undefined | false | Поле недоступно: панель не открывается, значение не меняется. Наследуется от `GrFormField`. |
readonly | boolean | undefined | false | Значение видно, панель открывается, но выбор не меняется. |
invalid | boolean | undefined | false | Визуальное и ARIA-состояние ошибки (`aria-invalid`). Наследуется от `GrFormField`. |
required | boolean | undefined | false | Обязательное поле (`aria-required`). Наследуется от `GrFormField`. |
loading | boolean | undefined | false | Фоновая работа: спиннер в поле и `aria-busy`. |
ariaLabel | string | undefined | undefined | Доступное имя поля, когда видимой подписи нет. Внутри `GrFormField` имя даёт его `label`. |
Слоты
| Slot | Type | Описание |
|---|---|---|
footer |
| Подвал панели: «сейчас», «очистить». Выбор отдаётся внутрь, как у пикеров дат: запрет складывается из `min`, `max` и шага, а `readonly` запрещает выбор целиком — снаружи этих правил не видно. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (e: "update:modelValue", value: TValue | null): void | Новое значение (`v-model`); `null` — значение снято. |
change | (e: "change", value: TValue | null): void | Значение зафиксировано: выбором в панели, вводом или очисткой. |
update:open | (e: "update:open", value: boolean): void | Панель открылась или закрылась (`v-model:open`). |
clear | (e: "clear"): void | Значение снято кнопкой очистки; только при `clearable`. |
focus | (e: "focus", event: FocusEvent): void | Фокус вошёл в поле. |
blur | (e: "blur", event: FocusEvent): void | Фокус ушёл из поля. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrTimePickerConfigurableProps, GrTimePickerSize, GrTimePickerProps } from '@feugene/granularity-chrono/components/GrTimePicker'Файлdist/types/components/GrTimePicker/config.d.ts
Пикер рендерит поповер ядра — ребро объявлено. Без него потребитель,
выбравший только GrTimePicker, получил бы панель без фона, рамки и тени:
granum подмешивает safelist и CSS лишь тем компонентам, что попали в
селекцию, а GrPopover попадает в неё только через этот граф.
const grTimePickerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrTimePicker">;Файлdist/types/components/GrTimePicker/defaults.d.ts
Пропы GrTimePicker, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrTimePicker: { … } }">.
Только оформление и поведение оболочки: границы времени задаются на месте.
interface GrTimePickerConfigurableProps {
size: GrTimePickerSize;
clearable: boolean;
placement: UseFloatingPlacement;
}Файлdist/types/components/GrTimePicker/grTimePickerStyles.d.ts
type GrTimePickerSize = GrPickerSize;Файлdist/types/components/GrTimePicker/safelist.d.ts
const grTimePickerSafelist: string[];Файлdist/types/components/GrTimePicker/GrTimePicker.vue.d.ts
GrTimePicker — поле со временем и панелью из колонок.
Колонки, а не поля ввода: набирать «14:30» с клавиатуры — это текстовый ввод,
который в этой версии пакета не поддерживается вовсе, а листать значения
стрелками можно и здесь. Каждая колонка — самостоятельный листбокс со своим
aria-activedescendant, как в GrSelect.
Панель по выбору **не закрывается**: время набирается за несколько шагов —
час, минута, иногда секунда и период. Закрытие остаётся за Esc, кликом вне и
v-model:open.
interface GrTimePickerProps<T = Date | null> {
/**
* Время (`v-model`) — `Date`, у которого значимо время суток. Тип задаёт
* `valueAdapter`; `null` — ничего не выбрано.
*/
modelValue?: T;
/**
* Как значение уходит наружу и приходит обратно: имя готового адаптера
* (`date`, `isoDate`, `isoDateTime`, `timestamp`) либо свой.
*/
valueAdapter?: GrChronoAdapterName | GrChronoAdapter<T>;
/** Нижняя граница. Учитывается только время суток. */
min?: Date;
/** Верхняя граница. Учитывается только время суток. */
max?: Date;
/** Шаг колонки минут в минутах. */
minuteStep?: number;
/** Шаг колонки секунд в секундах. */
secondStep?: number;
/** Колонка секунд в панели и секунды в поле. По умолчанию выключена. */
enableSeconds?: boolean;
/** 12-часовой вид. Не задан — из локали через `Intl`. */
use12Hours?: boolean;
/**
* Дата, к которой привязывается выбранное время, когда значения ещё нет.
* Задаётся ради воспроизводимых тестов и снимков.
*/
today?: Date;
/** Вид значения в поле — опциями `Intl`, а не строкой-паттерном. */
format?: Intl.DateTimeFormatOptions;
/** Подсказка в пустом поле. */
placeholder?: string;
/**
* Время можно набрать руками: `9:30`, `09:30:45`, `3:30 PM`. Маски здесь нет
* намеренно — разделитель у времени один, и дорисовывать нечего.
*/
editable?: boolean;
/** Разобранный текст уходит наружу на уходе фокуса, а не только по `Enter`. */
applyOnBlur?: boolean;
/**
* Кнопка очистки в поле: снимает значение и шлёт `clear`. Не задан — из
* `GrConfigProvider`, иначе `false`.
*/
clearable?: boolean;
/** Контролируемое состояние панели (`v-model:open`). */
open?: boolean;
/**
* Панель рисуется на месте: ни поля, ни поповера. Модель, адаптер и `name`
* остаются пикеровскими — этим `inline` и отличается от голого `GrCalendar`,
* который говорит кортежами.
*/
inline?: boolean;
/**
* Сторона поля, у которой раскрывается панель. Не задан — из `GrConfigProvider`,
* иначе `bottom-start`.
*/
placement?: UseFloatingPlacement;
/** Точечное переопределение точки монтирования панели. */
teleportTo?: string | HTMLElement;
/** Высота и кегль поля по шкале контролов. По умолчанию `md`; берётся из `GrConfigProvider`. */
size?: GrTimePickerSize;
/** Локаль показа. Не задана — из адаптера i18n приложения. */
locale?: string;
/** Собственный `id` поля. Не задан — берётся из `GrFormField`. */
id?: string;
/** Имя для нативной формы: сериализованное значение уходит скрытым полем. */
name?: string;
/** Поле недоступно: панель не открывается, значение не меняется. Наследуется от `GrFormField`. */
disabled?: boolean;
/** Значение видно, панель открывается, но выбор не меняется. */
readonly?: boolean;
/** Визуальное и ARIA-состояние ошибки (`aria-invalid`). Наследуется от `GrFormField`. */
invalid?: boolean;
/** Обязательное поле (`aria-required`). Наследуется от `GrFormField`. */
required?: boolean;
/** Фоновая работа: спиннер в поле и `aria-busy`. */
loading?: boolean;
/** Доступное имя поля, когда видимой подписи нет. Внутри `GrFormField` имя даёт его `label`. */
ariaLabel?: string;
}interface GrTimePickerEmits<T = Date | null> {
/** Новое значение (`v-model`); `null` — значение снято. */
(e: 'update:modelValue', value: T | null): void;
/** Значение зафиксировано: выбором в панели, вводом или очисткой. */
(e: 'change', value: T | null): void;
/** Панель открылась или закрылась (`v-model:open`). */
(e: 'update:open', value: boolean): void;
/** Значение снято кнопкой очистки; только при `clearable`. */
(e: 'clear'): void;
/** Фокус вошёл в поле. */
(e: 'focus', event: FocusEvent): void;
/** Фокус ушёл из поля. */
(e: 'blur', event: FocusEvent): void;
}Использование из прозы библиотеки
Панель не закрывается по выбору
Час, минута и период — три отдельных решения, и закрытие после первого из них означало бы, что пользователь выбрал время, которого он не выбирал. Это и есть причина, по которой панель времени ведёт себя не как календарь.
Колонки показывают выбранное и двигаются сами
С панелью или inline каждая колонка встаёт выбранным значением по центру —
сразу после монтирования, а не по первому действию: в inline панель никто не
открывает, и колонка часов иначе осталась бы на 00–05. Без значения колонка
встаёт на первое доступное — при min в 11:30 это 11, а не запрещённый ноль.
Прокручивается только сама колонка, её scrollTop. scrollIntoView здесь не
годится: он двигает все прокручиваемые предки вплоть до окна, и пикер ниже
первого экрана утаскивал бы к себе страницу на загрузке и гидрации. Шаг стрелкой
доводит курсор до ближнего края колонки, тоже не трогая страницу.
Подвал раздаёт выбор внутрь
Слот footer получает select, canSelect и close — та же тройка, что у пикеров дат: подвал у
всех пикеров пакета устроен одинаково.
<GrTimePicker v-model="time" :minute-step="15">
<template #footer="{ select, canSelect }">
<GrButton size="sm" variant="ghost" :disabled="!canSelect(new Date())" @click="select(new Date())">
Сейчас
</GrButton>
</template>
</GrTimePicker>Время встаёт на сетку шага округлением вверх. 14:37 при шаге в 15 минут даёт 14:45, а не 14:30: время в пикере почти всегда значит «начиная с этого момента» — запись, бронь, напоминание, — и округлённое вниз уже прошло. Этим «сейчас» отличается от колонок, где показ существующего значения округляется вниз: там задача обратная — не выдумать отметку, которой в колонке нет.
Побеждает самая крупная объявленная ступень, мелкие обнуляются: при minuteStep: 15 получится
14:45:00, а не 14:45:37. Секунды выключены — их не будет и в значении, иначе в модель уехало бы то,
чего на экране не было.
Границы считаются после округления, а не до. При max в 14:40 и шаге 15 минут «сейчас» в 14:37
округляется до 14:45 — уже за границей, и canSelect отдаёт false. Кнопка приходит выключенной, а
не молча ничего не делает: проверка до округления пропустила бы её.
readonly и disabled запрещают выбор целиком — снаружи этих правил не видно, поэтому подвал и
спрашивает canSelect, а не считает сам.
Значение остаётся Date
Даже когда дата не важна, наружу уходит полноценный момент — с датой из today
или из текущего значения. Отдельного типа «время суток» в модели нет: он
потребовал бы своей арифметики и своего адаптера ради одного компонента.
Нужна строка '14:30' — это valueAdapter на стороне потребителя.
Пропы этого примера