Skip to content
API 风格:

Popover (всплывающее окно) ​

Всплывающий слой с богатым содержимым: вмещает больше информации, чем Tooltip (заголовок, основной текст, кнопки действий). Триггер оборачивается в слот по умолчанию, содержимое передаётся через слот content. При срабатывании по клику слой имеет role="dialog" и захватывает фокус; при срабатывании по наведению только отображает содержимое, не прерывая поток клавиатурной навигации.

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

Срабатывание по клику
vue
<script setup lang="ts">
</script>

<template>
  <LhPopover title="Подсказка в стиле тушь" placement="top">
    <LhButton>Кликните, чтобы открыть</LhButton>
    <template #content>
      <p>Это содержимое с богатой разметкой: здесь можно разместить любой markup.</p>
    </template>
  </LhPopover>
</template>
vue
<script>
export default {}
</script>

<template>
  <LhPopover title="Подсказка в стиле тушь" placement="top">
    <LhButton>Кликните, чтобы открыть</LhButton>
    <template #content>
      <p>Это содержимое с богатой разметкой: здесь можно разместить любой markup.</p>
    </template>
  </LhPopover>
</template>

Способ вызова ​

hover / focus / manual
vue
<template>
  <!-- Срабатывание по наведению -->
  <LhPopover trigger="hover" title="Срабатывание по наведению" :show-after="0">
    <LhButton>Наведите курсор</LhButton>
    <template #content><p>Перемещение указателя внутрь всплывающего слоя не закроет его.</p></template>
  </LhPopover>
  <!-- Срабатывание по фокусу -->
  <LhPopover trigger="focus" title="Срабатывание по фокусу" :show-after="0">
    <LhInput placeholder="Поле для фокуса" />
    <template #content><p>Отображается, когда поле получает фокус.</p></template>
  </LhPopover>
  <!-- Ручное управление -->
  <LhPopover v-model:visible="manualVisible" trigger="manual" title="Ручное управление">
    <LhButton @click="manualVisible = !manualVisible">Внешний переключатель</LhButton>
    <template #content><p>Видимость полностью управляется внешним v-model.</p></template>
  </LhPopover>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const manualVisible = ref(false)
</script>
vue
<template>
  <!-- Срабатывание по наведению -->
  <LhPopover trigger="hover" title="Срабатывание по наведению" :show-after="0">
    <LhButton>Наведите курсор</LhButton>
    <template #content><p>Перемещение указателя внутрь всплывающего слоя не закроет его.</p></template>
  </LhPopover>
  <!-- Срабатывание по фокусу -->
  <LhPopover trigger="focus" title="Срабатывание по фокусу" :show-after="0">
    <LhInput placeholder="Поле для фокуса" />
    <template #content><p>Отображается, когда поле получает фокус.</p></template>
  </LhPopover>
  <!-- Ручное управление -->
  <LhPopover v-model:visible="manualVisible" trigger="manual" title="Ручное управление">
    <LhButton @click="manualVisible = !manualVisible">Внешний переключатель</LhButton>
    <template #content><p>Видимость полностью управляется внешним v-model.</p></template>
  </LhPopover>
</template>

<script>
export default {
  data() {
    return { manualVisible: false }
  },
}
</script>

Двенадцать сторон ​

placement
vue
<template>
  <LhPopover
    v-for="p in placements"
    :key="p"
    :placement="p"
    :title="p"
    trigger="hover"
    :show-after="0"
  >
    <LhButton size="small">{{ p }}</LhButton>
    <template #content><p>Сторона: {{ p }}</p></template>
  </LhPopover>
</template>

<script setup lang="ts">
const placements = [
  'top', 'top-start', 'top-end',
  'bottom', 'bottom-start', 'bottom-end',
  'left', 'left-start', 'left-end',
  'right', 'right-start', 'right-end',
] as const
</script>
vue
<template>
  <LhPopover
    v-for="p in placements"
    :key="p"
    :placement="p"
    :title="p"
    trigger="hover"
    :show-after="0"
  >
    <LhButton size="small">{{ p }}</LhButton>
    <template #content><p>Сторона: {{ p }}</p></template>
  </LhPopover>
</template>

<script>
export default {
  data() {
    return {
      placements: [
        'top', 'top-start', 'top-end',
        'bottom', 'bottom-start', 'bottom-end',
        'left', 'left-start', 'left-end',
        'right', 'right-start', 'right-end',
      ],
    }
  },
}
</script>

При нехватке места во viewport всплывающий слой автоматически меняет сторону (flip) и сдвигается по поперечной оси (shift); положение стрелки меняется вместе с ним.

Вложенное интерактивное содержимое ​

Пример с формой
vue
<template>
  <LhPopover placement="right" title="Быстрая заметка">
    <LhButton>Добавить заметку</LhButton>
    <template #content>
      <div class="demo-popover-form">
        <LhInput v-model="note" placeholder="Введите текст заметки" />
        <LhButton type="primary" @click="saveNote">Сохранить</LhButton>
      </div>
    </template>
  </LhPopover>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const note = ref('')
