Примитивы
37

Tooltip

Пояснение, появляющееся рядом с элементом по наведению или фокусу. Указатель ждёт задержку — задержаться на чём-то ещё не значит захотеть подсказку, — а клавиатура открывает сразу: придя по Tab, читатель уже выбрал. Собственного элемента у тултипа нет, поэтому каждый пакет цепляет его так, чтобы ничего не добавить в дерево: React клонирует ребёнка, Vue — vnode, Svelte цепляется к узлу, Angular — директива, снимающая собственный атрибут после сопоставления. Обёртка портировалась бы всюду и была бы хуже: на flex-элементе она меняла бы вёрстку, которую должна была только пояснить. ⚠️ Тултип на disabled-контроле не открывается никогда, и никакая обёртка с нашей стороны этого не изменит: отключённый элемент вообще не шлёт pointer-события, открывать нечем. Ровно наоборот тому, что нужно: подсказка на отключённой кнопке — это как раз та, что объясняет, *почему* она отключена. Ответ — aria-disabled="true" вместо disabled плюс ранний выход в собственном обработчике. Контрол остаётся фокусируемым и наводимым, объявляет себя отключённым и поддаётся объяснению; стили рисуют его инертным и — с тех пор как это проверили — больше не дают ему подпрыгивать, менять цвет и вдавливаться под указателем.

чистая разметка — библиотеки на странице нет
отрендерил aura-react
отрендерил aura-react
отрендерил aura-react
отрендерил aura-react

Код

index.html
<button type="button" class="aura-btn aura-btn--secondary" aria-describedby="t1">Delete</button>

<div id="t1" role="tooltip" data-state="open" class="aura-popup aura-tooltip">Delete this space</div>
Demo.tsx
import { Button, Group, IconButton, Tooltip } from '@ilomee/aura-react';

/**
 * Наведите указатель или поставьте фокус на любой из контролов. Панель портализуется в
 * `document.body` и позиционируется через `place()`: переворачивается, когда не влезает, и следует
 * за прокруткой. У второго показан `names`: у кнопки-иконки нет текста, поэтому там тултип не
 * дополняет имя — он и есть имя, а быть тем и другим значило бы прозвучать дважды.
 */
export default function Demo() {
  return (
    <Group>
      <Tooltip label="Delete this space">
        <Button variant="secondary">Delete</Button>
      </Tooltip>
      <Tooltip label="Settings" names>
        {/*
          Named here as well as by the tooltip. `names` points `aria-labelledby` at the tooltip
          only while it is open, so an icon-only button with nothing else would be nameless for
          as long as nobody is hovering it — which is most of the time. `aria-labelledby` wins
          while the tooltip is up, so the two never disagree.
        */}
        <IconButton aria-label="Settings">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
            <circle cx="12" cy="12" r="3" />
            <path d="M12 2v4M12 18v4M2 12h4M18 12h4" />
          </svg>
        </IconButton>
      </Tooltip>
    </Group>
  );
}
Demo.vue
<script setup lang="ts">
import { Tooltip, Button } from '@ilomee/aura-vue';
</script>

<template>
  <Tooltip label="Delete this space">
    <Button variant="secondary">Delete</Button>
  </Tooltip>
</template>
Demo.svelte
<script lang="ts">
  import { tooltip, Button } from '@ilomee/aura-svelte';
</script>

<Button variant="secondary" {@attach tooltip({ label: 'Delete this space' })}>Delete</Button>

Аттачмент, а не компонент, поэтому не добавляет элемента: {@attach tooltip({ label })} прямо на триггере.

Используется как [auraTooltip] — директива, которую вы вешаете на свой элемент.

demo.component.ts
<button auraButton variant="secondary" auraTooltip="Delete this space">Delete</button>

Директива снимает собственный атрибут auraTooltip в ngOnInit, поэтому итоговая разметка совпадает с остальными тремя. Angular сопоставляет директивы на этапе компиляции, так что удаление ничего не стоит.

Пропсы

У чистой разметки нет пропсов — классы ниже и есть весь API. Четыре пакета не делают ничего сверх того, что проставляют вам те же самые классы.
ИмяТипПо умолчаниюПримечания
label*stringПлоский текст, не узел: тултип — это доступное имя или описание, а они сводятся к тексту; разметку внутри вспомогательные технологии проигнорируют.
children*ReactElementРовно один элемент, который пробрасывает ref и раскрывает переданные пропсы. Все примитивы Aura так делают.
side'top' | 'right' | 'bottom' | 'left''top'
align'start' | 'center' | 'end''center'
delaynumber400Только для указателя. Фокус не ждёт.
namesbooleanfalseСделать тултип доступным именем триггера (aria-labelledby), а не дополнением к нему (aria-describedby). У кнопки без подписи тултип — единственное имя.
ИмяТипПо умолчаниюПримечания
label*string
defaultслотslotРовно один элемент. Клонируется, а не оборачивается.
side'top' | 'right' | 'bottom' | 'left''top'
align'start' | 'center' | 'end''center'
delaynumber400
namesbooleanfalse
ИмяТипПо умолчаниюПримечания
label*string
side'top' | 'right' | 'bottom' | 'left''top'
align'start' | 'center' | 'end''center'
delaynumber400
namesbooleanfalse
ИмяТипПо умолчаниюПримечания
auraTooltip*stringПодпись, названная по селектору, чтобы <button auraTooltip="Delete"> читался.
side'top' | 'right' | 'bottom' | 'left''top'
align'start' | 'center' | 'end''center'
delaynumber400
namesbooleanfalse

Какие классы отдаёт

Все фреймворки отдают одни и те же классы — именно поэтому четыре библиотеки выглядят одинаково. Их можно использовать напрямую, вообще без библиотеки компонентов.

КлассРольПримечания
.aura-popupбазаОбязательный. Без него модификатор не работает.
.aura-tooltipбазаОбязательный. Без него модификатор не работает.

Токены и правила

components.css
слой
var(--z-tooltip) — above every other float, on purpose
ширина
min(32ch, calc(100vw - 16px)) — a label, not a surface
зазор до якоря
var(--popover-offset)