Skip to content
API 风格:

Выпадающее меню ​

Раскрывающееся вниз командное меню. Триггером может быть любой элемент; пункты меню идентифицируются через command, а после нажатия значение выбрасывается событием command. Реализовано по паттерну меню WAI-ARIA: циклическая навигация стрелками, Home/End — в начало/конец, Enter/Space — активация, Esc — закрытие с возвратом фокуса.

Базовое использование ​

Запуск по click + событие command

Последняя команда: —

vue
<template>
  <LhDropdown @command="onCommand">
    <LhButton>{{ selectedLabel || 'Ещё действия' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="new">Создать</LhDropdownItem>
      <LhDropdownItem command="edit">Редактировать</LhDropdownItem>
      <LhDropdownItem command="delete">Удалить</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const lastCommand = ref('')
// Текст триггера поддерживается пользователем: событие command лишь выбрасывает значение команды,
// а то, как отображать подпись, решает внешняя сторона
const labelMap: Record<string, string> = { new: 'Создать', edit: 'Редактировать', delete: 'Удалить' }
const selectedLabel = ref('')
function onCommand(command: string | number | undefined) {
  lastCommand.value = String(command)
  selectedLabel.value = labelMap[String(command)] ?? String(command)
}
</script>
vue
<script>
export default {
  data() {
    return {
      lastCommand: '',
      // Текст триггера поддерживается пользователем: событие command лишь выбрасывает значение команды,
      // а то, как отображать подпись, решает внешняя сторона
      labelMap: { new: 'Создать', edit: 'Редактировать', delete: 'Удалить' },
      selectedLabel: '',
    }
  },
  methods: {
    onCommand(command) {
      this.lastCommand = String(command)
      this.selectedLabel = this.labelMap[String(command)] ?? String(command)
    },
  },
}
</script>

<template>
  <LhDropdown @command="onCommand">
    <LhButton>{{ selectedLabel || 'Ещё действия' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="new">Создать</LhDropdownItem>
      <LhDropdownItem command="edit">Редактировать</LhDropdownItem>
      <LhDropdownItem command="delete">Удалить</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

Способ запуска ​

По умолчанию click; также можно настроить раскрытие по наведению или по правой кнопке мыши.

hover / contextmenu
vue
<script setup lang="ts">
import { ref } from 'vue'
const hoverLabel = ref('')
function onHoverCommand(command: string | number | undefined) {
  hoverLabel.value = String(command)
}
const contextLabel = ref('')
function onContextCommand(command: string | number | undefined) {
  contextLabel.value = String(command)
}
</script>

<template>
  <!-- Запуск по наведению -->
  <LhDropdown trigger="hover" @command="onHoverCommand">
    <LhButton>{{ hoverLabel || 'Наведение мыши' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="hover-1">Пункт 1</LhDropdownItem>
      <LhDropdownItem command="hover-2">Пункт 2</LhDropdownItem>
    </template>
  </LhDropdown>
  <!-- Запуск по правой кнопке мыши -->
  <LhDropdown trigger="contextmenu" @command="onContextCommand">
    <LhButton>{{ contextLabel || 'Правая кнопка мыши' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="ctx-1">Обновить</LhDropdownItem>
      <LhDropdownItem command="ctx-2">Копировать</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {
  data() {
    return { hoverLabel: '', contextLabel: '' }
  },
  methods: {
    onHoverCommand(command) {
      this.hoverLabel = String(command)
    },
    onContextCommand(command) {
      this.contextLabel = String(command)
    },
  },
}
</script>

<template>
  <!-- Запуск по наведению -->
  <LhDropdown trigger="hover" @command="onHoverCommand">
    <LhButton>{{ hoverLabel || 'Наведение мыши' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="hover-1">Пункт 1</LhDropdownItem>
      <LhDropdownItem command="hover-2">Пункт 2</LhDropdownItem>
    </template>
  </LhDropdown>
  <!-- Запуск по правой кнопке мыши -->
  <LhDropdown trigger="contextmenu" @command="onContextCommand">
    <LhButton>{{ contextLabel || 'Правая кнопка мыши' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="ctx-1">Обновить</LhDropdownItem>
      <LhDropdownItem command="ctx-2">Копировать</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

Триггер contextmenu блокирует стандартное контекстное меню браузера.

Блокировка и разделители ​

disabled / divided
vue
<script setup lang="ts">
import { ref } from 'vue'
const actionLabel = ref('')
function onActionCommand(command: string | number | undefined) {
  actionLabel.value = String(command)
}
</script>

<template>
  <!-- Меню действий с заблокированным пунктом и разделителем -->
  <LhDropdown @command="onActionCommand">
    <LhButton>{{ actionLabel || 'Действие' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="export">Экспорт</LhDropdownItem>
      <LhDropdownItem command="import">Импорт</LhDropdownItem>
      <LhDropdownItem divided command="audit">Журнал аудита</LhDropdownItem>
      <LhDropdownItem disabled command="purge">Очистить данные</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {
  data() {
    return { actionLabel: '' }
  },
  methods: {
    onActionCommand(command) {
      this.actionLabel = String(command)
    },
  },
}
</script>

<template>
  <!-- Меню действий с заблокированным пунктом и разделителем -->
  <LhDropdown @command="onActionCommand">
    <LhButton>{{ actionLabel || 'Действие' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="export">Экспорт</LhDropdownItem>
      <LhDropdownItem command="import">Импорт</LhDropdownItem>
      <LhDropdownItem divided command="audit">Журнал аудита</LhDropdownItem>
      <LhDropdownItem disabled command="purge">Очистить данные</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

Заблокированный пункт всё же может получать фокус (aria-disabled="true"), и программа чтения с экрана его озвучивает; однако стрелки его пропускают, а нажатие и Enter/Space не вызывают command.

Позиция и максимальная высота ​

placement и maxHeight
vue
<script setup lang="ts">
import { ref } from 'vue'
const itemLabel = ref('')
function onItemCommand(command: string | number | undefined) {
  itemLabel.value = `Пункт ${command}`
}
</script>

<template>
  <!-- Выравнивание по нижнему правому краю с ограничением максимальной высоты -->
  <LhDropdown placement="bottom-end" :max-height="160" @command="onItemCommand">
    <LhButton>{{ itemLabel || 'Нижний правый край' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem v-for="i in 12" :key="i" :command="i">Пункт {{ i }}</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {
  data() {
    return { itemLabel: '' }
  },
  methods: {
    onItemCommand(command) {
      this.itemLabel = `Пункт ${command}`
    },
  },
}
</script>

<template>
  <!-- Выравнивание по нижнему правому краю с ограничением максимальной высоты -->
  <LhDropdown placement="bottom-end" :max-height="160" @command="onItemCommand">
    <LhButton>{{ itemLabel || 'Нижний правый край' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem v-for="i in 12" :key="i" :command="i">Пункт {{ i }}</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

Управляемое отображение и поведение при нажатии ​

v-model:visible управляет видимостью вручную; hide-on-click определяет, сворачивать ли меню после выбора; popper-class добавляет всплывающему слою пользовательский класс; teleport-target задаёт контейнер монтирования.

Управляемое отображение
vue
<script setup lang="ts">
import { ref } from 'vue'
const menuVisible = ref(false)
</script>

<template>
  <!-- Управляемое отображение: после выбора не сворачивается -->
  <LhDropdown
    v-model:visible="menuVisible"
    :hide-on-click="false"
    popper-class="demo-dropdown-popper"
  >
    <LhButton>{{ menuVisible ? 'Свернуть' : 'Развернуть' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="copy">Копировать (не сворачивать после выбора)</LhDropdownItem>
      <LhDropdownItem command="paste">Вставить</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {
  data() {
    return { menuVisible: false }
  },
}
</script>

<template>
  <!-- Управляемое отображение: после выбора не сворачивается -->
  <LhDropdown
    v-model:visible="menuVisible"
    :hide-on-click="false"
    popper-class="demo-dropdown-popper"
  >
    <LhButton>{{ menuVisible ? 'Свернуть' : 'Развернуть' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="copy">Копировать (не сворачивать после выбора)</LhDropdownItem>
      <LhDropdownItem command="paste">Вставить</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
Монтирование в заданный контейнер
vue
<script setup lang="ts">
</script>

<template>
  <!-- Монтирование в заданный контейнер -->
  <LhDropdown teleport-target="#dropdown-zone">
    <LhButton>Ещё ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="a">Пункт</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {}
</script>

<template>
  <!-- Монтирование в заданный контейнер -->
  <LhDropdown teleport-target="#dropdown-zone">
    <LhButton>Ещё ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="a">Пункт</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

Props ​

СвойствоОписаниеТипПо умолчанию
triggerСпособ запуска"hover" | "click" | "contextmenu"'click'
placementПозиция показа (12 вариантов, как у Tooltip)"top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end"'bottom-start'
visibleСостояние видимости (v-model:visible)boolean—
disabledБлокировать ли триггерboolean—
hideOnClickЗакрывать ли автоматически после нажатия пункта менюbooleantrue
teleportTargetЦель Teleport (селектор или элемент)string | HTMLElement'body'
popperClassПользовательский класс всплывающего слояstring—
maxHeightМаксимальная высота меню (число — px)string | number—
themeVarsТематические переменные уровня компонента{ [x: string]: string; }—
bgPatternФоновый узор уровня компонента, переопределяет конфигурацию ConfigProviderstring—
sxСвойство быстрой компоновки sx (применяется к корневому элементу триггера)LhSxProps—

Events ​

СобытиеОписаниеПараметр
commandСрабатывает при нажатии пункта меню(command: string | number | undefined)
clickСрабатывает при нажатии пункта меню (LhDropdownItem)(command: string | number | undefined)
visible-changeИзменение видимости(value: boolean)
update:visibleИзменение видимости (v-model)(value: boolean)

Slots ​

СлотОписание
defaultСодержимое триггера
dropdownСодержимое меню (список LhDropdownItem)

Methods ​

МетодОписание
showПрограммное раскрытие
hideПрограммное сворачивание
toggleПрограммное переключение

Props ​

СвойствоОписаниеТипПо умолчанию
commandИдентификатор команды, выбрасывается с событием command при нажатииstring | number—
disabledБлокировкаbooleanfalse
dividedПоказывать ли разделитель сверхуbooleanfalse

Доступность ​

  • Триггер несёт aria-haspopup="menu" и aria-expanded; меню — role="menu", пункты — role="menuitem"
  • Клавиатура следует паттерну меню APG: ↑/↓ — циклически (с пропуском заблокированных), Home/End — в начало/конец, Enter/Space — вызов command и закрытие, Esc — закрытие с возвратом фокуса
  • Заблокированные пункты используют aria-disabled, а не удаление из дерева фокуса; стрелки и Enter их пропускают
  • При раскрытии меню переполнение по главной оси автоматически разворачивает направление, по поперечной оси — вписывается обратно в область просмотра

Типы ​

Структура открытых TypeScript-типов этого компонента (автоматически извлечено из packages/components/dropdown/src/types.ts и исходников @lh-ui/share/core/*; раскройте каждый пункт, чтобы увидеть полное определение; описания свойств — в таблице Props выше):

LhDropdownProps
ts
/**
 * Props компонента Dropdown
 */
export interface LhDropdownProps {
  /** Способ запуска */
  trigger?: LhDropdownTrigger
  /** Позиция показа */
  placement?: LhDropdownPlacement
  /** Управляемое вручную состояние отображения (v-model:visible) */
  visible?: boolean
  /** Блокировка */
  disabled?: boolean
  /** Закрывать ли автоматически после нажатия пункта меню */
  hideOnClick?: boolean
  /** Цель Teleport (селектор или элемент, по умолчанию body) */
  teleportTarget?: string | HTMLElement
  /** Пользовательский класс всплывающего слоя */
  popperClass?: string
  /** Максимальная высота меню */
  maxHeight?: string | number
  /** Тематические переменные уровня компонента, переопределяют конфигурацию ConfigProvider */
  themeVars?: Record<string, string>
  /** Фоновый узор уровня компонента, переопределяет конфигурацию ConfigProvider */
  bgPattern?: string
  /** Свойство быстрой компоновки sx (применяется к корневому элементу триггера) */
  sx?: LhSxProps
}
LhDropdownItemProps
ts
/**
 * Props компонента DropdownItem
 */
export interface LhDropdownItemProps {
  /** Идентификатор команды, выбрасывается с событием command при нажатии */
  command?: string | number
  /** Блокировка (фокус сохраняется, чтобы оставаться в дереве доступности) */
  disabled?: boolean
  /** Показывать ли разделитель */
  divided?: boolean
}
LhDropdownTrigger (из @lh-ui/share/core/dropdown (logic.ts))
ts
/** Способ запуска */
export type LhDropdownTrigger = 'hover' | 'click' | 'contextmenu'

Красота традиций — основа кода