Português Brasil

Usando Popper como Tooltip com TypeScript e styled-component

July 29, 2020 • ☕️ 5 min de leitura

Neste artigo, falo um pouco sobre minha jornada de fazer uma dica de ferramenta com popper


Estava precisando fazer uma customização de um Tooltip, e surgiu a possibilidade de usar o Popper(https://popper.js.org). Infelizmente, para mim, era minha primeira vez usando ele com React. Nunca tive a oportunidade de trabalhar com ele.

Então minha primeira ação, foi claro ir fundo na documentação do dele. Logo de cara ele sugere usar a versão com Hooks, uma versão antiga mostrava como usa-lo usando Render Props. Para mim Hooks é mais que suficiente mesmo. O problema veio quando abri a documentação para ver os exemplos de uso.

Particularmente sou mais apegado a exemplos do que a textos destrinchando o código. Nesta página https://popper.js.org/react-popper/v2/ você vai encontrar um único exemplo de Hooks bem completo ou o mais completo que encontrei na documentação oficial.

import React, { useState } from 'react';
import { usePopper } from 'react-popper';

const Example = () => {
  const [referenceElement, setReferenceElement] = useState(null);
  const [popperElement, setPopperElement] = useState(null);
  const [arrowElement, setArrowElement] = useState(null);
  const { styles, attributes } = usePopper(referenceElement, popperElement, {
    modifiers: [{ name: 'arrow', options: { element: arrowElement } }],
  });

  return (
    <>
      <button type="button" ref={setReferenceElement}>
        Reference element
      </button>

      <div ref={setPopperElement} style={styles.popper} {...attributes.popper}>
        Popper element
        <div ref={setArrowElement} style={styles.arrow} />
      </div>
    </>
  );
};

Sabendo que a lib tem muito mais a oferecer isso não me satisfez, então foi aqui que comecei a busca de iluminação. Para resolver meu problema completamente eu precisava incluir styled-components e Typescript.Então foi aqui que encontrei esse artigo: https://medium.com/javascript-in-plain-english/usepopper-with-styled-components-for-react-react-popper-2-568284d029bf (Infelizmente ainda no medium e ainda por cima um post com wall) Você pode encontrar o outline dele aqui(https://outline.com/sf3jLE). A onde ele mostra e explica de for muito intuitiva como usa-lo e ele ajuda exemplificar alguns porquês. E aqui você vai encontrar o código completo rodando https://codesandbox.io/s/blue-worker-w9rtu?file=/src/App.js

import React, { useState, useRef } from 'react';
import { usePopper } from 'react-popper';
import styled from 'styled-components';

const Dropdown = () => {
  const [showPopper, setShowPopper] = useState(false);

  const buttonRef = useRef(null);
  const popperRef = useRef(null);
  // the ref for the arrow must be a callback ref
  const [arrowRef, setArrowRef] = useState(null);

  const { styles, attributes } = usePopper(buttonRef.current, popperRef.current, {
    modifiers: [
      {
        name: 'arrow',
        options: {
          element: arrowRef,
        },
      },
      {
        name: 'offset',
        options: {
          offset: [0, 10],
        },
      },
    ],
  });

  return (
    <Page>
      <Info>
        <p>Scroll down and right to find the popper!</p>
      </Info>
      <Button ref={buttonRef} onClick={() => setShowPopper(!showPopper)}>
        Click Me
      </Button>
      {showPopper ? (
        <PopperContainer ref={popperRef} style={styles.popper} {...attributes.popper}>
          <div ref={setArrowRef} style={styles.arrow} id="arrow" />
          <p>I'm a popper</p>
        </PopperContainer>
      ) : null}
    </Page>
  );
};

// a basic page styling
const Page = styled.div`
  width: 250%;
  height: 250%;
  display: flex;
  align-items: center;
  justify-content: space-around;
`;

const Info = styled.div`
  background: lightgreen;
  padding: 1rem;
  position: absolute;
  top: 20px;
  left: 20px;
`;

const Button = styled.button`
  background: lightblue;
  border: none;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
  width: 150px;
  height: 50px;
  outline: none;
`;

const PopperContainer = styled.div`
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
  border-radius: 5px;
  background-color: white;
  padding: 20px;
  text-align: center;

  #arrow {
    position: absolute;
    width: 10px;
    height: 10px;
    &:after {
      content: ' ';
      background-color: white;
      box-shadow: -1px -1px 1px rgba(0, 0, 0, 0.1);
      position: absolute;
      top: -25px; // padding + popper height
      left: 0;
      transform: rotate(45deg);
      width: 10px;
      height: 10px;
    }
  }

  &[data-popper-placement^='top'] > #arrow {
    bottom: -30px;
    :after {
      box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
    }
  }
`;

Ótimo só precisava incluir algum Typescript nesta mistura. Minha única dificuldade foi entender o porquê a lib precisava de useState ao invés de useRef no caso do arrow, ainda não consegui essa resposta, mas aparentemente é porque em alguns momentos, não é possível determinar o target do arrow, por causa do próprio funcionamento do useRef que inicialmente é definido com null então no primeiro render, não é possível saber a direção que deve apontar. O uso do useState melhorar neste sentido. Segundo o que foi referenciado neste comentário: https://github.com/popperjs/react-popper/issues/354#issuecomment-613937717

Para mim é bom o suficiente entretanto, ref={setArrowRed} meu Typescript estava reclamando desta sessão

<div ref={setArrowRed} style={styles.arrow} className="arrow" />

O erro era:

Type 'Dispatch<SetStateAction<null>>' is not assignable to type 'string | ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null | undefined'.
  Type 'Dispatch<SetStateAction<null>>' is not assignable to type '(instance: HTMLDivElement | null) => void'.
    Types of parameters 'value' and 'instance' are incompatible.
      Type 'HTMLDivElement | null' is not assignable to type 'SetStateAction<null>'.
        Type 'HTMLDivElement' is not assignable to type 'SetStateAction<null>'.
          Type 'HTMLDivElement' provides no match for the signature '(prevState: null): null'

A sacada foi tipar o useState assim:

- const [arrowRef, setArrowRed] = useState(null);
+ const [arrowRef, setArrowRed] = useState<HTMLDivElement | null>(null);

Isso resolveu 100% dos meus problemas. Você conseguirá ver o resultado neste codesandbox: https://codesandbox.io/s/usepopper-styled-components-xblqg?file=/src/components/Tooltip.tsx:317-390


Se isso lhe ajudou de alguma forma, comente ou compartilhe, caso tenha uma sugestão, não exite em me procurar.


David Costa

Frontend Enginneer, mantenedor do projeto withmoney, do pacote do fastexpress e trabalha na Papa