Pull to refresh

React, всплывающие подсказки (tooltips), для самых маленьких

Reading time11 min
Views14K

Что такое всплывающие подсказки?

Если дословно, то сам термин «tooltips» — английского происхождения и переводится как «советы по инструментам». Под этим термином согласно Википедии понимают информационную подсказку или подсказку, в которой при наведении курсора мыши на элемент экрана в текстовом поле отображается информация об этом элементе, например, описание функции кнопки, обозначение аббревиатуры и т. д.

Всплывающая подсказка (tooltip) может выглядеть например вот так:

Вы наверняка не раз с ними сталкивались, так что, всплывающие подсказки весьма распространенная штука для интерфейсов web приложений.

React — классный фреймворк для разработки web приложений. И на нем пишет множество разработчиков, в том числе и я. А поскольку у разработчика проектов как правило много, то хорошо было бы компоненты интерфейсов в своих проектах переиспользовать, с одного в другом. Для скорости разработки, классно иметь набор таких универсальных компонентов для интерфейса. Чтобы в этих компонентах поддерживался их базовый функционал, их можно было легко стилизовать под конкретный проект и по минимуму использовать сторонние библиотеки для их легковесности.

Давайте так и поступим — напишем самостоятельно универсальный и легко переиспользуемый компонент для всплывающих подсказок.

Писать будем на React + TypeScript, для стилизации будем использовать css.modules. В дальнейшем для плавной анимации появления и исчезновения всплывающих подсказок еще подключим React библиотеку «react‑transition‑group», но это потом.

А сейчас посмотрим на то, что мы хотим сделать. Наш будущий tooltip должен будет принять в себя произвольный элемент интерфейса в виде JSX компонента React, и при наведении указателя мыши на этот целевой элемент интерфейса — показать для него всплывающую подсказку.

Для начала создадим наш новый React компонент — ToolTipComponent.

ToolTipComponent.tsx

import React from 'react';
import classes from './ToolTipComponent.module.css';

const ToolTipComponent: React.FC = () => {  
  return (    
    <div className={classes.container}>      
      ToolTip-Component    
    </div>  
  );
};

export default ToolTipComponent;

Его стилизацию определим в файле — ToolTipComponent.module.css, она у нас пока состоит всего из одного класса:

ToolTipComponent.module.css

.container {  
  display: flex;
}

Вставляем наш tooltip в компонент App:

import React from 'react';
import classes from './App.module.css';
import ToolTipComponent from './Components/ToolTipComponent/ToolTipComponent';

function App() {  
  return (    
    <div className={classes.container}>      
      <ToolTipComponent />    
    </div>  
  );
}

export default App;

Наше приложение теперь выглядит так:

Для того, чтобы внутри компонента ToolTipComponent мы могли отобразить целевой компонент при наведении на который мы хотим видеть всплывающую подсказку, мы должны передать этот целевой компонент как props — children в наш tooltip.

Так же нам понадобится текст для самой всплывающей подсказки, передадим его как props — text. Внутри компонента обернем текст подсказки в тег <div>, для того чтобы стилизовать его и спозиционировать относительно его родительского контейнера.

Теперь наш ToolTipComponent выглядит так:

ToolTipComponent.tsx

import React, { ReactElement } from 'react';
import classes from './ToolTipComponent.module.css';

type PropsType = {
  children: ReactElement;
  text: string;
};

const ToolTipComponent: React.FC  <PropsType>= ({children, text}) => {
  return (
    <div className={classes.container}>
      {children}
      <div className={classes.tooltip}>
        {text}
      </div>
    </div>
  );
};

export default ToolTipComponent;

ToolTipComponent.module.css

.container {
  position: relative;
  display: flex;
}

.tooltip {
  position: absolute;
  width: 180px;
  padding: 4px 12px;
  margin-left: calc(100%);
  justify-content: center;
  color: #FFFFFF;
  background-color: #FF8E00;
  border-radius: 12px ;
  text-align: center;
  white-space: pre-line;
  font-weight: 700 ;
  pointer-events: none;
}

