CCCC
CCCC

Reputation: 6461

React - how to click outside to close the tooltip

This is my current tooltip.

I am using react-power-tooltip

When I click the button, I can close the tooltip.

But I want to close the tooltip when I click outside the tooltip.

How am I supposed to do it?

enter image description here

App.js

import "./styles.css";
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
import TooltipList from "./TooltipList";
import { useState } from "react";

export default function App() {
  const [showTooltip, setShowTooltip] = useState(true);
  return (
    <div className="App">
      <button
        className="post-section__body__list__item__right__menu-btn"
        onClick={() => {
          setShowTooltip((x) => !x);
        }}
        style={{ position: "relative" }}
      >
        <MoreHorizIcon />
        <TooltipList show={showTooltip} />
      </button>
    </div>
  );
}

TooltipList

import React from "react";
import Tooltip from "react-power-tooltip";

const options = [
  {
    id: "edit",
    label: "Edit"
  },
  {
    id: "view",
    label: "View"
  }
];

function Tooptip(props) {
  const { show } = props;
  return (
    <Tooltip
      show={show}
      position="top center"
      arrowAlign="end"
      textBoxWidth="180px"
      fontSize="0.875rem"
      fontWeight="400"
      padding="0.5rem 1rem"
    >
      {options.map((option) => {
        return (
          <div
            className="tooltop__option d-flex align-items-center w-100"
            key={option.id}
          >
            {option.icon}
            <span style={{ fontSize: "1rem" }}>{option.label}</span>
          </div>
        );
      })}
    </Tooltip>
  );
}

export default Tooptip;

CodeSandbox:
https://codesandbox.io/s/optimistic-morning-m9eq3?file=/src/App.js

Update 1:
I update the code based on the answer.
It can now click outside to close, but if I click the button to close the tooltip, it's not working.

App.js

import "./styles.css";
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
import TooltipList from "./TooltipList";
import { useState } from "react";

export default function App() {
  const [showTooltip, setShowTooltip] = useState(true);
  return (
    <div className="App">
      <button
        className="post-section__body__list__item__right__menu-btn"
        onClick={() => {
          setShowTooltip((x) => !x);
        }}
        style={{ position: "relative" }}
      >
        <MoreHorizIcon />
        <TooltipList
          show={showTooltip}
          onClose={() => {
            setShowTooltip();
          }}
        />
      </button>
    </div>
  );
}

TooltipList.js

import React, { useEffect, useRef } from "react";
import Tooltip from "react-power-tooltip";

const options = [
  {
    id: "edit",
    label: "Edit"
  },
  {
    id: "view",
    label: "View"
  }
];

function Tooptip(props) {
  const { show, onClose } = props;

  const containerRef = useRef();
  useEffect(() => {
    if (show) {
      containerRef.current.focus();
    }
  }, [show]);

  return (
    <div
      style={{ display: "inline-flex" }}
      ref={containerRef}
      tabIndex={0}
      onBlur={(e) => {
        onClose();
      }}
    >
      <Tooltip
        show={show}
        position="top center"
        arrowAlign="end"
        textBoxWidth="180px"
        fontSize="0.875rem"
        fontWeight="400"
        padding="0.5rem 1rem"
      >
        {options.map((option) => {
          return (
            <div
              className="tooltop__option d-flex align-items-center w-100"
              key={option.id}
            >
              {option.icon}
              <span style={{ fontSize: "1rem" }}>{option.label}</span>
            </div>
          );
        })}
      </Tooltip>
    </div>
  );
}

export default Tooptip;

Codesandbox
https://codesandbox.io/s/optimistic-morning-m9eq3?file=/src/App.js:572-579

Upvotes: 1

Views: 8131

Answers (3)

Sanish Joseph
Sanish Joseph

Reputation: 2256

Here is a crazy little idea. I wrapped your component in an inline-flex div and gave it focus on load. Then added an onBlur event which will hide the menu if you click anywhere else. This can be used if you don't want to give focus on any other element on the page.

https://codesandbox.io/s/epic-kapitsa-yh7si?file=/src/App.js:0-940

import "./styles.css";
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
import TooltipList from "./TooltipList";
import { useRef, useState, useEffect } from "react";

export default function App() {
  const [showTooltip, setShowTooltip] = useState(true);
  const containerRef = useRef();

  useEffect(() => {
    containerRef.current.focus();
  }, []);
  return (
    <div className="App">
      <div
         style={{ display: "inline-flex" }}
        ref={containerRef}
        tabIndex={0}
        onBlur={(e) => {
          debugger;
          setShowTooltip(false);
        }}
      >
        <button
          className="post-section__body__list__item__right__menu-btn"
          onClick={() => {
            setShowTooltip((x) => !x);
          }}
          style={{ position: "relative" }}
        >
          <MoreHorizIcon />

          <TooltipList show={showTooltip} />
        </button>
      </div>
    </div>
  );
}

