@cloud-ru/ds-dropdown

中文文档
1.2.2发布于 2026/11/11Apache-2.0TypeScriptESM + CJS近 30 天下载 1.7万

Выпадающий блок с произвольным контентом и встроенными состояниями loading / not-found / no-data / data-error.

原始使用文档
文档目录15 节
  1. Когда использовать
  2. Анатомия
  3. State
  4. Высота и прокрутка
  5. Width strategy (default gte )
  6. Установка
  7. Примеры использования
  8. Базовый Dropdown
  9. Открытое меню для визуальной сверки
  10. Состояние loading
  11. Состояние not-found с действием
  12. Props
  13. Адаптивность
  14. Как форсировать платформу
  15. Платформенные пропы

Dropdown

@cloud-ru/ds-dropdown — Выпадающий блок с произвольным контентом и встроенными состояниями loading / not-found / no-data / data-error.

Выпадающий блок над триггером: произвольный контент (меню, фильтры, справочная информация) + встроенные состояния loading / not-found / no-data / data-error через state. Поверх PopoverPrivate — те же пропсы позиционирования, плюс готовая визуальная обработка асинхронных случаев.

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

  • Меню действий над кнопкой (экспорт, фильтры, настройки).
  • Асинхронные подсказки и suggestions — встроенный state скрывает ручное ветвление UI.
  • Композитные виджеты: селекторы, комбобоксы, авторасшифровки.

Когда не нужен Dropdown: для одиночного подсказывающего текста используйте Tooltip, для модального выбора — Modal или Popover.

Анатомия

State

Встроенные состояния контента, позволяющие не ветвить UI вручную: loading — идёт запрос (скелетон/спиннер), no-data — у источника пусто (первая загрузка), not-found — пользователь ввёл фильтр и ничего не нашлось, data-error — запрос упал (с опцией повтора).

Высота и прокрутка

Компонент высоту не ограничивает и своей прокрутки не даёт: содержимое dropdown произвольное, поэтому предел высоты проектируется вместе с ним. Для типового случая — списка опций — есть готовый Droplist из @cloud-ru/ds-list: он сам ограничивает высоту (256 / 320 / 384px по размеру) и включает прокрутку через limitedScrollHeight.

Width strategy (default gte)

Ширина dropdown относительно триггера:

  • auto — по содержимому, триггер не учитывается.
  • eq — ровно по триггеру: width фиксируется его шириной, содержимое сжимается или обрезается.
  • gte — не уже триггера: ширина по содержимому, но min-width равна ширине триггера.

Разница между gte и eq видна, когда содержимое шире триггера: eq сожмёт список до ширины триггера, gte даст ему растянуться. Когда содержимое уже триггера, оба дают одинаковый результат — при триггере 96 и содержимом 40 auto даёт 40, eq и gte — по 96.

Значения gte в макетах нет: Figma рисует только «по содержимому» и «по триггеру». Это осознанное расширение API — промежуточный случай встречается в продукте чаще остальных, поэтому он и стоит дефолтом.

Установка

pnpm add @cloud-ru/ds-dropdown
import { Dropdown, STATE } from '@cloud-ru/ds-dropdown'

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

Базовый Dropdown

import { Button } from '@cloud-ru/ds-button';
import { Dropdown } from '@cloud-ru/ds-dropdown';

export function Basic() {
  return (
    <Dropdown content={<div style={{ padding: 12 }}>Контент меню</div>}>
      <Button label='Открыть' />
    </Dropdown>
  );
}

Открытое меню для визуальной сверки

Управляемый режим — open

import { Button } from '@cloud-ru/ds-button';
import { Dropdown } from '@cloud-ru/ds-dropdown';

export function OpenForReview() {
  return (
    <Dropdown content={<div style={{ padding: 12 }}>Видимое содержимое</div>}>
      <Button label='Триггер' />
    </Dropdown>
  );
}

Состояние loading

import { Button } from '@cloud-ru/ds-button';
import { Dropdown, STATE } from '@cloud-ru/ds-dropdown';

export function Loading() {
  return (
    <Dropdown state={{ type: STATE.Loading }} content={null}>
      <Button label='Загрузка' />
    </Dropdown>
  );
}