Создадим и стилизуем произвольный компонент. Я для нашего мини приложения создал ButtonComponent. Это самая обыкновенная кнопка, сделанная из тэга <div>, кнопка ничего не будет делать. Однако это будет тот целевой компонент, в который мы обернем в наш в tooltip, для того чтобы когда мы наведем указатель мыши на эту кнопку, мы могли увидеть всплывающую подсказку.

ButtonComponent.tsx

import React from 'react';
import classes from './ButtonComponent.module.css';

const ButtonComponent: React.FC = () => {  
  return (    
    <div className={classes.container}>      
      Нажми на меня    
    </div>  
  );
};

export default ButtonComponent;

Стилизация кнопки прописана здесь:

ButtonComponent.module.css

.container {  
  display: flex;  
  width: 80px;  
  justify-content: center;  
  align-items: center;  
  padding: 6px 24px;  
  text-align: center;  
  color: #FFFFFF;  
  background-color: #4A90E2;  
  font-size: 18px;  
  font-weight: 600;  
  border-radius: 12px;  
  cursor: pointer;
}

Экран нашего приложения теперь выглядит так — кнопка и рядом с ней спозиционирована наша всплывающая подсказка.

Управлять поведением нашей всплывающей подсказки мы будем через состояние React, используя hook — useState. В моменты, когда указатель мыши заходит на компонент tooltip, мы будем подсказку отображать. Как только указатель мыши компонент tooltip покидает, мы будем подсказку скрывать. Для чего используем события мыши для компонента React — «onMouseEnter» и «onMouseLeave».

Теперь наш TooTtipComponent выглядит так

ToolTipComponent.tsx

import React, { ReactElement, useState } from 'react';
import classes from './ToolTipComponent.module.css';

type PropsType = {
  children: ReactElement;
  text: string;
};

const ToolTipComponent: React.FC<PropsType> = ({ children, text }) => {
  const [showToolTip, setShowToolTip] = useState(false);

  const onMouseEnterHandler = () => {
    setShowToolTip(true);
  };

  const onMouseLeaveHandler = () => {
    setShowToolTip(false);
  };

  return (
    <div className={classes.container} onMouseEnter={onMouseEnterHandler} onMouseLeave={onMouseLeaveHandler}>
      {children}
      {showToolTip && <div className={classes.tooltip}>{text}</div>}
    </div>
  );
};

export default ToolTipComponent;

и теперь наш tooltip уже динамически может показывать подсказку для нашей кнопки.

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

Поправим наш tooltip, так что бы была задержка в 0,75 секунды, перед тем как появится всплывающая подсказка. За эти 0,75 секунды, пользователь успеет нажать на кнопку и подсказка для него не появится.

Чтобы реализовать подобный функционал, мы используем метод таймера setTimeout из браузерного API. При заходе указателя мыши на целевой компонент будет запускаться функция, обеспечивающая задержку показа текста всплывающей подсказки, а в случае выхода указателя мыши за пределы целевого компонента мы будем сбрасывать таймер setTimeout и скрывать текст всплывающей подсказки. Чтобы не потерять идентификатор setTimeOut между перерендерами нашего React компонента ToolTipComponrent, мы поместим значение идентификатора метода setTimeout в поле current хука React — useRef. Поле current хука useRef является универсальным хранилищем в React компонентах для данных, которые мы хотим сохранять между их перерендерами.

И теперь наш код нашего tooltip компонента выглядит так:

ToolTipComponent.tsx

import React, { ReactElement, useRef, useState } from 'react';
import classes from './ToolTipComponent.module.css';

type PropsType = {
  children: ReactElement;
  text: string;
};

