GrTooltip
Берут, когда подпись к иконочной кнопке.
Когда брать
- подпись к иконочной кнопке — единственный способ дать имя контролу, у которого нет видимого текста;
- расшифровка сокращения или статуса — короткий текст, который иначе занял бы место в раскладке;
- подсказка появляется и по фокусу — не только по наведению, поэтому она достижима с клавиатуры;
- текста немного — одна-две строки: длинный текст в подсказке нельзя ни выделить, ни прокрутить.
Когда взять другое
| Нужно | Берите |
|---|---|
| Внутри нужен интерактив: ссылка, кнопка, поле | 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
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
open | boolean | undefined | undefined | Управляемая видимость. Без неё компонент ведёт видимость сам. |
disabled | boolean | undefined | false | Подсказка не показывается ничем — ни курсором, ни фокусом, ни `v-model:open`. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Размер панели и дефолтной триггер-иконки. |
placement | Placement | undefined | "top" | Предпочтительная сторона панели; `flip` может её изменить. |
offsetPx | number | undefined | undefined | Зазор между триггером и панелью, px. |
openDelay | number | undefined | 0 | Задержка перед показом, мс. Спасает от мигания при проведении курсором по панели кнопок. |
closeDelay | number | undefined | 0 | Задержка перед скрытием, мс. |
text | string | undefined | undefined | Текст подсказки. Разметка вместо текста — слот `#content`. |
iconColor | string | undefined | "var(--gr-muted-fg)" | Цвет триггер-иконки (CSS color). По умолчанию — `var(--gr-muted-fg)`. |
Slots
| Slot | Type | Описание |
|---|---|---|
default | any | Триггер. По умолчанию — иконка info. |
content | any | Содержимое подсказки; заменяет проп `text`. |
Events
| Event | Type | Описание |
|---|---|---|
update:open | [value: boolean] | — |
Примеры 5
Подсказка рядом с подписью поля
Самый частый сценарий для GrTooltip — короткое пояснение рядом с label или small helper-control.
<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.
<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.
icon-color="var(--gr-warning)" <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>Шкала размеров
Масштабируются и панель, и дефолтная триггер-иконка; предельная ширина растёт вместе с кеглем, чтобы строка не рвалась.
<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: описание уезжает на саму кнопку, и до подсказки доходит и клавиатура, и скринридер.
<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>