TimePickerDropdown

Обёртка над TimePicker с Dropdown: произвольный триггер (children), управление открытием, позиционирование и опциональное закрытие после Apply. Подходит для полей «время открытия», фильтров и компактных форм.

Когда использовать

  • Нужна кнопка или кастомный триггер, а панель времени показывается по клику или ховеру.
  • Требуется связать открытие с другим UI (например, контролируемый open).

Когда не нужен: для постоянно видимого времени в форме без попапа используйте TimePicker.

  • ✅ Задавать placement и trigger в зависимости от layout и UX‑требований.
  • ❌ Смешивать управляемый open с лишними внешними кнопками, которые дублируют открытие — проще контролировать только через onOpenChange.

Анатомия

Триггер и открытие

triggerclick | hover | focus и др. (см. пропсы Dropdown в таблице). closeOnApply закрывает панель после подтверждения, если сценарий это предполагает.

Позиционирование

placement и fallbackPlacements наследуются из @cloud-ru/ds-dropdown — подбирайте, чтобы панель не обрезалась у края экрана.

Визуал времени

Макет барабана времени совпадает с TimePicker; см. узел Figma ниже.

Установка

pnpm add @cloud-ru/ds-calendar @cloud-ru/ds-button
import { Button } from '@cloud-ru/ds-button'
import { TimePickerDropdown, SIZE } from '@cloud-ru/ds-calendar'

Примеры использования

Клик по кнопке

Клик по кнопкеcloseOnApply закрывает после выбора
tsx
import { Button } from '@cloud-ru/ds-button';
import { SIZE, TimePickerDropdown, TimeValue } from '@cloud-ru/ds-calendar';
import { useState } from 'react';

export function TimePickerDropdownBasic() {
  const [value, setValue] = useState<TimeValue | undefined>({ hours: 10, minutes: 5, seconds: 0 });

  return (
    <TimePickerDropdown
      closeOnApply
      fitToContainer={false}
      placement='bottom-start'
      size={SIZE.M}
      trigger='click'
      value={value}
      onChangeValue={v => setValue(v)}
    >
      <Button label='Выбрать время' />
    </TimePickerDropdown>
  );
}

Контролируемое open

Контролируемое open
tsx
import { Button } from '@cloud-ru/ds-button';
import { SIZE, TimePickerDropdown, TimeValue } from '@cloud-ru/ds-calendar';
import { useState } from 'react';

export function TimePickerDropdownControlled() {
  const [open, setOpen] = useState(false);
  const [value, setValue] = useState<TimeValue | undefined>({ hours: 14, minutes: 0, seconds: 0 });

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
      <TimePickerDropdown
        fitToContainer={false}
        open={open}
        placement='bottom-start'
        showSeconds={false}
        size={SIZE.S}
        trigger='click'
        value={value}
        onChangeValue={v => setValue(v)}
        onOpenChange={setOpen}
      >
        <Button label='Время (controlled open)' />
      </TimePickerDropdown>
      <span style={{ fontSize: 14, opacity: 0.8 }}>Панель времени: {open ? 'открыта' : 'закрыта'}</span>
    </div>
  );
}

Разные placement

Разные placement
tsx
import { Button } from '@cloud-ru/ds-button';
import { SIZE, TimePickerDropdown, TimeValue } from '@cloud-ru/ds-calendar';
import { useState } from 'react';

export function TimePickerDropdownPlacement() {
  const [value, setValue] = useState<TimeValue | undefined>({ hours: 7, minutes: 30, seconds: 0 });

  return (
    <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'flex-start' }}>
      <TimePickerDropdown
        closeOnApply
        fitToContainer={false}
        placement='bottom-start'
        size={SIZE.M}
        trigger='click'
        value={value}
        onChangeValue={v => setValue(v)}
      >
        <Button label='bottom-start' />
      </TimePickerDropdown>
      <TimePickerDropdown
        closeOnApply
        fitToContainer={false}
        placement='top-end'
        size={SIZE.M}
        trigger='click'
        value={value}
        onChangeValue={v => setValue(v)}
      >
        <Button label='top-end' />
      </TimePickerDropdown>
    </div>
  );
}

Props

Types