const ToolTipComponent: React.FC<PropsType> = ({ children, text }) => {
  const refSetTimeout = useRef<NodeJS.Timeout>();
  const [showToolTip, setShowToolTip] = useState(false);

  const onMouseEnterHandler = () => {
    refSetTimeout.current = setTimeout(() => {
      setShowToolTip(true);
    }, 750);
  };

  const onMouseLeaveHandler = () => {
    clearTimeout(refSetTimeout.current);
    setShowToolTip(false);
  };

  return (
    <div className={classes.container} onMouseEnter={onMouseEnterHandler} onMouseLeave={onMouseLeaveHandler}>
      {children}
      {showToolTip && <div className={classes.tooltip}>{text}</div>}
    </div>
  );
};

export default ToolTipComponent;

Как видно на видео, наша идея с задержкой появления текста подсказки в 0,75 секунды работает. Всплывающая подсказка появляется только спустя какое‑то время, после того как пользователь навел указатель мыши на кнопку. Если бы он нажал на кнопку или увел указатель за пределы кнопки быстрее, чем установленная нами задержка в 0,75 секунды, то всплывающая подсказка для него так бы и не появилась.

Но для того, чтобы наш компонент всплывающих подсказок был универсальным и мы могли переиспользовать его в разных проектах, нам необходимо добавить возможность стилизации всплывающей подсказки. Сделаем это через передачу еще одного props — customClass в наш компонент.

И теперь наш код компонент всплывающих подсказок выглядит так:

ToolTipComponent.tsx

import React, { ReactElement, useRef, useState } from 'react';
import classes from './ToolTipComponent.module.css';

type PropsType = {
  children: ReactElement;
  text: string;
  customClass?: string;
};

const ToolTipComponent: React.FC<PropsType> = ({ children, text, customClass }) => {
  const refSetTimeout = useRef<NodeJS.Timeout>();
  const [showToolTip, setShowToolTip] = useState(false);
  const toolTipClasses = customClass ? `${classes.tooltip} ${customClass}` : `${classes.tooltip}`;

  const onMouseEnterHandler = () => {
    refSetTimeout.current = setTimeout(() => {
      setShowToolTip(true);
    }, 750);
  };

  const onMouseLeaveHandler = () => {
    clearTimeout(refSetTimeout.current);
    setShowToolTip(false);
  };

  return (
    <div className={classes.container} onMouseEnter={onMouseEnterHandler} onMouseLeave={onMouseLeaveHandler}>
      {children}
      {showToolTip && <div className={toolTipClasses}>{text}</div>}
    </div>
  );
};

export default ToolTipComponent;

Подключим.customStyle — передав его в props нашему ToolTipComponent:

App.tsx

import React from 'react';
import classes from './App.module.css';
import ToolTipComponent from './Components/ToolTipComponent/ToolTipComponent';
import ButtonComponent from './Components/ButtonComponent/ButtonComponent';

function App() {
  return (
    <div className={classes.container}>
      <ToolTipComponent text={'Я подсказка'} customClass={classes.toolTipCustom}>
        <ButtonComponent />
      </ToolTipComponent>
    </div>
  );
}

export default App;

.customClass для нашего нового стиля подсказки выглядит так:

App.module.css

.container {
  display: flex;
  padding: 64px;
}

.toolTipCustom {
  display: flex;
  top: -5px;
  left: 32px;
  height: 48px;
  padding: 8px 32px;
  align-items: center;
  background-color: #1e9f00;
  color: #fdfa65;
  border-radius: 50%;
}

Вот как теперь выглядит наша всплывающая подсказка. Она стилизована и спозиционированна точно так как мы ее описали в css классе.toolTipCustom

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

И этот вполне себе уже рабочий компонент, однако совсем несложно улучшить его еще.

С помощью библиотеки «react‑transition‑group» предоствляемой командой создателей React, мы добавим к нашей всплывающей подсказке возможность появляться плавно, выезжать со стороны и затем плавно исчезать. И для придания всех этих расширенных свойств нашей всплывающей подсказке мы используем компонент «CSSTransition» из библиотеки «react‑transition‑group»

Сначала установим из саму библиотеку из npm и затем подключим «CSSTransition» в наш tooltip компонент. Заметьте, что вместо условия по которому мы раньше разрешали рендеринг подсказки, теперь импортированный компонент «CSSTransition» возьмет на себя обязанность контролировать отображение нашей всплывающей подсказки, а так же будет применять стилизацию для анимации которую мы сейчас реализуем.