Update 1:

The problem was your button click was called every time you select an item that toggles your state. I have updated the code to prevent that using a useRef that holds a value.

ToolTip:

import React from "react";
import Tooltip from "react-power-tooltip";

const options = [
  {
    id: "edit",
    label: "Edit"
  },
  {
    id: "view",
    label: "View"
  }
];

function Tooptip(props) {
  const { show, onChange } = props;
  return (
    <>
      <Tooltip
        show={show}
        position="top center"
        arrowAlign="end"
        textBoxWidth="180px"
        fontSize="0.875rem"
        fontWeight="400"
        padding="0.5rem 1rem"
      >
        {options.map((option) => {
          return (
            <div
              onClick={onChange}
              className="tooltop__option d-flex align-items-center w-100"
              key={option.id}
            >
              {option.icon}
              <span style={{ fontSize: "1rem" }}>{option.label}</span>
            </div>
          );
        })}
      </Tooltip>
    </>
  );
}

export default Tooptip;

App

import "./styles.css";
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
import TooltipList from "./TooltipList";
import { useRef, useState, useEffect, useCallback } from "react";

export default function App() {
  const [showTooltip, setShowTooltip] = useState(true);
  const [onChangeTriggered, setonChangeTriggered] = useState(false);
  const containerRef = useRef();
  const itemClicked = useRef(false);

  useEffect(() => {
    containerRef.current.focus();
  }, []);
  return (
    <div className="App">
      <div
        style={{ display: "inline-flex" }}
        ref={containerRef}
        tabIndex={0}
        onBlur={(e) => {
          debugger;
          if (!onChangeTriggered) setShowTooltip(false);
        }}
        // onFocus={() => {
        //   setShowTooltip(true);
        // }}
      >
        <button
          className="post-section__body__list__item__right__menu-btn"
          onClick={() => {
            if (!itemClicked.current) setShowTooltip((x) => !x);
            itemClicked.current = false;
          }}
          style={{ position: "relative" }}
        >
          <MoreHorizIcon />

          <TooltipList
            show={showTooltip}
            onChange={useCallback(() => {
              itemClicked.current = true;
            }, [])}
          />
        </button>
      </div>
    </div>
  );
}

https://codesandbox.io/s/epic-kapitsa-yh7si

Enjoy !!

Upvotes: 0

Shiladitya
Shiladitya

Reputation: 12181

As I can see, you are using material-ui for icons, so there is an option known as ClickAwayListner within material-ui

App.js

import "./styles.css";
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import TooltipList from "./TooltipList";
import { useState } from "react";

export default function App() {
  const [showTooltip, setShowTooltip] = useState(true);
  const handleClickAway = () => {
    setShowTooltip(false);
  }
  return (
    <div className="App">
      <ClickAwayListener onClickAway={handleClickAway}>
        <button
          className="post-section__body__list__item__right__menu-btn"
          onClick={e => {
            e.stopPropagation();
            setShowTooltip((x) => !x);
          }}
          style={{ position: "relative" }}
        >
          <MoreHorizIcon />
          <TooltipList show={showTooltip} />
        </button>
      </ClickAwayListener>
    </div>
    
  );
}

Wrap your container with ClickAwayListener

Upvotes: 5

Saigon Bitmaster
Saigon Bitmaster

Reputation: 1

You should add a wrapper element to detect if the click is outside a component then the showTooltip to false at your code: codeSanbox

   import "./styles.css";
   import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
   import TooltipList from "./TooltipList";
   import { useState, useEffect, useRef } from "react";
   
   export default function App() {
     const [showTooltip, setShowTooltip] = useState(true);
     const outsideClick = (ref) => {
       useEffect(() => {
         const handleOutsideClick = (event) => {
           if (ref.current && !ref.current.contains(event.target)) {
             setShowTooltip(false);
           }
         };
         // add the event listener
         document.addEventListener("mousedown", handleOutsideClick);
       }, [ref]);
     };
     const wrapperRef = useRef(null);
     outsideClick(wrapperRef);
     return (
       <div className="App" ref={wrapperRef}>
         <button
           className="post-section__body__list__item__right__menu-btn"
           onClick={() => {
             setShowTooltip((x) => !x);
           }}
           style={{ position: "relative" }}
         >
           <MoreHorizIcon />
           <TooltipList show={showTooltip} />
         </button>
       </div>
     );
   }

Upvotes: 0

Related Questions