Состояние not-found с действием

import { Button } from '@cloud-ru/ds-button';
import { Dropdown, STATE } from '@cloud-ru/ds-dropdown';

export function NotFound() {
  return (
    <Dropdown
      state={{
        type: STATE.NotFound,
        content: 'Ничего не нашли',
        actionLabel: 'Сбросить фильтры',
        onActionClick: () => {},
      }}
      content={null}
    >
      <Button label='Поиск' />
    </Dropdown>
  );
}

Props

DropdownProps

PropTypeDefaultDescription
bodyPaddingbooleantrueПаддинги body. false — убрать (контент во всю ширину; на mobile прокидывается в BottomSheet).
childrenstring | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | null | undefined—
classNamestring—CSS-класс
closeOnEscapeKeybooleantrueЗакрывать ли по нажатию на кнопку Esc
closeOnPopstateboolean—Закрывать ли поповер при переходе по истории браузера
containerRefObject<HTMLElement | null>—Контейнер портала (ref). Переопределяет PortalContext для этого инстанса —
по аналогии с container у Modal/Drawer. По умолчанию берётся из PortalContextProvider.
contentReactNode—Содержимое внутри поповера (body)
data-test-idstring—
defaultSnapIndexnumber—Только mobile: индекс snap'а по умолчанию (см. BottomSheet).
disableSpanWrapperboolean—Отключает для isValidElement внешнюю обертку триггера
Пригодится для элементов с position: absolute
Работает для триггеров, которые умеют отдать свою DOM-ноду: нативные элементы, forwardRef-компоненты
и компоненты, помеченные withInnerRefSupport из @cloud-ru/ds-utils. Остальные всё равно получают <span> —
без ноды поповеру не от чего считать позицию; в dev-режиме об этом печатается предупреждение.
fallbackPlacementsPlacement—Цепочка расположений которая будет применяться к поповеру от первого к последнему если при текущем он не влезает.
footerReactNode—Слот футера (bottomBar)
footerDividerboolean—Divider между body и футером
headerDividerboolean—Divider между шапкой и body
hoverDelayClosenumber—Задержка закрытия по ховеру
hoverDelayOpennumber—Задержка открытия по ховеру
offsetnumber0Отступ поповера от его триггер-элемента (в пикселях).
onOpenChange((isOpen: boolean) => void)—Колбек отображения компонента. Срабатывает при изменении состояния open.
openboolean—Управляет состоянием показан/не показан.
outsideClickOutsideClickHandler—Закрывать ли при клике вне поповера
placement"bottom" | "bottom-end" | "bottom-start" | "left" | "left-end" | "left-start" | "right" | "right-end" | "right-start" | "top" | "top-end" | "top-start"topПоложение поповера относительно своего триггера (children).
searchReactNode—Слот поиска в шапке (topBar)
slotAfterTitleReactNode—Подсказка-иконка рядом с заголовком (потребитель собирает, напр. <QuestionTooltip />)
snapPointsSnapPoint['fit-content']Только mobile: snap-точки BottomSheet'а (высота листа). На desktop игнорируется.
stateActionButtonProps | BlockProps | BlockPropsWithIcon | DropdownState—Состояние
stopPropagationStopPropagationHandlers{ onClick: true, onMouseDown: true, onMouseUp: true, onTouchStart: true, onTouchEnd: true, onTouchMove: true }Гасить всплытие pointer/touch-событий с floating-контейнера (stopPropagation).
По умолчанию все хендлеры включены. Для drag&drop внутри поповера отключите
onMouseUp / onTouchEnd, чтобы они дошли до document.
titleReactNode—Заголовок в шапке (topBar)
trigger"click" | "clickAndFocusVisible" | "focus" | "focusVisible" | "hover" | "hoverAndFocus" | "hoverAndFocusVisible"—Условие отображения поповера:
- click - открывать по клику
- hover - открывать по ховеру
- focusVisible - открывать по focus-visible
- focus - открывать по фокусу
- hoverAndFocusVisible - открывать по ховеру и focus-visible
- hoverAndFocus - открывать по ховеру и фокусу
- clickAndFocusVisible - открывать по клику и focus-visible
triggerClassNamestring—CSS-класс триггера
triggerClickByKeysbooleantrueВызывается ли попоповер по нажатию клавиш Enter/Space (при trigger = click)
triggerRefForwardedRef<HTMLElement | ReferenceType | null>—Ref ссылка на триггер
widthStrategy"auto" | "eq" | "gte"autoСтратегия управления шириной контейнера поповера
- auto - соответствует ширине контента,
- gte - Great Than or Equal, равен ширине таргета или больше ее, если контент в поповере шире,
- eq - Equal, строго равен ширине таргета.