ToolTipComponent.tsx

import React, { ReactElement, useRef, useState } from 'react';
import classes from './ToolTipComponent.module.css';
import { CSSTransition } from 'react-transition-group';

type PropsType = {
  children: ReactElement;
  text: string;
  customClass?: string;
};

const transitionClasses = {
  enter: classes.exampleEnter,
  enterActive: classes.exampleEnterActive,
  exit: classes.exampleExit,
  exitActive: classes.exampleExitActive,
};

const ToolTipComponent: React.FC<PropsType> = ({ children, text, customClass }) => {
  const refSetTimeout = useRef<NodeJS.Timeout>();
  const [showToolTip, setShowToolTip] = useState(false);
  const toolTipClasses = customClass ? `${classes.tooltip} ${customClass}` : `${classes.tooltip}`;


  const onMouseEnterHandler = () => {
    refSetTimeout.current = setTimeout(() => {
      setShowToolTip(true);
    }, 750);
  };

  const onMouseLeaveHandler = () => {
    clearTimeout(refSetTimeout.current);
    setShowToolTip(false);
  };

  return (
    <div className={classes.container} onMouseEnter={onMouseEnterHandler} onMouseLeave={onMouseLeaveHandler}>
      {children}
      <CSSTransition in={showToolTip} timeout={750} classNames={transitionClasses} unmountOnExit>
        <div className={toolTipClasses}>{text}</div>
      </CSSTransition>
    </div>
  );
};

export default ToolTipComponent;

Для стилизации анимации у компонента «CSSTransition» задействуется его 4 внутренних класса:

  • enter — как компонент выглядит на начале анимации появления;

  • enterActive — как компонент выглядит в процессе анимации появления;

  • exit — как компонент выглядит на конце анимации исчезновения;

  • exitActive — как компонент выглядит в процессе анимации исчезновения.

Зададим в css классах для нашей всплывающей подсказки плавное ее появление через свойство «opacity,» и такой же плавный спуск сверху через transform: translateY().

ToolTipComponent.module.css

.container {
  position: relative;
  display: flex;
}

.tooltip {
  position: absolute;
  width: 180px;
  padding: 4px 12px;
  margin-left: calc(100%);
  justify-content: center;
  color: #FFFFFF;
  background-color: #FF8E00;
  border-radius: 12px ;
  text-align: center;
  white-space: pre-line;
  font-weight: 700 ;
  pointer-events: none;
}

.exampleEnter {
  opacity: 0;
  transform:translateY(-100%);
}
.exampleEnterActive {
  opacity: 1;
  transform:translateY(0);
  transition: opacity 350ms, transform 350ms;
}
.exampleExit {
  opacity: 1;
  transform:translateY(0);
}
.exampleExitActive {
  opacity: 0;
  transform:translateY(-100%);
  transition: opacity 350ms, transform 350ms;
}

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

Поигравшись со стилями класса «.toolTipCustom» в файле App.module.css вы легко настроите внешний вид вашей всплывающей подсказки. А поигравшись со стилями «.exampleEnter», «.exampleEnterActive», «.exampleExit», «.exampleExitActive» в файле ToolTipComponent.module.cssвы легко настроите любую анимацию появления и исчезновения вашей подсказки. Вы можете менять время задержки, время анимации, стороны появления и исчезновения, цвета, размеры, шрифты и пользоваться всей мощью стилизации через css классы, что нам предоставляют современные браузеры.

Вы свободны экспериментировать с кодом приведенным в данной статье и использовать его в своих интересах и проектах.

Код приведенный в статье доступен на GitHub.

P. S. — Статья написана в память о моей классной работе в 2021–2023 году в компании «E‑ngineers», с коллегами которые много чему научили меня за эти два года которые мы программировали вместе!!!

С уважением, alexeyk500.

Tags:
Hubs:
Total votes 2: ↑1 and ↓10
Comments16

Articles