GrTimePicker

  • chrono
  • Дата и время
  • API · 28 props

Берут, когда время без даты.

import { GrTimePicker } from '@feugene/granularity-chrono/components/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 week
    Sent 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.

    00
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    00
    30
    Call me backBilling support · today, Tuesday 13 October
    Number
    +49 40 1234 5678
    Topic
    Invoice INV-2041
    Time
    14:30 – 15:00
    Calls are taken until 17:30, Berlin time.
  • Инцидент с точностью до секунды

    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

PropTypeпо умолчаниюОписание
modelValuev-modelTValue | undefinedundefinedВремя (`v-model`) — `Date`, у которого значимо время суток. Тип задаёт `valueAdapter`; `null` — ничего не выбрано.
valueAdapter
"date" | "isoDate" | "isoDateTime" | "timestamp" | GrChronoA"date" | "isoDate" | "isoDateTime" | "timestamp" | GrChronoAdapter<TValue> | undefined
undefinedКак значение уходит наружу и приходит обратно: имя готового адаптера (`date`, `isoDate`, `isoDateTime`, `timestamp`) либо свой.
minDate | undefinedundefinedНижняя граница. Учитывается только время суток.
maxDate | undefinedundefinedВерхняя граница. Учитывается только время суток.
minuteStepnumber | undefined1Шаг колонки минут в минутах.
secondStepnumber | undefined1Шаг колонки секунд в секундах.
enableSecondsboolean | undefinedfalseКолонка секунд в панели и секунды в поле. По умолчанию выключена.
use12Hoursboolean | undefinedundefined12-часовой вид. Не задан — из локали через `Intl`.
todayDate | undefinedundefinedДата, к которой привязывается выбранное время, когда значения ещё нет. Задаётся ради воспроизводимых тестов и снимков.
formatIntl.DateTimeFormatOptions | undefinedundefinedВид значения в поле — опциями `Intl`, а не строкой-паттерном.
placeholderstring | undefinedundefinedПодсказка в пустом поле.
editableboolean | undefinedfalseВремя можно набрать руками: `9:30`, `09:30:45`, `3:30 PM`. Маски здесь нет намеренно — разделитель у времени один, и дорисовывать нечего.
applyOnBlurboolean | undefinedtrueРазобранный текст уходит наружу на уходе фокуса, а не только по `Enter`.
clearableboolean | undefinedundefinedКнопка очистки в поле: снимает значение и шлёт `clear`. Не задан — из `GrConfigProvider`, иначе `false`.

Слоты

SlotTypeОписание
footer
{ select: (date: Date) => boolean; canSelect: (date: Date) ={ select: (date: Date) => boolean; canSelect: (date: Date) => boolean; close: () => void; }
Подвал панели: «сейчас», «очистить». Выбор отдаётся внутрь, как у пикеров дат: запрет складывается из `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

grTimePickerConfigconst

Пикер рендерит поповер ядра — ребро объявлено. Без него потребитель, выбравший только GrTimePicker, получил бы панель без фона, рамки и тени: granum подмешивает safelist и CSS лишь тем компонентам, что попали в селекцию, а GrPopover попадает в неё только через этот граф.

const grTimePickerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrTimePicker">;

Файлdist/types/components/GrTimePicker/defaults.d.ts

GrTimePickerConfigurablePropsinterface

Пропы GrTimePicker, настраиваемые глобально через <GrConfigProvider :component-defaults="{ GrTimePicker: { … } }">. Только оформление и поведение оболочки: границы времени задаются на месте.

interface GrTimePickerConfigurableProps {
    size: GrTimePickerSize;
    clearable: boolean;
    placement: UseFloatingPlacement;
}

Файлdist/types/components/GrTimePicker/grTimePickerStyles.d.ts

GrTimePickerSizetype
type GrTimePickerSize = GrPickerSize;

Файлdist/types/components/GrTimePicker/safelist.d.ts

grTimePickerSafelistconst
const grTimePickerSafelist: string[];

Файлdist/types/components/GrTimePicker/GrTimePicker.vue.d.ts

GrTimePickerPropsinterface

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;
}
GrTimePickerEmitsinterface
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 — та же тройка, что у пикеров дат: подвал у всех пикеров пакета устроен одинаково.

vue
<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 на стороне потребителя.