ActionButtonProps

PropTypeDefaultDescription
actionLabelstring | undefined—Лейбл кнопки-действия
onActionClick(() => void) | undefined—Действие при клике по кнопке

BlockProps

PropTypeDefaultDescription
contentstring | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | null | undefined—Подзаголовок

BlockPropsWithIcon

PropTypeDefaultDescription
contentstring | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | null | undefined—Подзаголовок
iconIconPredefinedProps—Иконка
  • DropdownState = { type: typeof STATE.Loading; } | ({ type: typeof STATE.NotFound; } & ActionButtonProps & BlockProps) | ({ type: typeof STATE.NoData; } & ActionButtonProps & BlockPropsWithIcon) | ({ type: typeof STATE.DataError; } & ActionButtonProps & BlockPropsWithIcon)

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

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

  • desktop (по умолчанию) — popover над триггером с позиционированием через floating-ui.
  • mobile — контент рендерится в BottomSheet из @cloud-ru/ds-bottom-sheet (панель снизу со свайпом для закрытия).

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

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

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

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

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

Часть пропов управляет позиционированием desktop-popover'а и на mobile молча игнорируется (у BottomSheet своё позиционирование снизу). Таблица синхронизирована с type-level JSDoc у DropdownProps.

Пропыdesktopmobile
placement, widthStrategy, offset, fallbackPlacementsиспользуетсяигнорируется
hoverDelayOpen, hoverDelayClose, closeOnEscapeKey, triggerClickByKeys, outsideClickиспользуетсяигнорируется
disableSpanWrapper, triggerClassName, triggerRef, containerиспользуетсяигнорируется
content, title, slotAfterTitle, search, footer, headerDivider, footerDividerиспользуетсяиспользуется
state, open, onOpenChange, closeOnPopstate, classNameиспользуетсяиспользуется

На mobile портал BottomSheet берётся из @cloud-ru/ds-portal-context, поэтому container не действует.

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

版本与发布记录

@cloud-ru/ds-dropdown 共发布 131 个版本,首次发布于 2026/08/18;过去 12 个月发布了 131 个版本。

版本发布日期距今
1.2.2latest2026/11/11今天
1.2.2-preview-e8fe6b56.0preview2026/11/11今天
1.2.2-preview-c0e895ed.02026/11/11今天
1.2.2-preview-90b10112.02026/11/11今天
1.2.2-preview-fd7dd437.02026/11/101 天前
1.2.2-preview-9a75a2a9.02026/11/101 天前

关于 @cloud-ru/ds-dropdown 的常见问题

@cloud-ru/ds-dropdown 是什么?

Выпадающий блок с произвольным контентом и встроенными состояниями loading / not-found / no-data / data-error.

如何安装 @cloud-ru/ds-dropdown?

在项目目录运行 npm install @cloud-ru/ds-dropdown,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@cloud-ru/ds-locale(^1.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@cloud-ru/ds-dropdown 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cloud-ru/ds-dropdown 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@cloud-ru/ds-dropdown 可以商用吗?

@cloud-ru/ds-dropdown 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@cloud-ru/ds-dropdown 的下载量有多少?

根据 npm 官方下载统计,@cloud-ru/ds-dropdown 在 2026/10/10 – 2026/11/08 这 30 天内被下载 17,001 次,日均约 567 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cloud-ru/ds-dropdown 还在维护吗?

最新版本 1.2.2 发布于 2026/11/11(今天);至今共发布 131 个版本,其中过去 12 个月发布了 131 个;首次发布于 2026/08/18。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。