GrTooltip

Пакет: @feugene/granularityядроГруппа: Навигация

Берут, когда подпись к иконочной кнопке.

Когда брать

  • подпись к иконочной кнопке — единственный способ дать имя контролу, у которого нет видимого текста;
  • расшифровка сокращения или статуса — короткий текст, который иначе занял бы место в раскладке;
  • подсказка появляется и по фокусу — не только по наведению, поэтому она достижима с клавиатуры;
  • текста немного — одна-две строки: длинный текст в подсказке нельзя ни выделить, ни прокрутить.

Когда взять другое

НужноБерите
Внутри нужен интерактив: ссылка, кнопка, полеGrPopover
Список действийGrDropdownMenu
Сообщение, которое пользователь обязан заметитьGrAlert
Сообщение о результате действияGrToaster
Постоянное пояснение под полемGrFormField

Единственным носителем информации подсказка быть не может. На тач-устройстве наведения нет, а текст, доступный только по нему, для части пользователей не существует вовсе. То, без чего задачу не выполнить, живёт в самой раскладке — подписью, хинтом поля или строкой рядом.

Триггер и таб-порядок

Без слота триггер — иконка info, и остановкой Tab становится обёртка. Со слотом обёртка отходит в сторону: если внутри есть фокусируемый элемент (кнопка, ссылка, поле), aria-describedby уезжает на него, а обёртка теряет tabindex. Иначе на один визуальный контрол приходилось бы два таб-стопа, причём внешний — безролевой <span>.

Если фокусироваться в слоте нечему (текст, картинка), обёртка остаётся остановкой Tab — иначе подсказка была бы недоступна с клавиатуры вовсе.

<!-- Обёртка прозрачна: остановка Tab одна — сама кнопка. -->
<GrTooltip text="Удалить безвозвратно">
  <GrButton variant="ghost" square><IconTrash /></GrButton>
</GrTooltip>

Содержимое

text — короткая строка; разметка вместо неё — слот #content. Без того и другого подсказка не открывается: пустая панель хуже отсутствующей.

Размещение

placement — любая сторона из шкалы @floating-ui (top, right-start, …), offsetPx — зазор до триггера. Итоговая сторона может отличаться от заданной: flip переворачивает подсказку, когда места не хватает, shift не даёт ей вылезти за край экрана.

Задержки

openDelay и closeDelay (мс, по умолчанию оба 0). Задержка показа нужна там, где подсказки стоят плотно — на панели кнопок без неё подсказка мигает при каждом проведении курсором. Escape и клик вне закрывают мгновенно, минуя closeDelay: задержка там читалась бы как залипание.

Управление снаружи

disabled запрещает показ полностью. v-model:open переводит видимость под контроль владельца — компонент перестаёт открывать себя сам и только сообщает о намерении через update:open.

Тач

На тач-устройствах hover не существует, а фокус по тапу не гарантирован, поэтому тап по триггеру переключает подсказку, а тап вне — закрывает.

Playground 8

Загружается…

Код
<GrTooltip />

Установка

npm i @feugene/granularity

Импорт

import { GrTooltip } from '@feugene/granularity/components/GrTooltip'

API

Props

PropTypeпо умолчаниюОписание
openboolean | undefinedundefinedУправляемая видимость. Без неё компонент ведёт видимость сам.
disabledboolean | undefinedfalseПодсказка не показывается ничем — ни курсором, ни фокусом, ни `v-model:open`.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedРазмер панели и дефолтной триггер-иконки.
placementPlacement | undefined"top"Предпочтительная сторона панели; `flip` может её изменить.
offsetPxnumber | undefinedundefinedЗазор между триггером и панелью, px.
openDelaynumber | undefined0Задержка перед показом, мс. Спасает от мигания при проведении курсором по панели кнопок.
closeDelaynumber | undefined0Задержка перед скрытием, мс.
textstring | undefinedundefinedТекст подсказки. Разметка вместо текста — слот `#content`.
iconColorstring | undefined"var(--gr-muted-fg)"Цвет триггер-иконки (CSS color). По умолчанию — `var(--gr-muted-fg)`.

Slots

SlotTypeОписание
defaultanyТриггер. По умолчанию — иконка info.
contentanyСодержимое подсказки; заменяет проп `text`.

Events

EventTypeОписание
update:open[value: boolean]

Примеры 5

Подсказка рядом с подписью поля

Самый частый сценарий для GrTooltip — короткое пояснение рядом с label или small helper-control.

Hover or focus the info icon to inspect the help copy.

Inline Help
<script setup lang="ts">
import { GrTooltip } from '@feugene/granularity'
</script>

<template>
  <div class="grid gap-4">
    <label class="inline-flex items-center gap-2 text-sm font-600 text-[var(--gr-fg)]">
      Notification email
      <GrTooltip text="We use this address only for billing alerts and incident updates." />
    </label>

    <div class="rounded-2xl border border-dashed border-[var(--gr-brd)] bg-[var(--gr-muted)]/60 p-4 text-sm text-[var(--gr-muted-fg)]">
      Hover or focus the info icon to inspect the help copy.
    </div>
  </div>
</template>

Свой триггер через слот по умолчанию

Показываем, что tooltip не ограничен встроенной info-иконкой: любой trigger можно прокинуть через default slot.

Reuse the same tooltip primitive for icon buttons, labels or table headers.

Custom Trigger
<script setup lang="ts">
import { GrTooltip } from '@feugene/granularity'
</script>

<template>
  <div class="flex flex-wrap items-center gap-4">
    <GrTooltip text="Custom slot lets you attach the tooltip to any trigger element.">
      <button
        type="button"
        class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-[var(--gr-brd)] bg-[var(--gr-card)] text-sm font-700 text-[var(--gr-fg)] transition-colors hover:bg-[var(--gr-muted)]"
        aria-label="Open contextual help"
      >
        ?
      </button>
    </GrTooltip>

    <span class="text-sm text-[var(--gr-muted-fg)]">
      Reuse the same tooltip primitive for icon buttons, labels or table headers.
    </span>
  </div>
