Кастомные триггеры на основе IntersectionObserver

Tippy.js предоставляет гибкий механизм для создания всплывающих подсказок (tooltip), поддерживая различные события триггера, такие как mouseenter, click, focus. Однако иногда стандартных событий недостаточно, и требуется кастомный триггер, основанный на наблюдении за элементом в видимой области окна браузера. Для таких случаев идеально подходит IntersectionObserver.

IntersectionObserver позволяет отслеживать момент появления элемента на экране и автоматически инициировать открытие или закрытие тултипа. Это особенно полезно для ленивой загрузки подсказок, подсказок в каруселях, списках или при работе с бесконечной прокруткой.


Создание базового кастомного триггера

Пример простого тултипа с использованием IntersectionObserver:

import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';

const target = document.querySelector('#my-element');

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      tooltip.show();
    } else {
      tooltip.hide();
    }
  });
}, {
  threshold: 0.5 // Элемент считается видимым, если 50% площади видны
});

const tooltip = tippy(target, {
  content: 'Подсказка при появлении на экране',
  trigger: 'manual', // Обязательный параметр для кастомного управления
});

observer.observe(target);

Ключевые моменты:

  • trigger: 'manual' отключает стандартные события и позволяет управлять тултипом программно через методы .show() и .hide().
  • Параметр threshold определяет, насколько элемент должен быть видим, чтобы сработал триггер. Можно задать массив, например [0, 0.25, 0.5, 1], чтобы отслеживать различные состояния видимости.
  • Использование observer.observe(target) инициирует слежение за элементом.

Настройка поведения при множественных элементах

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

const items = document.querySelectorAll('.product-card');

items.forEach(item => {
  const tooltip = tippy(item, {
    content: 'Информация о продукте',
    trigger: 'manual',
  });

  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      entry.isIntersecting ? tooltip.show() : tooltip.hide();
    });
  }, { threshold: 0.5 });

  observer.observe(item);
});

Важные детали:

  • Каждый элемент получает свой экземпляр тултипа, что предотвращает конфликты при показе нескольких подсказок одновременно.
  • Можно комбинировать с delay в Tippy.js, чтобы добавлять плавное появление при скролле.

Использование кастомных событий для Tippy.js

Tippy.js позволяет полностью интегрировать любые кастомные события с помощью tippy.setProps() и tippy.state. Пример:

const tooltip = tippy(target, {
  content: 'Кастомный триггер через IntersectionObserver',
  trigger: 'manual',
  onShow(instance) {
    console.log('Тултип показан', instance);
  },
  onHide(instance) {
    console.log('Тултип скрыт', instance);
  }
});

const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    entry.isIntersecting
      ? tooltip.setProps({ content: 'Элемент на экране' }) && tooltip.show()
      : tooltip.hide();
  });
}, { threshold: 0.5 });

observer.observe(target);

Особенности:

  • onShow и onHide позволяют подключать анимации, логику аналитики или динамическую подгрузку контента.
  • setProps дает возможность менять контент или другие параметры тултипа динамически при пересечении с viewport.

Оптимизация производительности

При использовании IntersectionObserver с большим количеством элементов важно учитывать:

  1. Один observer для нескольких элементов: Можно создать один экземпляр IntersectionObserver и передавать в него множество элементов через observer.observe(element), вместо создания отдельного observer на каждый элемент. Это снижает нагрузку на браузер.
const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    const tooltip = entry.target._tippy;
    entry.isIntersecting ? tooltip.show() : tooltip.hide();
  });
}, { threshold: 0.5 });

document.querySelectorAll('.item').forEach(item => {
  const tooltip = tippy(item, { content: 'Информация', trigger: 'manual' });
  item._tippy = tooltip;
  observer.observe(item);
});
  1. Debounce для скролла: IntersectionObserver сам оптимизирован для работы со скроллом, но при сложных анимациях можно использовать throttle или debounce на callback функции.

  2. Управление удалением элементов: При удалении элементов из DOM важно вызывать observer.unobserve(element) для предотвращения утечек памяти.


Продвинутое использование с динамическим контентом

Для страниц с подгрузкой контента по скроллу (infinite scroll) можно автоматически привязывать тултипы к новым элементам:

const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (!entry.target._tippy) {
      const tooltip = tippy(entry.target, { content: 'Новая карточка', trigger: 'manual' });
      entry.target._tippy = tooltip;
    }
    entry.isIntersecting ? entry.target._tippy.show() : entry.target._tippy.hide();
  });
}, { threshold: 0.5 });

const observeNewItems = (nodes) => {
  nodes.forEach(node => observer.observe(node));
};

// Пример динамического добавления
const container = document.querySelector('#list');
const newNode = document.createElement('div');
newNode.classList.add('item');
container.appendChild(newNode);
observeNewItems([newNode]);

Преимущества:

  • Автоматическое создание тултипов при добавлении новых элементов.
  • Полная совместимость с кастомными триггерами, без изменения исходной логики Tippy.js.

Итоговые рекомендации по кастомным триггерам

  • Всегда использовать trigger: 'manual' для полного контроля.
  • Оптимизировать создание observer для множества элементов.
  • Использовать setProps и события onShow/onHide для динамических изменений контента.
  • Учитывать threshold для точного срабатывания видимости.
  • Не забывать очищать наблюдателей при удалении элементов из DOM.

Такой подход делает Tippy.js максимально гибким для сложных интерфейсов, где стандартные триггеры hover или click не подходят, а управление через viewport является ключевым.