const saved = ref('')
function saveNote() {
  saved.value = note.value
  note.value = ''
}
</script>
vue
<template>
  <LhPopover placement="right" title="Быстрая заметка">
    <LhButton>Добавить заметку</LhButton>
    <template #content>
      <div class="demo-popover-form">
        <LhInput v-model="note" placeholder="Введите текст заметки" />
        <LhButton type="primary" @click="saveNote">Сохранить</LhButton>
      </div>
    </template>
  </LhPopover>
</template>

<script>
export default {
  data() {
    return { note: '', saved: '' }
  },
  methods: {
    saveNote() {
      this.saved = this.note
      this.note = ''
    },
  },
}
</script>

Дополнительные свойства ​

disabled / arrow / width / hideAfter / popperClass / teleportTarget
Контейнер Teleport (#demo-popover-host)
vue
<script setup lang="ts">
</script>

<template>
  <!-- Отключено -->
  <LhPopover disabled title="Отключено">
    <LhButton disabled>Отключённый триггер</LhButton>
    <template #content><p>При disabled триггер и всплывающий слой не реагируют.</p></template>
  </LhPopover>
  <!-- Без стрелки -->
  <LhPopover title="Без стрелки" :arrow="false" :show-after="0">
    <LhButton>Без стрелки</LhButton>
    <template #content><p>При arrow, равном false, стрелка-указатель не отрисовывается.</p></template>
  </LhPopover>
  <!-- Фиксированная ширина -->
  <LhPopover title="Фиксированная ширина" width="240" :show-after="0">
    <LhButton>Фиксированная ширина</LhButton>
    <template #content><p>Ширина всплывающего слоя задаётся через width, здесь 240px.</p></template>
  </LhPopover>
  <!-- Отложенное скрытие -->
  <LhPopover title="Отложенное скрытие" :hide-after="800" :show-after="0">
    <LhButton>Отложенное скрытие</LhButton>
    <template #content><p>Скрытие происходит с задержкой 800 мс после ухода курсора.</p></template>
  </LhPopover>
  <!-- Пользовательский класс -->
  <LhPopover title="Пользовательский класс" popper-class="demo-popper-accent" :show-after="0">
    <LhButton>Пользовательский класс</LhButton>
    <template #content><p>popperClass добавляется корневому элементу всплывающего слоя и пригоден для стилизации.</p></template>
  </LhPopover>
  <!-- Цель Teleport -->
  <LhPopover title="Цель Teleport" teleport-target="#demo-popover-host" :show-after="0">
    <LhButton>Контейнер Teleport</LhButton>
    <template #content><p>Всплывающий слой teleport-ируется в контейнер ниже, а не в body.</p></template>
  </LhPopover>
</template>
vue
<script>
export default {}
</script>

<template>
  <!-- Отключено -->
  <LhPopover disabled title="Отключено">
    <LhButton disabled>Отключённый триггер</LhButton>
    <template #content><p>При disabled триггер и всплывающий слой не реагируют.</p></template>
  </LhPopover>
  <!-- Без стрелки -->
  <LhPopover title="Без стрелки" :arrow="false" :show-after="0">
    <LhButton>Без стрелки</LhButton>
    <template #content><p>При arrow, равном false, стрелка-указатель не отрисовывается.</p></template>
  </LhPopover>
  <!-- Фиксированная ширина -->
  <LhPopover title="Фиксированная ширина" width="240" :show-after="0">
    <LhButton>Фиксированная ширина</LhButton>
    <template #content><p>Ширина всплывающего слоя задаётся через width, здесь 240px.</p></template>
  </LhPopover>
  <!-- Отложенное скрытие -->
  <LhPopover title="Отложенное скрытие" :hide-after="800" :show-after="0">
    <LhButton>Отложенное скрытие</LhButton>
    <template #content><p>Скрытие происходит с задержкой 800 мс после ухода курсора.</p></template>
  </LhPopover>
  <!-- Пользовательский класс -->
  <LhPopover title="Пользовательский класс" popper-class="demo-popper-accent" :show-after="0">
    <LhButton>Пользовательский класс</LhButton>
    <template #content><p>popperClass добавляется корневому элементу всплывающего слоя и пригоден для стилизации.</p></template>
  </LhPopover>
  <!-- Цель Teleport -->
  <LhPopover title="Цель Teleport" teleport-target="#demo-popover-host" :show-after="0">
    <LhButton>Контейнер Teleport</LhButton>
    <template #content><p>Всплывающий слой teleport-ируется в контейнер ниже, а не в body.</p></template>
  </LhPopover>
</template>

Контролируемая видимость ​

v-model:visible управляет видимостью всплывающего слоя вручную.

Контролируемая видимость
vue
<script setup lang="ts">
import { ref } from 'vue'
const popVisible = ref(false)
</script>

<template>
  <LhPopover v-model:visible="popVisible" title="Контролируемый слой" trigger="manual">
    <LhButton>{{ popVisible ? 'Свернуть' : 'Развернуть' }}</LhButton>
  </LhPopover>
</template>
vue
<script>
export default {
  data() {
    return { popVisible: false }
  },
}
</script>

<template>
  <LhPopover v-model:visible="popVisible" title="Контролируемый слой" trigger="manual">
    <LhButton>{{ popVisible ? 'Свернуть' : 'Развернуть' }}</LhButton>
  </LhPopover>
</template>

Popover API ​

Props ​

СвойствоОписаниеТипПо умолчанию
placementПозиция отображения"top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end"'top'
triggerСпособ вызова"click" | "hover" | "focus" | "manual"'click'
titleЗаголовок (можно также использовать слот title)string—
visibleВидимость при ручном управлении (v-model:visible)boolean—
disabledОтключёнboolean—
arrowПоказывать ли стрелкуbooleantrue
widthШирина всплывающего слояstring | number—
popperClassПользовательский класс всплывающего слояstring—
teleportTargetЦель Teleport (селектор или элемент)string | HTMLElement'body'
showAfterЗадержка показа (мс)number—
hideAfterЗадержка скрытия (мс)number—
themeVarsПеременные темы на уровне компонента, переопределяют конфигурацию ConfigProvider{ [x: string]: string; }—
bgPatternУзор фона на уровне компонента, переопределяет конфигурацию ConfigProviderstring—
sxСвойства быстрой разметки sx (применяются к корневому элементу триггера)LhSxProps—

Events ​

СобытиеОписаниеПараметры
update:visibleИзменение видимости(value: boolean)
showСрабатывает после показа—
hideСрабатывает после скрытия—

Slots ​

СлотОписание
defaultСодержимое триггера
titleПользовательский заголовок
contentСодержимое всплывающего слоя

Methods ​

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

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

  • При срабатывании по клику всплывающий слой имеет role="dialog" + aria-modal="true", триггер — aria-haspopup / aria-expanded
  • При срабатывании по клику фокус перемещается на первый фокусируемый элемент внутри слоя, Tab циклически ходит по слою, после закрытия фокус возвращается триггеру
  • При срабатывании по наведению / фокусу фокус не перемещается, чтобы не прерывать работу с клавиатурой
  • Закрытие по Escape и по клику вне всплывающего слоя; при наличии заголовка он связывается через aria-labelledby
  • Движок позиционирования — собственная реализация на чистых функциях (flip/shift от переполнения), без сторонних runtime-зависимостей

Триггер прокрутился за пределы viewport — слой скрывается: всплывающий слой позиционирован как fixed, и после ухода триггера из viewport он больше не следует за ним, оставаясь висеть над содержимым страницы с высоким z-index. Поэтому при любом способе вызова (включая контролируемый режим manual) полный уход триггера из viewport сворачивает панель и порождает update:visible для синхронизации внешнего состояния.

Определения типов ​

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

LhPopoverProps
ts
/**
 * Props компонента Popover
 */
export interface LhPopoverProps {
  /** Позиция отображения */
  placement?: LhPlacement
  /** Способ вызова */
  trigger?: LhPopoverTrigger
  /** Заголовок */
  title?: string
  /** Видимость при ручном управлении (v-model:visible) */
  visible?: boolean
  /** Отключён */
  disabled?: boolean
  /** Показывать ли стрелку */
  arrow?: boolean
  /** Ширина всплывающего слоя */
  width?: string | number
  /** Пользовательский класс всплывающего слоя */
  popperClass?: string
  /** Цель Teleport (селектор или элемент, по умолчанию body) */
  teleportTarget?: string | HTMLElement
  /** Задержка показа (мс) */
  showAfter?: number
  /** Задержка скрытия (мс) */
  hideAfter?: number
  /** Переменные темы на уровне компонента, переопределяют конфигурацию ConfigProvider */
  themeVars?: Record<string, string>
  /** Узор фона на уровне компонента, переопределяет конфигурацию ConfigProvider */
  bgPattern?: string
  /** Свойства быстрой разметки sx (применяются к корневому элементу триггера) */
  sx?: LhSxProps
}
LhPopoverPlacement (из @lh-ui/share/core/shared/popper)
ts
/**
 * Движок позиционирования Popper (чистые функции, без зависимости от фреймворка)
 *
 * Геометрические вычисления, общие для всплывающих слоёв
 * (Tooltip / Popover / Popconfirm / Dropdown и др.):
 * размещение для 12 вариантов placement, flip при переполнении по главной оси,
 * сдвиг (shift) при выходе за поперечную ось.
 * Все аргументы — литералы rect, DOM не затрагивается, что позволяет
 * тестировать функции напрямую.
 */

/** Сторона всплывающего слоя (направление + выравнивание) */
export type LhPlacement =
  | 'top'
  | 'top-start'
  | 'top-end'
  | 'bottom'
  | 'bottom-start'
  | 'bottom-end'
  | 'left'
  | 'left-start'
  | 'left-end'
  | 'right'
  | 'right-start'
  | 'right-end'
LhPopoverTrigger (из @lh-ui/share/core/popover (logic.ts))
ts
/** Способ вызова */
export type LhPopoverTrigger = 'click' | 'hover' | 'focus' | 'manual'

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