PropsTimePickerDropdownProps
PropTypeDefaultRequiredDescription
childrenReactNodenoКонтент триггера открытия dropdown
classNamestringnoCSS-класс контейнера
closeOnApplybooleannoЗакрыть dropdown после нажатия кнопки Apply
closeOnEscapeKeybooleantruenoЗакрывать ли по нажатию на кнопку `Esc`
closeOnPopstatebooleannoЗакрывать ли поповер при переходе по истории браузера
data-test-idstringno
defaultValueTimeValuenoЗначение по-умолчанию для uncontrolled.
disableSpanWrapperbooleannoОтключает для `isValidElement` внешнюю обертку триггера <br/> Пригодится для элементов с `position: absolute` <br/> Работает для триггеров, которые умеют отдать свою DOM-ноду: нативные элементы, `forwardRef`-компоненты и компоненты, помеченные `withInnerRefSupport` из `@cloud-ru/ds-utils`. Остальные всё равно получают `<span>` — без ноды поповеру не от чего считать позицию; в dev-режиме об этом печатается предупреждение.
fallbackPlacementsPlacement[]noЦепочка расположений которая будет применяться к поповеру от первого к последнему если при текущем он не влезает.
fitToContainerbooleantruenoОтключает предустановленный размер, заставляя компонент подстраиваться к размеру контейнра: (width: 100%, height: 100%).
hoverDelayClosenumbernoЗадержка закрытия по ховеру
hoverDelayOpennumbernoЗадержка открытия по ховеру
navigationStartRefRefObject<{ focus(): void; }>noСсылка на управление первым элементом навигации
onApply(() => void)noКолбек по нажатию Apply
onChangeValue((value?: TimeValue) => void)noКолбек выбора значения
onCurrent(() => void)noКолбек по нажатию Current
onFocusLeave((direction: FocusDirection) => void)noКолбек потери фокуса. Вызывается со значением `next`, когда фокус покидает компонент, передвигаясь вперед, по клавише `tab`. Со значением `prev` - по клавише стрелки вверх или `shift + tab`.
onOpenChange((isOpen: boolean) => void)noКолбек отображения компонента. Срабатывает при изменении состояния open.
openbooleannoУправляет состоянием показан/не показан.
outsideClickboolean | OutsideClickHandlernoЗакрывать ли при клике вне поповера
placement"bottom" | "bottom-end" | "bottom-start" | "left" | "left-end" | "left-start" | "right" | "right-end" | "right-start" | "top" | "top-end" | "top-start"topnoПоложение поповера относительно своего триггера (children).
showSecondsbooleannoПоказывать ли секунды
size"l" | "m" | "s"mnoРазмер
todaynumber | DatenoДата сегодняшнего дня
trigger"click" | "clickAndFocusVisible" | "focus" | "focusVisible" | "hover" | "hoverAndFocus" | "hoverAndFocusVisible"noУсловие отображения поповера: <br/> - `click` - открывать по клику <br/> - `hover` - открывать по ховеру <br/> - `focusVisible` - открывать по focus-visible <br/> - `focus` - открывать по фокусу <br/> - `hoverAndFocusVisible` - открывать по ховеру и focus-visible <br/> - `hoverAndFocus` - открывать по ховеру и фокусу <br/> - `clickAndFocusVisible` - открывать по клику и focus-visible
triggerClassNamestringnoCSS-класс триггера
triggerClickByKeysbooleantruenoВызывается ли попоповер по нажатию клавиш Enter/Space (при trigger = `click`)
triggerRefForwardedRef<HTMLElement | ReferenceType | null>noRef ссылка на триггер
valueTimeValuenoВыбранное значение.

Unions

Types

TimePickerDropdownProps

Unions

Адаптивность

TimePickerDropdown — адаптивный компонент с переключением поверхности (surface-swap). Раскладку он берёт из AdaptiveProvider (контекст @cloud-ru/ds-adaptive); встраиваемый TimePicker не адаптируется. Публичный API единый для обеих платформ:

  • desktop (по умолчанию) — барабан времени в popover над триггером (@cloud-ru/ds-dropdown).
  • mobile — барабан времени в BottomSheet из @cloud-ru/ds-bottom-sheet с кнопками «Сейчас» / «Применить».

Верстайте под desktop и поставьте один <AdaptiveProvider> в корне приложения — mobile-поверхность включается автоматически (desktop-first). Пропа layoutType у компонента нет: источник раскладки — только контекст.

Как форсировать платформу

Форс — только контекстом, не пропом:

  • Поддерево — вложенный провайдер:
    import { AdaptiveProvider } from '@cloud-ru/ds-adaptive'
    
    <AdaptiveProvider layoutType='mobile'>
      <TimePickerDropdown>…</TimePickerDropdown>
    </AdaptiveProvider>
  • Отдельный компонент — withLayoutType (module-scope, сахар над провайдером):
    import { withLayoutType } from '@cloud-ru/ds-adaptive'
    import { TimePickerDropdown } from '@cloud-ru/ds-calendar'
    
    const MobileTimePickerDropdown = withLayoutType(TimePickerDropdown, 'mobile')

Платформенные пропы

Пропы позиционирования и открытия popover’а (placement, fallbackPlacements, trigger, closeOnApply и др.) применяются только на desktop; на mobile BottomSheet имеет своё позиционирование снизу и игнорирует их.

Пропыdesktopmobile
placement, fallbackPlacements, trigger, closeOnApplyиспользуетсяигнорируется
value, defaultValue, onChangeValue, showSeconds, open, onOpenChangeиспользуетсяиспользуется

Подробнее о модели адаптивности — Adaptive.

Storybook

Figma

Смотри также