</template>

Правимый текст и тон иконки

Выделяем вторую важную возможность компонента: управлять plain-text сообщением и цветом trigger-иконки из внешнего state.

Custom tone icon-color="var(--gr-warning)"

Tone
<script setup lang="ts">
import { ref } from 'vue'

import { GrInput, GrTooltip } from '@feugene/granularity'

const tooltipText = ref('Escalation policy will be applied to new alerts only.')
const iconColor = ref('var(--gr-warning)')

// Пресеты цвета иконки из палитры GrTone: клик подставляет валидную CSS-переменную
// темы в инпут `icon-color` (раньше в демо был несуществующий `var(--warning)`).
const tonePresets: Array<{ tone: string, value: string }> = [
  { tone: 'primary', value: 'var(--gr-primary)' },
  { tone: 'neutral', value: 'var(--gr-muted-fg)' },
  { tone: 'success', value: 'var(--gr-success)' },
  { tone: 'warning', value: 'var(--gr-warning)' },
  { tone: 'danger', value: 'var(--gr-danger)' },
  { tone: 'info', value: 'var(--gr-info)' },
  { tone: 'slate', value: 'var(--gr-slate)' },
  { tone: 'azure', value: 'var(--gr-azure)' },
]
</script>

<template>
  <div class="grid gap-4">
    <div class="flex flex-wrap items-center gap-3">
      <span class="text-sm font-600 text-[var(--gr-fg)]">
        Custom tone
      </span>
      <GrTooltip :text="tooltipText" :icon-color="iconColor" />
      <code class="rounded bg-[var(--gr-muted)] px-2 py-1 text-xs text-[var(--gr-muted-fg)]">
        icon-color="{{ iconColor }}"
      </code>
    </div>

    <div class="flex flex-wrap items-center gap-2">
      <button
        v-for="preset in tonePresets"
        :key="preset.tone"
        type="button"
        class="inline-flex items-center gap-2 rounded-full border border-[var(--gr-brd)] px-3 py-1 text-xs font-600 transition-colors hover:bg-[var(--gr-muted)]"
        :class="iconColor === preset.value ? 'ring-2 ring-[var(--gr-ring)]' : ''"
        @click="iconColor = preset.value"
      >
        <span class="h-3 w-3 rounded-full" :style="{ backgroundColor: preset.value }" />
        {{ preset.tone }}
      </button>
    </div>

    <div class="grid gap-3 md:grid-cols-2">
      <GrInput v-model="tooltipText" placeholder="Tooltip text" />
      <GrInput v-model="iconColor" placeholder="var(--gr-warning) / #f59e0b" />
    </div>
  </div>
</template>

Шкала размеров

Масштабируются и панель, и дефолтная триггер-иконка; предельная ширина растёт вместе с кеглем, чтобы строка не рвалась.

size="xs"
size="sm"
size="md"
size="lg"

Sizes
<script setup lang="ts">
import { GrTooltip } from '@feugene/granularity'

const sizes = ['xs', 'sm', 'md', 'lg'] as const
</script>

<template>
  <div class="flex flex-wrap items-center gap-6">
    <div v-for="size in sizes" :key="size" class="flex items-center gap-2">
      <span class="text-xs font-semibold text-[var(--gr-muted-fg)]">
        size="{{ size }}"
      </span>

      <GrTooltip :size="size" text="Billing runs on the first day of each month." />
    </div>
  </div>
</template>

Сторона, задержка и disabled

Подсказка встаёт с любой стороны, openDelay убирает мигание на плотной панели кнопок, а слот-триггер не добавляет второй остановки Tab.

Обёртка не добавляет второй остановки Tab: описание уезжает на саму кнопку, и до подсказки доходит и клавиатура, и скринридер.

Placement
<script setup lang="ts">
import { ref } from 'vue'

import type { GrTooltipPlacement } from '@feugene/granularity'
import { GrButton, GrSegmented, GrSwitch, GrTooltip } from '@feugene/granularity'

const placement = ref<GrTooltipPlacement>('top')
const openDelay = ref(400)
const disabled = ref(false)

const placements = [
  { value: 'top', label: 'top' },
  { value: 'right', label: 'right' },
  { value: 'bottom', label: 'bottom' },
  { value: 'left', label: 'left' },
]

const actions = ['Merge', 'Revert', 'Rebase'] as const
</script>

<template>
  <div class="grid gap-5">
    <div class="flex flex-wrap items-center gap-4">
      <GrSegmented v-model="placement" size="sm" :options="placements" />

      <label class="flex items-center gap-2 text-sm text-[var(--gr-muted-fg)]">
        <GrSwitch v-model="disabled" size="sm" />
        disabled
      </label>
    </div>

    <div class="rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-6">
      <div class="flex flex-wrap items-center gap-2">
        <GrTooltip
          v-for="action in actions"
          :key="action"
          :placement="placement"
          :open-delay="openDelay"
          :close-delay="120"
          :disabled="disabled"
          :text="`${action} — задержка ${openDelay} мс, подсказка не мигает при проведении курсором`"
        >
          <GrButton size="sm" variant="outline">
            {{ action }}
          </GrButton>
        </GrTooltip>
      </div>

      <p class="mt-4 text-sm text-[var(--gr-muted-fg)]">
        Обёртка не добавляет второй остановки Tab: описание уезжает на саму
        кнопку, и до подсказки доходит и клавиатура, и скринридер.
      </p>
    </div>
  </div>
</template>

Документация компонентаВсе компоненты