From 80d81a10c3e3d9fc8de274a32713fc5184c58241 Mon Sep 17 00:00:00 2001
From: Dalton <daltojohnso@users.noreply.github.com>
Date: Thu, 18 Nov 2021 09:30:02 -0800
Subject: [PATCH] Reimplement tooltips using tippy.js (#18880)

* add tippy and react-is

* add tippy styles to vendor.css

we'll probably override most things but it handles
apply the triangle to tooltips

* reimplement Tooltip using tippy

* reimplement ChartTooltip using Tooltip

* add tooltip theme styling

* fix StoreLink styling

* remove TooltipPopover

* add util for easy mapping to innerRef

* use innerRef util with StoreLink target

* react-is type defs

* convert Tooltip into tsx

* convert styled-components util into typescript

* fix Tooltip fallback return

* use Tooltip in Icon over Tooltipify

* remove Tooltipify

* pass ref all the way through Icon

* fix DimensionListItem styling caused by missing ref

* fix ChartTooltip event target bug

* fix unit test by making Icon.tsx grosser

* replace popover() util with tooltip() in cy tests

* fix random broken e2e test

* improve types in Tooltip

* move everything to a separate folder + add tests

* add more examples

* rmv accidental deps

* fix run button styling

* add Link tooltip prop to avoid tooltip on icon

* wrap MetabotLogo in forwardRef

* support reduced motion

* remove flow type from dom.js so we can use in ts files

* pass isEnabled prop to tippy's disabled prop

once you've activated "control mode" in tippy you can't turn it off so
using "visible" as a toggle doesn't work. instead, use disabled.

* fix uncentered tooltip the right way

tippy includes padding/margin in its centering calculations, unlike
tether, so we will need to be careful about adding one-sides
padding/margins to a tooltip target, otherwise the tooltip will appear
offcenter

* workaround for absolutely positioned element

the tooltip appeared on the far end of the target because of a child div
that is absolutely positioned. quick fix is to instead target the icon

* fix positioning of HintIcon tooltip

* lint fix

* add placement prop

* fix styled component tooltip target styling

* place QuestionNotebookButton tooltip below

* Make the NativeQueryButton an actual button

* set tooltip placement to bottom

* lint fix warning
---
 .../ModerationReviewBanner.jsx                |   6 +-
 .../ModerationReviewBanner.styled.jsx         |   4 +-
 .../PermissionsSelect.styled.jsx              |   7 +-
 .../PermissionsTable.styled.jsx               |   9 +-
 .../CollectionHeader.styled.jsx               |   1 +
 frontend/src/metabase/components/Button.jsx   |  41 +++--
 .../src/metabase/components/CopyButton.jsx    |  12 +-
 .../src/metabase/components/ExternalLink.jsx  |  42 ++---
 frontend/src/metabase/components/Icon.tsx     |  49 ++++--
 frontend/src/metabase/components/Link.jsx     |   9 +-
 .../src/metabase/components/MetabotLogo.jsx   |  10 +-
 frontend/src/metabase/components/Popover.css  |  16 ++
 .../src/metabase/components/Tooltip.info.js   |  21 ---
 frontend/src/metabase/components/Tooltip.jsx  | 134 ---------------
 .../components/Tooltip/Tooltip.info.js        |  81 +++++++++
 .../metabase/components/Tooltip/Tooltip.tsx   |  92 +++++++++++
 .../components/Tooltip/Tooltip.unit.spec.js   | 154 ++++++++++++++++++
 .../src/metabase/components/Tooltip/index.ts  |   1 +
 .../metabase/components/TooltipPopover.jsx    |  40 -----
 .../src/metabase/containers/Overworld.jsx     |   5 +-
 frontend/src/metabase/css/vendor.css          |   2 +
 frontend/src/metabase/hoc/Tooltipify.jsx      |  26 ---
 frontend/src/metabase/lib/dom.js              |   2 +-
 .../components/StoreLink/StoreLink.styled.jsx |  10 +-
 .../src/metabase/nav/containers/Navbar.jsx    |  11 +-
 .../query_builder/components/RunButton.jsx    | 115 ++++++-------
 .../components/notebook/NotebookStep.jsx      |   5 +-
 .../notebook/steps/JoinStep.styled.jsx        |   6 +-
 .../components/view/NativeQueryButton.jsx     |  15 +-
 .../view/NativeQueryButton.styled.jsx         |  22 +++
 .../view/QuestionNotebookButton.jsx           |   5 +-
 .../components/view/ViewHeader.jsx            |  13 +-
 .../components/view/ViewHeader.styled.jsx     |  11 --
 .../AddAggregationButton.styled.jsx           |   6 +-
 .../DimensionListItem.styled.jsx              |  13 +-
 .../src/metabase/styled-components/utils.tsx  |  13 ++
 .../components/ChartTooltip.jsx               |  78 +++++----
 .../e2e/helpers/e2e-ui-elements-helpers.js    |   4 +
 .../dashboard/dashboard-drill.cy.spec.js      |   5 +-
 .../scenarios/dashboard/dashboard.cy.spec.js  |   9 +-
 .../drillthroughs/chart_drill.cy.spec.js      |   9 +-
 .../visualizations/line_chart.cy.spec.js      |  13 +-
 ...aps-null-location-wrong-tooltip.cy.spec.js |   4 +-
 .../visualizations/scatter.cy.spec.js         |   6 +-
 package.json                                  |   4 +
 yarn.lock                                     |  28 +++-
 46 files changed, 718 insertions(+), 451 deletions(-)
 delete mode 100644 frontend/src/metabase/components/Tooltip.info.js
 delete mode 100644 frontend/src/metabase/components/Tooltip.jsx
 create mode 100644 frontend/src/metabase/components/Tooltip/Tooltip.info.js
 create mode 100644 frontend/src/metabase/components/Tooltip/Tooltip.tsx
 create mode 100644 frontend/src/metabase/components/Tooltip/Tooltip.unit.spec.js
 create mode 100644 frontend/src/metabase/components/Tooltip/index.ts
 delete mode 100644 frontend/src/metabase/components/TooltipPopover.jsx
 delete mode 100644 frontend/src/metabase/hoc/Tooltipify.jsx
 create mode 100644 frontend/src/metabase/query_builder/components/view/NativeQueryButton.styled.jsx
 create mode 100644 frontend/src/metabase/styled-components/utils.tsx

diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.jsx b/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.jsx
index 1cfefdf6e49..acc914e17ca 100644
--- a/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.jsx
+++ b/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.jsx
@@ -22,7 +22,6 @@ import {
 import Tooltip from "metabase/components/Tooltip";
 
 const ICON_BUTTON_SIZE = 20;
-const TOOLTIP_X_OFFSET = ICON_BUTTON_SIZE / 4;
 
 const mapStateToProps = (state, props) => ({
   currentUser: getUser(state),
@@ -71,10 +70,7 @@ export function ModerationReviewBanner({
       onMouseEnter={() => setIsHovering(true)}
       onMouseLeave={() => setIsHovering(false)}
     >
-      <Tooltip
-        targetOffsetX={TOOLTIP_X_OFFSET}
-        tooltip={onRemove && tooltipText}
-      >
+      <Tooltip tooltip={onRemove && tooltipText}>
         {onRemove ? (
           <IconButton
             data-testid="moderation-remove-review-action"
diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.styled.jsx b/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.styled.jsx
index 60439334ad7..f15398c45e8 100644
--- a/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.styled.jsx
+++ b/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.styled.jsx
@@ -4,7 +4,7 @@ import Button from "metabase/components/Button";
 import Icon from "metabase/components/Icon";
 
 export const Container = styled.div`
-  padding: 1rem 1rem 1rem 0.5rem;
+  padding: 1rem;
   background-color: ${props => props.backgroundColor};
   display: flex;
   justify-content: space-between;
@@ -25,7 +25,7 @@ export const Time = styled.time`
 `;
 
 export const IconButton = styled(Button)`
-  padding: 0 0 0 0.5rem !important;
+  padding: 0 !important;
   border: none;
   background-color: transparent;
 
diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsSelect/PermissionsSelect.styled.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsSelect/PermissionsSelect.styled.jsx
index c196e6de434..dda8bbee252 100644
--- a/frontend/src/metabase/admin/permissions/components/PermissionsSelect/PermissionsSelect.styled.jsx
+++ b/frontend/src/metabase/admin/permissions/components/PermissionsSelect/PermissionsSelect.styled.jsx
@@ -1,7 +1,10 @@
 import styled from "styled-components";
+
+import { forwardRefToInnerRef } from "metabase/styled-components/utils";
 import Label from "metabase/components/type/Label";
 import { color, lighten } from "metabase/lib/colors";
 import Icon from "metabase/components/Icon";
+
 import { PermissionsSelectOption } from "./PermissionsSelectOption";
 
 export const PermissionsSelectRoot = styled.div`
@@ -47,13 +50,13 @@ export const ToggleLabel = styled.label`
   margin-right: 1rem;
 `;
 
-export const WarningIcon = styled(Icon).attrs({
+export const WarningIcon = forwardRefToInnerRef(styled(Icon).attrs({
   size: 18,
   name: "warning",
 })`
   margin-right: 0.25rem;
   color: ${color("text-light")};
-`;
+`);
 
 export const DisabledPermissionOption = styled(PermissionsSelectOption)`
   color: ${props =>
diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsTable/PermissionsTable.styled.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsTable/PermissionsTable.styled.jsx
index 739e25cd9ea..9578b3522f8 100644
--- a/frontend/src/metabase/admin/permissions/components/PermissionsTable/PermissionsTable.styled.jsx
+++ b/frontend/src/metabase/admin/permissions/components/PermissionsTable/PermissionsTable.styled.jsx
@@ -1,7 +1,9 @@
 import styled from "styled-components";
+
 import { color, alpha, lighten } from "metabase/lib/colors";
 import Link from "metabase/components/Link";
 import Icon from "metabase/components/Icon";
+import { forwardRefToInnerRef } from "metabase/styled-components/utils";
 
 const HORIZONTAL_PADDING_VARIANTS = {
   sm: "0.5rem",
@@ -47,8 +49,11 @@ export const EntityNameLink = styled(Link)`
 
 export const PermissionTableHeaderRow = styled.tr``;
 
-export const HintIcon = styled(Icon).attrs({ name: "info", size: 12 })`
+export const HintIcon = forwardRefToInnerRef(styled(Icon).attrs({
+  name: "info",
+  size: 12,
+})`
   color: ${lighten("text-dark", 0.3)};
   margin-left: 0.375rem;
   cursor: pointer;
-`;
+`);
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.styled.jsx b/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.styled.jsx
index 50ddbb4d665..f1968dc942b 100644
--- a/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.styled.jsx
+++ b/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.styled.jsx
@@ -40,6 +40,7 @@ export const DescriptionTooltipIcon = styled(Icon).attrs({
 })`
   color: ${color("bg-dark")};
   margin-left: ${space(1)};
+  margin-right: ${space(1)};
   margin-top: ${space(0)};
 
   &:hover {
diff --git a/frontend/src/metabase/components/Button.jsx b/frontend/src/metabase/components/Button.jsx
index d904874ddb1..d68f02e9d1c 100644
--- a/frontend/src/metabase/components/Button.jsx
+++ b/frontend/src/metabase/components/Button.jsx
@@ -1,13 +1,14 @@
 /* eslint-disable react/prop-types */
-import React from "react";
+import React, { forwardRef } from "react";
 import PropTypes from "prop-types";
-
-import Icon from "metabase/components/Icon";
 import cx from "classnames";
 import _ from "underscore";
 import styled from "styled-components";
 import { color, space } from "styled-system";
 
+import { forwardRefToInnerRef } from "metabase/styled-components/utils";
+import Icon from "metabase/components/Icon";
+
 const BUTTON_VARIANTS = [
   "small",
   "medium",
@@ -24,18 +25,21 @@ const BUTTON_VARIANTS = [
   "onlyIcon",
 ];
 
-const BaseButton = ({
-  className,
-  icon,
-  iconRight,
-  iconSize,
-  iconColor,
-  iconVertical,
-  labelBreakpoint,
-  color,
-  children,
-  ...props
-}) => {
+const BaseButton = forwardRef(function BaseButton(
+  {
+    className,
+    icon,
+    iconRight,
+    iconSize,
+    iconColor,
+    iconVertical,
+    labelBreakpoint,
+    color,
+    children,
+    ...props
+  },
+  ref,
+) {
   const variantClasses = BUTTON_VARIANTS.filter(variant => props[variant]).map(
     variant => "Button--" + variant,
   );
@@ -46,6 +50,7 @@ const BaseButton = ({
       className={cx("Button", className, "flex-no-shrink", variantClasses, {
         p1: !children,
       })}
+      ref={ref}
     >
       <div
         className={cx("flex layout-centered", { "flex-column": iconVertical })}
@@ -75,7 +80,7 @@ const BaseButton = ({
       </div>
     </button>
   );
-};
+});
 
 BaseButton.propTypes = {
   className: PropTypes.string,
@@ -95,10 +100,10 @@ BaseButton.propTypes = {
   borderless: PropTypes.bool,
 };
 
-const Button = styled(BaseButton)`
+const Button = forwardRefToInnerRef(styled(BaseButton)`
   ${color};
   ${space};
-`;
+`);
 
 Button.displayName = "Button";
 
diff --git a/frontend/src/metabase/components/CopyButton.jsx b/frontend/src/metabase/components/CopyButton.jsx
index dbb892bca13..ea9b2fedda2 100644
--- a/frontend/src/metabase/components/CopyButton.jsx
+++ b/frontend/src/metabase/components/CopyButton.jsx
@@ -31,13 +31,13 @@ export default class CopyWidget extends Component {
   render() {
     const { value, className, style, ...props } = this.props;
     return (
-      <Tooltip tooltip={t`Copied!`} isOpen={this.state.copied}>
-        <CopyToClipboard text={value} onCopy={this.onCopy}>
-          <div className={className} style={style} data-testid="copy-button">
+      <CopyToClipboard text={value} onCopy={this.onCopy}>
+        <div className={className} style={style} data-testid="copy-button">
+          <Tooltip tooltip={t`Copied!`} isOpen={this.state.copied}>
             <Icon name="copy" {...props} />
-          </div>
-        </CopyToClipboard>
-      </Tooltip>
+          </Tooltip>
+        </div>
+      </CopyToClipboard>
     );
   }
 }
diff --git a/frontend/src/metabase/components/ExternalLink.jsx b/frontend/src/metabase/components/ExternalLink.jsx
index 0c55d76cad6..2cadcb73c68 100644
--- a/frontend/src/metabase/components/ExternalLink.jsx
+++ b/frontend/src/metabase/components/ExternalLink.jsx
@@ -1,27 +1,27 @@
 /* eslint-disable react/prop-types */
-import React from "react";
+import React, { forwardRef } from "react";
 
 import { getUrlTarget } from "metabase/lib/dom";
 
-const ExternalLink = ({
-  href,
-  target = getUrlTarget(href),
-  className,
-  children,
-  ...props
-}) => (
-  <a
-    href={href}
-    className={className || "link"}
-    target={target}
-    // prevent malicious pages from navigating us away
-    rel="noopener noreferrer"
-    // disables quickfilter in tables
-    onClickCapture={e => e.stopPropagation()}
-    {...props}
-  >
-    {children}
-  </a>
-);
+const ExternalLink = forwardRef(function ExternalLink(
+  { href, target = getUrlTarget(href), className, children, ...props },
+  ref,
+) {
+  return (
+    <a
+      ref={ref}
+      href={href}
+      className={className || "link"}
+      target={target}
+      // prevent malicious pages from navigating us away
+      rel="noopener noreferrer"
+      // disables quickfilter in tables
+      onClickCapture={e => e.stopPropagation()}
+      {...props}
+    >
+      {children}
+    </a>
+  );
+});
 
 export default ExternalLink;
diff --git a/frontend/src/metabase/components/Icon.tsx b/frontend/src/metabase/components/Icon.tsx
index 93e0ac39da7..7595c1f3fe2 100644
--- a/frontend/src/metabase/components/Icon.tsx
+++ b/frontend/src/metabase/components/Icon.tsx
@@ -1,14 +1,14 @@
-import React, { Component } from "react";
 import PropTypes from "prop-types";
+import React, { Component, forwardRef } from "react";
 import styled from "styled-components";
 import { color, space, hover } from "styled-system";
 import cx from "classnames";
-import { color as c } from "metabase/lib/colors";
 
+import { color as c } from "metabase/lib/colors";
 import { loadIcon } from "metabase/icon_paths";
 import { stripLayoutProps } from "metabase/lib/utils";
-
-import Tooltipify from "metabase/hoc/Tooltipify";
+import Tooltip from "metabase/components/Tooltip";
+import { forwardRefToInnerRef } from "metabase/styled-components/utils";
 
 const MISSING_ICON_NAME = "unknown";
 
@@ -57,6 +57,7 @@ export const iconPropTypes = {
   scale: stringOrNumberPropType,
   tooltip: PropTypes.string,
   className: PropTypes.string,
+  innerRef: PropTypes.func.isRequired,
 };
 
 type IconProps = PropTypes.InferProps<typeof iconPropTypes>;
@@ -65,12 +66,12 @@ class BaseIcon extends Component<IconProps> {
   static propTypes = iconPropTypes;
 
   render() {
-    const { name, className, ...rest } = this.props;
+    const { name, className, innerRef, ...rest } = this.props;
 
     const icon = loadIcon(name) || loadIcon(MISSING_ICON_NAME);
     if (!icon) {
       console.warn(`Icon "${name}" does not exist.`);
-      return <span />;
+      return <span ref={innerRef} />;
     }
 
     const props = {
@@ -99,6 +100,7 @@ class BaseIcon extends Component<IconProps> {
       const { _role, ...rest } = props;
       return (
         <img
+          ref={innerRef}
           src={icon.img}
           srcSet={`
           ${icon.img}    1x,
@@ -108,25 +110,46 @@ class BaseIcon extends Component<IconProps> {
         />
       );
     } else if (icon.svg) {
-      return <svg {...props} dangerouslySetInnerHTML={{ __html: icon.svg }} />;
+      return (
+        <svg
+          {...props}
+          dangerouslySetInnerHTML={{ __html: icon.svg }}
+          ref={innerRef}
+        />
+      );
     } else if (icon.path) {
       return (
-        <svg {...props}>
+        <svg {...props} ref={innerRef}>
           <path d={icon.path} />
         </svg>
       );
     } else {
       console.warn(`Icon "${name}" must have an img, svg, or path`);
-      return <span />;
+      return <span ref={innerRef} />;
     }
   }
 }
 
-const Icon = styled(BaseIcon)`
+const BaseIconWithRef = forwardRefToInnerRef<IconProps>(BaseIcon);
+
+const StyledIcon = forwardRefToInnerRef<IconProps>(styled(BaseIconWithRef)`
   ${space}
   ${color}
   ${hover}
   flex-shrink: 0
-`;
-
-export default Tooltipify(Icon);
+`);
+
+const Icon = forwardRef(function Icon(
+  { tooltip, ...props }: IconProps,
+  ref?: React.Ref<any>,
+) {
+  return tooltip ? (
+    <Tooltip tooltip={tooltip}>
+      <StyledIcon {...props} />
+    </Tooltip>
+  ) : (
+    <StyledIcon ref={ref} {...props} />
+  );
+});
+
+export default Icon;
diff --git a/frontend/src/metabase/components/Link.jsx b/frontend/src/metabase/components/Link.jsx
index 919b5097af4..689e2b6e81c 100644
--- a/frontend/src/metabase/components/Link.jsx
+++ b/frontend/src/metabase/components/Link.jsx
@@ -4,17 +4,20 @@ import PropTypes from "prop-types";
 import { Link as ReactRouterLink } from "react-router";
 import styled from "styled-components";
 import { display, color, hover, space } from "styled-system";
+
 import { stripLayoutProps } from "metabase/lib/utils";
+import Tooltip from "metabase/components/Tooltip";
 
 BaseLink.propTypes = {
   to: PropTypes.string.isRequired,
   disabled: PropTypes.bool,
   className: PropTypes.string,
   children: PropTypes.node,
+  tooltip: PropTypes.string,
 };
 
-function BaseLink({ to, className, children, disabled, ...props }) {
-  return (
+function BaseLink({ to, className, children, disabled, tooltip, ...props }) {
+  const link = (
     <ReactRouterLink
       to={to}
       className={cx(className || "link", {
@@ -26,6 +29,8 @@ function BaseLink({ to, className, children, disabled, ...props }) {
       {children}
     </ReactRouterLink>
   );
+
+  return tooltip ? <Tooltip tooltip={tooltip}>{link}</Tooltip> : link;
 }
 
 const Link = styled(BaseLink)`
diff --git a/frontend/src/metabase/components/MetabotLogo.jsx b/frontend/src/metabase/components/MetabotLogo.jsx
index c6ec484b864..d8079b2006d 100644
--- a/frontend/src/metabase/components/MetabotLogo.jsx
+++ b/frontend/src/metabase/components/MetabotLogo.jsx
@@ -1,7 +1,9 @@
-import React from "react";
+import React, { forwardRef } from "react";
 
-const MetabotLogo = () => (
-  <img className="brand-hue" src="app/assets/img/metabot.svg" />
-);
+const MetabotLogo = forwardRef(function MetabotLogo(props, ref) {
+  return (
+    <img ref={ref} className="brand-hue" src="app/assets/img/metabot.svg" />
+  );
+});
 
 export default MetabotLogo;
diff --git a/frontend/src/metabase/components/Popover.css b/frontend/src/metabase/components/Popover.css
index 23f7ad7d388..92c4b23a794 100644
--- a/frontend/src/metabase/components/Popover.css
+++ b/frontend/src/metabase/components/Popover.css
@@ -22,6 +22,22 @@
   overflow: auto;
 }
 
+.tippy-box[data-theme~="tooltip"] {
+  color: white;
+  font-weight: bold;
+  background-color: var(--color-bg-black);
+  border: none;
+  pointer-events: none;
+  line-height: 1.26;
+  font-size: 12px;
+  border-radius: 6px;
+  box-shadow: 0 4px 10px var(--color-shadow);
+}
+
+.tippy-box[data-theme~="tooltip"] .tippy-content {
+  padding: 10px 12px;
+}
+
 /* remove the max-width in cases where the popover content needs to expand
  * initially added  for date pickers so the dual date picker can fully
  * expand as necessary - metabase#5971
diff --git a/frontend/src/metabase/components/Tooltip.info.js b/frontend/src/metabase/components/Tooltip.info.js
deleted file mode 100644
index d9c5a13055a..00000000000
--- a/frontend/src/metabase/components/Tooltip.info.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from "react";
-import Tooltip from "./Tooltip";
-
-export const component = Tooltip;
-
-export const description = `
-Add context to a target element.
-`;
-
-export const examples = {
-  default: (
-    <Tooltip tooltip="Action">
-      <a className="link">Hover on me</a>
-    </Tooltip>
-  ),
-  longerString: (
-    <Tooltip tooltip="This does an action that needs some explaining">
-      <a className="link">Hover on me</a>
-    </Tooltip>
-  ),
-};
diff --git a/frontend/src/metabase/components/Tooltip.jsx b/frontend/src/metabase/components/Tooltip.jsx
deleted file mode 100644
index c30a3c65d1e..00000000000
--- a/frontend/src/metabase/components/Tooltip.jsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import React, { Component } from "react";
-import PropTypes from "prop-types";
-import ReactDOM from "react-dom";
-
-import TooltipPopover from "./TooltipPopover";
-
-// TOOLTIP_STACK and related functions are to ensure only the most recent tooltip is visible
-
-let TOOLTIP_STACK = [];
-
-function pushTooltip(component) {
-  // if for some reason the tooltip is already in the stack (it shouldn't be) remove it and we'll add it again as if it wasn't
-  TOOLTIP_STACK = TOOLTIP_STACK.filter(t => t !== component);
-  // close all other tooltips
-  TOOLTIP_STACK.filter(t => t.state.isOpen).forEach(t =>
-    t.setState({ isOpen: false }),
-  );
-  // add this tooltip
-  TOOLTIP_STACK.push(component);
-}
-
-function popTooltip(component) {
-  // remove the tooltip from the stack
-  TOOLTIP_STACK = TOOLTIP_STACK.filter(t => t !== component);
-  // reopen the top tooltip, if any
-  const top = TOOLTIP_STACK[TOOLTIP_STACK.length - 1];
-  if (top && !top.state.isOpen) {
-    top.setState({ isOpen: true });
-  }
-}
-
-export default class Tooltip extends Component {
-  constructor(props, context) {
-    super(props, context);
-
-    this.state = {
-      isOpen: false,
-      isHovered: false,
-    };
-  }
-
-  static propTypes = {
-    // the tooltip to show
-    tooltip: PropTypes.node,
-    // the element to be tooltipped
-    children: PropTypes.element.isRequired,
-    // Can be used to show / hide the tooltip based on outside conditions
-    // like a menu being open
-    isEnabled: PropTypes.bool,
-    verticalAttachments: PropTypes.array,
-    // Whether the tooltip should be shown
-    isOpen: PropTypes.bool,
-  };
-
-  static defaultProps = {
-    isEnabled: true,
-    verticalAttachments: ["top", "bottom"],
-    horizontalAttachments: ["center", "left", "right"],
-  };
-
-  componentDidMount() {
-    const elem = ReactDOM.findDOMNode(this);
-
-    if (elem) {
-      elem.addEventListener("mouseenter", this._onMouseEnter, false);
-      elem.addEventListener("mouseleave", this._onMouseLeave, false);
-
-      // HACK: These two event listeners ensure that if a click on the child causes the tooltip to
-      // unmount (e.x. navigating away) then the popover is removed by the time this component
-      // unmounts. Previously we were seeing difficult to debug error messages like
-      // "Cannot read property 'componentDidUpdate' of null"
-      elem.addEventListener("mousedown", this._onMouseDown, true);
-      elem.addEventListener("mouseup", this._onMouseUp, true);
-    } else {
-      console.warn(
-        `Tooltip::componentDidMount: no DOM node for tooltip ${this.props.tooltip}`,
-      );
-    }
-  }
-
-  componentWillUnmount() {
-    popTooltip(this);
-    const elem = ReactDOM.findDOMNode(this);
-    if (elem) {
-      elem.removeEventListener("mouseenter", this._onMouseEnter, false);
-      elem.removeEventListener("mouseleave", this._onMouseLeave, false);
-      elem.removeEventListener("mousedown", this._onMouseDown, true);
-      elem.removeEventListener("mouseup", this._onMouseUp, true);
-    } else {
-      console.warn(
-        `Tooltip::componentWillUnmount: no DOM node for tooltip ${this.props.tooltip}`,
-      );
-    }
-    clearTimeout(this.timer);
-  }
-
-  _onMouseEnter = e => {
-    pushTooltip(this);
-    this.setState({ isOpen: true, isHovered: true });
-  };
-
-  _onMouseLeave = e => {
-    popTooltip(this);
-    this.setState({ isOpen: false, isHovered: false });
-  };
-
-  _onMouseDown = e => {
-    this.setState({ isOpen: false });
-  };
-
-  _onMouseUp = e => {
-    // This is in a timeout to ensure the component has a chance to fully unmount
-    this.timer = setTimeout(
-      () => this.setState({ isOpen: this.state.isHovered }),
-      0,
-    );
-  };
-
-  render() {
-    const { isEnabled, tooltip } = this.props;
-    const isOpen =
-      this.props.isOpen != null ? this.props.isOpen : this.state.isOpen;
-    return (
-      <React.Fragment>
-        {React.Children.only(this.props.children)}
-        {tooltip && isEnabled && isOpen && (
-          <TooltipPopover isOpen={true} target={this} hasArrow {...this.props}>
-            {this.props.tooltip}
-          </TooltipPopover>
-        )}
-      </React.Fragment>
-    );
-  }
-}
diff --git a/frontend/src/metabase/components/Tooltip/Tooltip.info.js b/frontend/src/metabase/components/Tooltip/Tooltip.info.js
new file mode 100644
index 00000000000..402298e4c9c
--- /dev/null
+++ b/frontend/src/metabase/components/Tooltip/Tooltip.info.js
@@ -0,0 +1,81 @@
+import React from "react";
+import Tooltip from "./Tooltip";
+
+export const component = Tooltip;
+
+export const description = `
+Add context to a target element.
+`;
+
+const jsxContent = (
+  <React.Fragment>
+    <div style={{ backgroundColor: "blue", opacity: "50%" }}>
+      blah blah blah
+    </div>
+    <div style={{ backgroundColor: "red", opacity: "50%" }}>blah blah blah</div>
+  </React.Fragment>
+);
+
+function ReferenceTargetDemo() {
+  const [target, setTarget] = React.useState();
+
+  const onMouseEnter = () => {
+    setTarget(document.getElementById("reference-target"));
+  };
+
+  const onMouseLeave = () => {
+    setTarget(null);
+  };
+
+  return (
+    <span>
+      <a
+        onMouseEnter={onMouseEnter}
+        onMouseLeave={onMouseLeave}
+        className="link"
+      >
+        Hover on me
+      </a>{" "}
+      <span id="reference-target" style={{ backgroundColor: "yellow" }}>
+        target
+      </span>
+      <Tooltip
+        isOpen={!!target}
+        reference={target}
+        tooltip="reference tooltip"
+      />
+    </span>
+  );
+}
+
+export const examples = {
+  default: (
+    <Tooltip tooltip="Action">
+      <a className="link">Hover on me</a>
+    </Tooltip>
+  ),
+  longerString: (
+    <Tooltip tooltip="This does an action that needs some explaining">
+      <a className="link">Hover on me</a>
+    </Tooltip>
+  ),
+  "changeable maxWidth": (
+    <Tooltip
+      maxWidth="unset"
+      tooltip="This does an action that needs some explaining and you will see that it does not wrap"
+    >
+      <a className="link">Hover on me</a>
+    </Tooltip>
+  ),
+  controllable: (
+    <Tooltip isOpen tooltip="this tooltip is always open">
+      <a className="link">Hover on me</a>
+    </Tooltip>
+  ),
+  "jsx content": (
+    <Tooltip tooltip={jsxContent}>
+      <a className="link">Hover on me</a>
+    </Tooltip>
+  ),
+  "reference element": <ReferenceTargetDemo />,
+};
diff --git a/frontend/src/metabase/components/Tooltip/Tooltip.tsx b/frontend/src/metabase/components/Tooltip/Tooltip.tsx
new file mode 100644
index 00000000000..d08d6250c26
--- /dev/null
+++ b/frontend/src/metabase/components/Tooltip/Tooltip.tsx
@@ -0,0 +1,92 @@
+import React from "react";
+import PropTypes from "prop-types";
+import * as Tippy from "@tippyjs/react";
+import * as ReactIs from "react-is";
+
+import { isReducedMotionPreferred } from "metabase/lib/dom";
+
+const TippyComponent = Tippy.default;
+
+Tooltip.propTypes = {
+  tooltip: PropTypes.node,
+  children: PropTypes.node,
+  reference: PropTypes.instanceOf(Element),
+  placement: PropTypes.string,
+  isEnabled: PropTypes.bool,
+  isOpen: PropTypes.bool,
+  maxWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+};
+
+interface TooltipProps
+  extends Partial<
+    Pick<Tippy.TippyProps, "reference" | "placement" | "maxWidth">
+  > {
+  tooltip?: React.ReactNode;
+  children?: React.ReactNode;
+  isEnabled?: boolean;
+  isOpen?: boolean;
+  maxWidth?: string | number | undefined;
+}
+
+// checking to see if the `element` is in JSX.IntrinisicElements since they support refs
+// tippy's `children` prop seems to complain about anything more specific that React.ReactElement, unfortunately
+function isReactDOMTypeElement(element: any): element is React.ReactElement {
+  return ReactIs.isElement(element) && typeof element.type === "string";
+}
+
+// Tippy relies on child nodes forwarding refs, so when `children` is neither
+// a DOM element or a forwardRef, we need to wrap it in a span.
+function getSafeChildren(children: React.ReactNode) {
+  if (isReactDOMTypeElement(children) || ReactIs.isForwardRef(children)) {
+    return children;
+  } else {
+    return <span data-testid="tooltip-component-wrapper">{children}</span>;
+  }
+}
+
+function getTargetProps(
+  reference?: Element | React.RefObject<Element> | null,
+  children?: React.ReactNode,
+) {
+  if (reference) {
+    return { reference };
+  } else if (children != null) {
+    return { children: getSafeChildren(children) };
+  }
+}
+
+function Tooltip({
+  tooltip,
+  children,
+  reference,
+  placement,
+  isEnabled,
+  isOpen,
+  maxWidth = 200,
+}: TooltipProps) {
+  const visible = isOpen != null ? isOpen : undefined;
+  const animationDuration = isReducedMotionPreferred() ? 0 : undefined;
+  const disabled = isEnabled === false;
+  const targetProps = getTargetProps(reference, children);
+
+  if (tooltip && targetProps) {
+    return (
+      <TippyComponent
+        theme="tooltip"
+        appendTo={() => document.body}
+        content={tooltip}
+        visible={visible}
+        disabled={disabled}
+        maxWidth={maxWidth}
+        reference={reference}
+        duration={animationDuration}
+        placement={placement}
+        {...targetProps}
+      />
+    );
+  } else {
+    return <React.Fragment>{children}</React.Fragment>;
+  }
+}
+
+export default Tooltip;
diff --git a/frontend/src/metabase/components/Tooltip/Tooltip.unit.spec.js b/frontend/src/metabase/components/Tooltip/Tooltip.unit.spec.js
new file mode 100644
index 00000000000..c62be7b2b52
--- /dev/null
+++ b/frontend/src/metabase/components/Tooltip/Tooltip.unit.spec.js
@@ -0,0 +1,154 @@
+import React, { forwardRef } from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+import Tooltip from "./Tooltip";
+
+const defaultTooltip = "tooltip content";
+
+const defaultTarget = (
+  <div id="child-target" style={{ width: 100, height: 100 }}>
+    child target element
+  </div>
+);
+
+function setup({
+  target = defaultTarget,
+  tooltip = defaultTooltip,
+  ...otherProps
+} = {}) {
+  return render(
+    <div>
+      <Tooltip tooltip={tooltip} {...otherProps}>
+        {target}
+      </Tooltip>
+    </div>,
+  );
+}
+
+describe("Tooltip", () => {
+  it("should be visible on hover of child target element", () => {
+    setup();
+    userEvent.hover(screen.getByText("child target element"));
+    expect(screen.getByText("tooltip content")).toBeInTheDocument();
+  });
+
+  describe("isOpen", () => {
+    it("should override hover behavior", () => {
+      setup({ isOpen: false });
+
+      userEvent.hover(screen.getByText("child target element"));
+      expect(screen.queryByText("tooltip content")).not.toBeInTheDocument();
+    });
+
+    it("should be visible when isOpen is set to true", () => {
+      setup({ isOpen: true });
+      expect(screen.getByText("tooltip content")).toBeInTheDocument();
+    });
+  });
+
+  describe("isEnabled", () => {
+    it("should override hover behavior when false", () => {
+      setup({ isEnabled: false });
+
+      userEvent.hover(screen.getByText("child target element"));
+      expect(screen.queryByText("tooltip content")).not.toBeInTheDocument();
+    });
+
+    it("should not override hover behavior when true", () => {
+      setup({ isEnabled: true });
+
+      userEvent.hover(screen.getByText("child target element"));
+      expect(screen.getByText("tooltip content")).toBeInTheDocument();
+    });
+
+    it("should override isOpen when false", () => {
+      setup({ isEnabled: false, isOpen: true });
+
+      userEvent.hover(screen.getByText("child target element"));
+      expect(screen.queryByText("tooltip content")).not.toBeInTheDocument();
+    });
+
+    it("should not override isOpen when true", () => {
+      setup({ isEnabled: true, isOpen: true });
+
+      userEvent.hover(screen.getByText("child target element"));
+      expect(screen.getByText("tooltip content")).toBeInTheDocument();
+    });
+  });
+
+  it("should wrap target Components without forwarded refs in a <span>", () => {
+    function BadTarget() {
+      return <div>bad target</div>;
+    }
+
+    setup({ isOpen: true, target: <BadTarget /> });
+
+    const tooltip = screen.getByTestId("tooltip-component-wrapper");
+
+    expect(tooltip).toBeInTheDocument();
+    expect(tooltip.textContent).toEqual("bad target");
+  });
+
+  it("should not wrap target Components with forwarded refs", () => {
+    const GoodTarget = forwardRef(function GoodTarget(props, ref) {
+      return <div ref={ref}>good target</div>;
+    });
+
+    setup({ isOpen: true, target: <GoodTarget /> });
+
+    expect(
+      screen.queryByTestId("tooltip-component-wrapper"),
+    ).not.toBeInTheDocument();
+    expect(screen.getByText("good target")).toBeInTheDocument();
+  });
+
+  it("should not wrap targets that are JSX dom elements", () => {
+    setup({ isOpen: true, target: <div>good target</div> });
+
+    expect(
+      screen.queryByTestId("tooltip-component-wrapper"),
+    ).not.toBeInTheDocument();
+    expect(screen.getByText("good target")).toBeInTheDocument();
+  });
+
+  it("should still render children when not given tooltip content", () => {
+    setup({ isOpen: true, tooltip: null });
+
+    expect(screen.getByText("child target element")).toBeInTheDocument();
+    expect(document.querySelector(".tippy-box")).toBeNull();
+  });
+
+  it("should be themed as a tooltip", () => {
+    setup({ isOpen: true });
+    expect(
+      document.querySelector('[data-theme~="tooltip"'),
+    ).toBeInTheDocument();
+  });
+
+  it("should support using a reference element instead of a child target element", () => {
+    function ReferenceTooltipTest() {
+      const [eventTarget, setEventTarget] = React.useState();
+      return (
+        <div>
+          <Tooltip reference={eventTarget} tooltip="reference tooltip" isOpen />
+          <div
+            onClick={event => {
+              setEventTarget(event.target);
+            }}
+            style={{ width: 100, height: 100 }}
+          >
+            sibling element
+          </div>
+        </div>
+      );
+    }
+
+    render(<ReferenceTooltipTest />);
+    expect(screen.queryByText("reference tooltip")).not.toBeInTheDocument();
+
+    screen.getByText("sibling element").click();
+
+    expect(screen.getByText("reference tooltip")).toBeInTheDocument();
+  });
+});
diff --git a/frontend/src/metabase/components/Tooltip/index.ts b/frontend/src/metabase/components/Tooltip/index.ts
new file mode 100644
index 00000000000..d6006235128
--- /dev/null
+++ b/frontend/src/metabase/components/Tooltip/index.ts
@@ -0,0 +1 @@
+export { default } from "./Tooltip";
diff --git a/frontend/src/metabase/components/TooltipPopover.jsx b/frontend/src/metabase/components/TooltipPopover.jsx
deleted file mode 100644
index d706955c17f..00000000000
--- a/frontend/src/metabase/components/TooltipPopover.jsx
+++ /dev/null
@@ -1,40 +0,0 @@
-/* eslint-disable react/prop-types */
-import React from "react";
-import cx from "classnames";
-
-import Popover from "./Popover";
-
-const TooltipPopover = ({ children, constrainedWidth, ...props }) => {
-  let popoverContent;
-
-  if (typeof children === "string") {
-    popoverContent = <span>{children}</span>;
-  } else {
-    popoverContent = children;
-  }
-
-  return (
-    <Popover
-      className={cx("PopoverBody--tooltip", {
-        "PopoverBody--tooltipConstrainedWidth": constrainedWidth,
-      })}
-      targetOffsetY={10}
-      hasArrow
-      horizontalAttachments={["center", "left", "right"]}
-      // OnClickOutsideWrapper is unecessary and causes existing popovers not to
-      // be dismissed if a tooltip is visisble, so pass noOnClickOutsideWrapper
-      noOnClickOutsideWrapper
-      role="tooltip"
-      {...props}
-    >
-      {popoverContent}
-    </Popover>
-  );
-};
-
-TooltipPopover.defaultProps = {
-  // default to having a constrained toolip, which limits the width so longer strings wrap.
-  constrainedWidth: true,
-};
-
-export default React.memo(TooltipPopover);
diff --git a/frontend/src/metabase/containers/Overworld.jsx b/frontend/src/metabase/containers/Overworld.jsx
index 424becc6b86..f387b3858e8 100644
--- a/frontend/src/metabase/containers/Overworld.jsx
+++ b/frontend/src/metabase/containers/Overworld.jsx
@@ -74,7 +74,10 @@ class Overworld extends React.Component {
     return (
       <Box>
         <Flex px={PAGE_PADDING} pt={3} pb={1} align="center">
-          <Tooltip tooltip={t`Don't tell anyone, but you're my favorite.`}>
+          <Tooltip
+            tooltip={t`Don't tell anyone, but you're my favorite.`}
+            placement="bottom"
+          >
             <MetabotLogo />
           </Tooltip>
           <Box ml={2}>
diff --git a/frontend/src/metabase/css/vendor.css b/frontend/src/metabase/css/vendor.css
index 927e26c2b47..a3e93c2ceef 100644
--- a/frontend/src/metabase/css/vendor.css
+++ b/frontend/src/metabase/css/vendor.css
@@ -3,3 +3,5 @@
 
 /* z-index utils */
 @import "z-index/z-index.css";
+
+@import "tippy.js/dist/tippy.css";
diff --git a/frontend/src/metabase/hoc/Tooltipify.jsx b/frontend/src/metabase/hoc/Tooltipify.jsx
deleted file mode 100644
index d93a0a334ff..00000000000
--- a/frontend/src/metabase/hoc/Tooltipify.jsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/* eslint-disable react/prop-types */
-import React, { Component } from "react";
-
-import Tooltip from "metabase/components/Tooltip";
-
-const Tooltipify = ComposedComponent =>
-  class extends Component {
-    static displayName =
-      "Tooltipify[" +
-      (ComposedComponent.displayName || ComposedComponent.name) +
-      "]";
-    render() {
-      const { tooltip, targetOffsetX, ...props } = this.props;
-      if (tooltip) {
-        return (
-          <Tooltip tooltip={tooltip} targetOffsetX={targetOffsetX}>
-            <ComposedComponent {...props} />
-          </Tooltip>
-        );
-      } else {
-        return <ComposedComponent {...props} />;
-      }
-    }
-  };
-
-export default Tooltipify;
diff --git a/frontend/src/metabase/lib/dom.js b/frontend/src/metabase/lib/dom.js
index 02c8503ca1a..4052b3d20d6 100644
--- a/frontend/src/metabase/lib/dom.js
+++ b/frontend/src/metabase/lib/dom.js
@@ -378,7 +378,7 @@ export function parseDataUri(url) {
 /**
  * @returns the clip-path CSS property referencing the clip path in the current document, taking into account the <base> tag.
  */
-export function clipPathReference(id: string): string {
+export function clipPathReference(id) {
   // add the current page URL (with fragment removed) to support pages with <base> tag.
   // https://stackoverflow.com/questions/18259032/using-base-tag-on-a-page-that-contains-svg-marker-elements-fails-to-render-marke
   const url = window.location.href.replace(/#.*$/, "") + "#" + id;
diff --git a/frontend/src/metabase/nav/components/StoreLink/StoreLink.styled.jsx b/frontend/src/metabase/nav/components/StoreLink/StoreLink.styled.jsx
index 8df56a95afc..01c95032387 100644
--- a/frontend/src/metabase/nav/components/StoreLink/StoreLink.styled.jsx
+++ b/frontend/src/metabase/nav/components/StoreLink/StoreLink.styled.jsx
@@ -1,12 +1,16 @@
 import styled from "styled-components";
+
 import { color, darken } from "metabase/lib/colors";
 import { space } from "metabase/styled-components/theme";
 import Icon, { IconWrapper } from "metabase/components/Icon";
 import ExternalLink from "metabase/components/ExternalLink";
+import { forwardRefToInnerRef } from "metabase/styled-components/utils";
 
-export const StoreIconRoot = styled(ExternalLink)`
-  margin-right: ${space(1)};
-`;
+export const StoreIconRoot = forwardRefToInnerRef(
+  styled(ExternalLink)`
+    margin-right: ${space(1)};
+  `,
+);
 
 export const StoreIconWrapper = styled(IconWrapper)`
   color: ${color("white")};
diff --git a/frontend/src/metabase/nav/containers/Navbar.jsx b/frontend/src/metabase/nav/containers/Navbar.jsx
index b0ddd972ece..75414ea8f95 100644
--- a/frontend/src/metabase/nav/containers/Navbar.jsx
+++ b/frontend/src/metabase/nav/containers/Navbar.jsx
@@ -252,13 +252,9 @@ export default class Navbar extends Component {
                 to="browse"
                 className="flex align-center rounded transition-background"
                 data-metabase-event={`NavBar;Data Browse`}
+                tooltip={t`Browse data`}
               >
-                <Icon
-                  name="table_spaced"
-                  size={14}
-                  p={"11px"}
-                  tooltip={t`Browse data`}
-                />
+                <Icon name="table_spaced" size={14} p={"11px"} />
               </Link>
             </IconWrapper>
           )}
@@ -291,8 +287,9 @@ export default class Navbar extends Component {
                 to={this.props.plainNativeQuery.question().getUrl()}
                 className="flex align-center"
                 data-metabase-event={`NavBar;SQL`}
+                tooltip={t`Write SQL`}
               >
-                <Icon size={18} p={"11px"} name="sql" tooltip={t`Write SQL`} />
+                <Icon size={18} p={"11px"} name="sql" />
               </Link>
             </IconWrapper>
           )}
diff --git a/frontend/src/metabase/query_builder/components/RunButton.jsx b/frontend/src/metabase/query_builder/components/RunButton.jsx
index 71d4c5d31e7..d2b74ba7eaf 100644
--- a/frontend/src/metabase/query_builder/components/RunButton.jsx
+++ b/frontend/src/metabase/query_builder/components/RunButton.jsx
@@ -1,5 +1,5 @@
 /* eslint-disable react/prop-types */
-import React, { Component } from "react";
+import React, { forwardRef } from "react";
 import PropTypes from "prop-types";
 import { t } from "ttag";
 
@@ -7,62 +7,65 @@ import Button from "metabase/components/Button";
 
 import cx from "classnames";
 
-export default class RunButton extends Component {
-  static propTypes = {
-    className: PropTypes.string,
-    isRunning: PropTypes.bool.isRequired,
-    isDirty: PropTypes.bool.isRequired,
-    isPreviewing: PropTypes.bool,
-    onRun: PropTypes.func.isRequired,
-    onCancel: PropTypes.func,
-  };
+const propTypes = {
+  className: PropTypes.string,
+  isRunning: PropTypes.bool.isRequired,
+  isDirty: PropTypes.bool.isRequired,
+  isPreviewing: PropTypes.bool,
+  onRun: PropTypes.func.isRequired,
+  onCancel: PropTypes.func,
+};
 
-  static defaultProps = {};
-
-  render() {
-    const {
-      isRunning,
-      isDirty,
-      isPreviewing,
-      onRun,
-      onCancel,
-      className,
-      compact,
-      circular,
-      hidden,
-      ...props
-    } = this.props;
-    let buttonText = null;
-    let buttonIcon = null;
-    if (isRunning) {
-      buttonIcon = "close";
-      if (!compact) {
-        buttonText = t`Cancel`;
-      }
-    } else if (isDirty) {
-      if (compact) {
-        buttonIcon = "play";
-      } else {
-        buttonText = isPreviewing ? t`Get Preview` : t`Get Answer`;
-      }
+const RunButton = forwardRef(function RunButton(
+  {
+    isRunning,
+    isDirty,
+    isPreviewing,
+    onRun,
+    onCancel,
+    className,
+    compact,
+    circular,
+    hidden,
+    ...props
+  },
+  ref,
+) {
+  let buttonText = null;
+  let buttonIcon = null;
+  if (isRunning) {
+    buttonIcon = "close";
+    if (!compact) {
+      buttonText = t`Cancel`;
+    }
+  } else if (isDirty) {
+    if (compact) {
+      buttonIcon = "play";
     } else {
-      buttonIcon = "refresh";
+      buttonText = isPreviewing ? t`Get Preview` : t`Get Answer`;
     }
-    return (
-      <Button
-        {...props}
-        icon={buttonIcon}
-        primary={isDirty}
-        iconSize={16}
-        className={cx(className, "RunButton", {
-          "RunButton--hidden": hidden,
-          "RunButton--compact": circular && !props.borderless && compact,
-          circular: circular,
-        })}
-        onClick={isRunning ? onCancel : onRun}
-      >
-        {buttonText}
-      </Button>
-    );
+  } else {
+    buttonIcon = "refresh";
   }
-}
+  return (
+    <Button
+      {...props}
+      icon={buttonIcon}
+      primary={isDirty}
+      iconSize={16}
+      className={cx(className, "RunButton", {
+        "RunButton--hidden": hidden,
+        "RunButton--compact": circular && !props.borderless && compact,
+        circular: circular,
+      })}
+      onClick={isRunning ? onCancel : onRun}
+      ref={ref}
+    >
+      {buttonText}
+    </Button>
+  );
+});
+
+RunButton.propTypes = propTypes;
+
+export default RunButton;
diff --git a/frontend/src/metabase/query_builder/components/notebook/NotebookStep.jsx b/frontend/src/metabase/query_builder/components/notebook/NotebookStep.jsx
index 65813fb911d..9fc472c5947 100644
--- a/frontend/src/metabase/query_builder/components/notebook/NotebookStep.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/NotebookStep.jsx
@@ -12,6 +12,7 @@ import Tooltip from "metabase/components/Tooltip";
 import Icon from "metabase/components/Icon";
 import Button from "metabase/components/Button";
 import ExpandingContent from "metabase/components/ExpandingContent";
+import { forwardRefToInnerRef } from "metabase/styled-components/utils";
 
 import { Box, Flex } from "grid-styled";
 
@@ -217,7 +218,7 @@ export default class NotebookStep extends React.Component {
   }
 }
 
-const ColorButton = styled(Button)`
+const ColorButton = forwardRefToInnerRef(styled(Button)`
   border: none;
   color: ${({ color }) => (color ? color : c("text-medium"))};
   background-color: ${({ color }) => (color ? lighten(color, 0.61) : null)};
@@ -227,7 +228,7 @@ const ColorButton = styled(Button)`
       color ? lighten(color, 0.5) : lighten(color("brand"), 0.61)};
   }
   transition: background 300ms;
-`;
+`);
 
 const ActionButton = ({ icon, title, color, large, onClick, ...props }) => {
   const button = (
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.styled.jsx b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.styled.jsx
index c1875a9ae63..315d73a82c9 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.styled.jsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep.styled.jsx
@@ -1,17 +1,19 @@
 import styled from "styled-components";
+
 import { color } from "metabase/lib/colors";
 import { space, breakpointMaxMedium } from "metabase/styled-components/theme";
 import Icon from "metabase/components/Icon";
+import { forwardRefToInnerRef } from "metabase/styled-components/utils";
 
 export const Row = styled.div`
   display: flex;
   align-items: center;
 `;
 
-export const JoinStepRoot = styled.div`
+export const JoinStepRoot = forwardRefToInnerRef(styled.div`
   display: flex;
   align-items: center;
-`;
+`);
 
 export const JoinClausesContainer = styled.div`
   display: flex;
diff --git a/frontend/src/metabase/query_builder/components/view/NativeQueryButton.jsx b/frontend/src/metabase/query_builder/components/view/NativeQueryButton.jsx
index bb5a7323cb7..d6a48214325 100644
--- a/frontend/src/metabase/query_builder/components/view/NativeQueryButton.jsx
+++ b/frontend/src/metabase/query_builder/components/view/NativeQueryButton.jsx
@@ -4,14 +4,14 @@ import { t } from "ttag";
 import _ from "underscore";
 
 import Modal from "metabase/components/Modal";
-import Icon from "metabase/components/Icon";
 import Button from "metabase/components/Button";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
-
+import Tooltip from "metabase/components/Tooltip";
 import { formatNativeQuery, getEngineNativeType } from "metabase/lib/engine";
-
 import { MetabaseApi } from "metabase/services";
 
+import { SqlIconButton } from "./NativeQueryButton.styled";
+
 const STRINGS = {
   "": {
     tooltip: t`View the native query`,
@@ -80,12 +80,9 @@ export default class NativeQueryButton extends React.Component {
 
     return (
       <span {...props}>
-        <Icon
-          name="sql"
-          size={size}
-          tooltip={tooltip}
-          onClick={this.handleOpen}
-        />
+        <Tooltip tooltip={tooltip} placement="bottom">
+          <SqlIconButton iconSize={size} onClick={this.handleOpen} />
+        </Tooltip>
         <Modal
           isOpen={this.state.open}
           title={title}
diff --git a/frontend/src/metabase/query_builder/components/view/NativeQueryButton.styled.jsx b/frontend/src/metabase/query_builder/components/view/NativeQueryButton.styled.jsx
new file mode 100644
index 00000000000..38b7862fd49
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/NativeQueryButton.styled.jsx
@@ -0,0 +1,22 @@
+import styled from "styled-components";
+
+import { color } from "metabase/lib/colors";
+import { space } from "metabase/styled-components/theme";
+import Button from "metabase/components/Button";
+import { forwardRefToInnerRef } from "metabase/styled-components/utils";
+
+export const SqlIconButton = forwardRefToInnerRef(styled(Button).attrs({
+  icon: "sql",
+})`
+  margin-left: ${space(2)};
+  padding: ${space(1)};
+  border: none;
+  background-color: transparent;
+  color: ${color("text-medium")};
+  cursor: pointer;
+
+  :hover {
+    background-color: transparent;
+    color: ${color("brand")};
+  }
+`);
diff --git a/frontend/src/metabase/query_builder/components/view/QuestionNotebookButton.jsx b/frontend/src/metabase/query_builder/components/view/QuestionNotebookButton.jsx
index 81c420c6779..184b452a505 100644
--- a/frontend/src/metabase/query_builder/components/view/QuestionNotebookButton.jsx
+++ b/frontend/src/metabase/query_builder/components/view/QuestionNotebookButton.jsx
@@ -15,7 +15,10 @@ export default function QuestionNotebookButton({
   ...props
 }) {
   return QuestionNotebookButton.shouldRender({ question }) ? (
-    <Tooltip tooltip={isShowingNotebook ? t`Hide editor` : t`Show editor`}>
+    <Tooltip
+      tooltip={isShowingNotebook ? t`Hide editor` : t`Show editor`}
+      placement="bottom"
+    >
       <Button
         borderless={!isShowingNotebook}
         primary={isShowingNotebook}
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx b/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx
index cfea5106ed8..ca3cb076f9b 100644
--- a/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader.jsx
@@ -32,7 +32,6 @@ import {
   ViewHeaderMainLeftContentContainer,
   ViewHeaderLeftSubHeading,
   ViewHeaderContainer,
-  ViewSQLButtonContainer,
 } from "./ViewHeader.styled";
 
 const viewTitleHeaderPropTypes = {
@@ -395,13 +394,11 @@ function ViewTitleHeaderRightSide(props) {
         />
       )}
       {NativeQueryButton.shouldRender(props) && (
-        <ViewSQLButtonContainer>
-          <NativeQueryButton
-            size={16}
-            question={question}
-            data-metabase-event={`Notebook Mode; Convert to SQL Click`}
-          />
-        </ViewSQLButtonContainer>
+        <NativeQueryButton
+          size={16}
+          question={question}
+          data-metabase-event={`Notebook Mode; Convert to SQL Click`}
+        />
       )}
       {isNative && isSaved && <ExploreResultsLink question={question} />}
       {isRunnable && !isNativeEditorOpen && (
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader.styled.jsx b/frontend/src/metabase/query_builder/components/view/ViewHeader.styled.jsx
index 633b4f1539e..e5f41dc8383 100644
--- a/frontend/src/metabase/query_builder/components/view/ViewHeader.styled.jsx
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader.styled.jsx
@@ -48,14 +48,3 @@ export const SavedQuestionHeaderButtonContainer = styled.div`
   position: relative;
   right: 0.38rem;
 `;
-
-export const ViewSQLButtonContainer = styled.div`
-  margin-left: ${space(2)};
-  padding: ${space(1)};
-
-  cursor: pointer;
-  color: ${color("text-medium")};
-  :hover {
-    color: ${color("brand")};
-  }
-`;
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/AddAggregationButton/AddAggregationButton.styled.jsx b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/AddAggregationButton/AddAggregationButton.styled.jsx
index aaa3491fb65..f554a513100 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/AddAggregationButton/AddAggregationButton.styled.jsx
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/AddAggregationButton/AddAggregationButton.styled.jsx
@@ -1,7 +1,9 @@
 import styled from "styled-components";
 import { color } from "metabase/lib/colors";
 
-export const AddAggregationButtonRoot = styled.button`
+import { forwardRefToInnerRef } from "metabase/styled-components/utils";
+
+export const AddAggregationButtonRoot = forwardRefToInnerRef(styled.button`
   display: inline-flex;
   align-items: center;
   justify-content: center;
@@ -18,4 +20,4 @@ export const AddAggregationButtonRoot = styled.button`
   &:hover {
     background-color: ${color("bg-medium")};
   }
-`;
+`);
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/DimensionList/DimensionListItem/DimensionListItem.styled.jsx b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/DimensionList/DimensionListItem/DimensionListItem.styled.jsx
index d4c9229e877..f12444040f0 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/DimensionList/DimensionListItem/DimensionListItem.styled.jsx
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/DimensionList/DimensionListItem/DimensionListItem.styled.jsx
@@ -1,6 +1,7 @@
 import styled, { css } from "styled-components";
-import { color, alpha } from "metabase/lib/colors";
 
+import { color, alpha } from "metabase/lib/colors";
+import { forwardRefToInnerRef } from "metabase/styled-components/utils";
 import Icon from "metabase/components/Icon";
 
 export const SubDimensionButton = styled.button`
@@ -50,7 +51,7 @@ export const DimensionListItemRemoveButton = styled.button`
   }
 `;
 
-export const DimensionListItemAddButton = styled.button`
+export const _DimensionListItemAddButton = styled.button`
   display: flex;
   align-items: center;
   align-self: stretch;
@@ -62,6 +63,10 @@ export const DimensionListItemAddButton = styled.button`
   cursor: pointer;
 `;
 
+export const DimensionListItemAddButton = forwardRefToInnerRef(
+  _DimensionListItemAddButton,
+);
+
 export const DimensionListItemIcon = styled(Icon)`
   color: ${color("text-medium")};
 `;
@@ -99,12 +104,12 @@ const unselectedStyle = css`
   &:hover {
     ${DimensionListItemIcon},
     ${DimensionListItemContent},
-    ${DimensionListItemAddButton} {
+    ${_DimensionListItemAddButton} {
       color: ${color("accent1")};
       background-color: ${color("bg-light")};
     }
 
-    ${DimensionListItemAddButton}:hover {
+    ${_DimensionListItemAddButton}:hover {
       background-color: ${color("bg-medium")};
     }
 
diff --git a/frontend/src/metabase/styled-components/utils.tsx b/frontend/src/metabase/styled-components/utils.tsx
new file mode 100644
index 00000000000..acb9f5cb258
--- /dev/null
+++ b/frontend/src/metabase/styled-components/utils.tsx
@@ -0,0 +1,13 @@
+import React, { forwardRef } from "react";
+
+// this function should be removed after we update to styled-components v4
+export function forwardRefToInnerRef<Props>(
+  Component: React.ComponentType<Props>,
+) {
+  return forwardRef(function StyledComponentWithInnerRef(
+    props: Props,
+    ref?: React.Ref<any>,
+  ) {
+    return <Component {...props} innerRef={ref} />;
+  });
+}
diff --git a/frontend/src/metabase/visualizations/components/ChartTooltip.jsx b/frontend/src/metabase/visualizations/components/ChartTooltip.jsx
index fe44efebeee..ac9bf72eea2 100644
--- a/frontend/src/metabase/visualizations/components/ChartTooltip.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartTooltip.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 
-import TooltipPopover from "metabase/components/TooltipPopover";
+import Tooltip from "metabase/components/Tooltip";
 
 import { getFriendlyName } from "metabase/visualizations/lib/utils";
 import { formatValue } from "metabase/lib/formatting";
@@ -45,35 +45,42 @@ export default class ChartTooltip extends Component {
   render() {
     const { hovered, settings } = this.props;
     const rows = this._getRows();
-    const hasEventOrElement =
-      hovered &&
-      ((hovered.element && document.body.contains(hovered.element)) ||
-        hovered.event);
-    const isOpen = rows.length > 0 && !!hasEventOrElement;
-    return (
-      <TooltipPopover
-        target={hovered && hovered.element}
-        targetEvent={hovered && hovered.event}
-        verticalAttachments={["bottom", "top"]}
+
+    const hasTargetElement =
+      hovered?.element != null && document.body.contains(hovered.element);
+    const hasTargetEvent = hovered?.event != null;
+
+    const isOpen = (rows.length > 0 && hasTargetElement) || hasTargetEvent;
+
+    let target;
+    if (hasTargetElement) {
+      target = hovered.element;
+    } else if (hasTargetEvent) {
+      target = getEventTarget(hovered.event);
+    }
+
+    return target ? (
+      <Tooltip
+        reference={target}
         isOpen={isOpen}
-        // Make sure that for chart tooltips we don't constrain the width so longer strings don't get cut off
-        constrainedWidth={false}
-      >
-        <table className="py1 px2">
-          <tbody>
-            {rows.map(({ key, value, col }, index) => (
-              <TooltipRow
-                key={index}
-                name={key}
-                value={value}
-                column={col}
-                settings={settings}
-              />
-            ))}
-          </tbody>
-        </table>
-      </TooltipPopover>
-    );
+        tooltip={
+          <table className="py1 px2">
+            <tbody>
+              {rows.map(({ key, value, col }, index) => (
+                <TooltipRow
+                  key={index}
+                  name={key}
+                  value={value}
+                  column={col}
+                  settings={settings}
+                />
+              ))}
+            </tbody>
+          </table>
+        }
+        maxWidth="unset"
+      />
+    ) : null;
   }
 }
 
@@ -98,3 +105,16 @@ export function formatValueForTooltip({ value, column, settings }) {
     majorWidth: 0,
   });
 }
+
+function getEventTarget(event) {
+  let target = document.getElementById("popover-event-target");
+  if (!target) {
+    target = document.createElement("div");
+    target.id = "popover-event-target";
+    document.body.appendChild(target);
+  }
+  target.style.left = event.clientX - 3 + "px";
+  target.style.top = event.clientY - 3 + "px";
+
+  return target;
+}
diff --git a/frontend/test/__support__/e2e/helpers/e2e-ui-elements-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-ui-elements-helpers.js
index 6dd1c3b7046..dcc4723bf6a 100644
--- a/frontend/test/__support__/e2e/helpers/e2e-ui-elements-helpers.js
+++ b/frontend/test/__support__/e2e/helpers/e2e-ui-elements-helpers.js
@@ -3,6 +3,10 @@ export function popover() {
   return cy.get(".PopoverContainer.PopoverContainer--open");
 }
 
+export function tooltip() {
+  return cy.get(".tippy-box[data-state~='visible']");
+}
+
 export function modal() {
   return cy.get(".ModalContainer .ModalContent");
 }
diff --git a/frontend/test/metabase/scenarios/dashboard/dashboard-drill.cy.spec.js b/frontend/test/metabase/scenarios/dashboard/dashboard-drill.cy.spec.js
index c6393ea3273..2820b34e4e2 100644
--- a/frontend/test/metabase/scenarios/dashboard/dashboard-drill.cy.spec.js
+++ b/frontend/test/metabase/scenarios/dashboard/dashboard-drill.cy.spec.js
@@ -2,6 +2,7 @@ import {
   restore,
   modal,
   popover,
+  tooltip,
   filterWidget,
   showDashboardCardActions,
 } from "__support__/e2e/cypress";
@@ -736,7 +737,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
             .first()
             .trigger("mousemove");
 
-          popover().within(() => {
+          tooltip().within(() => {
             testPairedTooltipValues("AXIS", "1");
             testPairedTooltipValues("VALUE", "5");
           });
@@ -745,7 +746,7 @@ describe("scenarios > dashboard > dashboard drill", () => {
             .last()
             .trigger("mousemove");
 
-          popover().within(() => {
+          tooltip().within(() => {
             testPairedTooltipValues("AXIS", "1");
             testPairedTooltipValues("VALUE", "10");
           });
diff --git a/frontend/test/metabase/scenarios/dashboard/dashboard.cy.spec.js b/frontend/test/metabase/scenarios/dashboard/dashboard.cy.spec.js
index 5f73c986482..952e9961abf 100644
--- a/frontend/test/metabase/scenarios/dashboard/dashboard.cy.spec.js
+++ b/frontend/test/metabase/scenarios/dashboard/dashboard.cy.spec.js
@@ -7,6 +7,7 @@ import {
   filterWidget,
   sidebar,
 } from "__support__/e2e/cypress";
+import { modal } from "__support__/e2e/helpers/e2e-ui-elements-helpers";
 
 import { SAMPLE_DATASET } from "__support__/e2e/cypress_sample_dataset";
 
@@ -28,9 +29,11 @@ describe("scenarios > dashboard", () => {
     cy.visit("/");
     cy.icon("add").click();
     cy.findByText("New dashboard").click();
-    cy.findByLabelText("Name").type("Test Dash");
-    cy.findByLabelText("Description").type("Desc");
-    cy.findByText("Create").click();
+    modal().within(() => {
+      cy.findByLabelText("Name").type("Test Dash");
+      cy.findByLabelText("Description").type("Desc");
+      cy.findByText("Create").click();
+    });
     cy.findByText("This dashboard is looking empty.");
     cy.findByText("You're editing this dashboard.");
 
diff --git a/frontend/test/metabase/scenarios/visualizations/drillthroughs/chart_drill.cy.spec.js b/frontend/test/metabase/scenarios/visualizations/drillthroughs/chart_drill.cy.spec.js
index 3a6e8005790..43ed277e950 100644
--- a/frontend/test/metabase/scenarios/visualizations/drillthroughs/chart_drill.cy.spec.js
+++ b/frontend/test/metabase/scenarios/visualizations/drillthroughs/chart_drill.cy.spec.js
@@ -3,6 +3,7 @@ import {
   openProductsTable,
   openOrdersTable,
   popover,
+  tooltip,
   sidebar,
   visitQuestionAdhoc,
   visualize,
@@ -397,13 +398,13 @@ describe("scenarios > visualizations > drillthroughs > chart drill", () => {
       cy.visit(`/question/${QUESTION_ID}`);
 
       clickLineDot({ index: 0 });
-      popover().within(() => {
+      tooltip().within(() => {
         cy.findByText("January 1, 2020");
         cy.findByText("10");
       });
 
       clickLineDot({ index: 1 });
-      popover().within(() => {
+      tooltip().within(() => {
         cy.findByText("January 2, 2020");
         cy.findByText("5");
       });
@@ -430,7 +431,7 @@ describe("scenarios > visualizations > drillthroughs > chart drill", () => {
     cy.get(".bar")
       .last()
       .trigger("mousemove");
-    popover().findByText("12");
+    tooltip().findByText("12");
   });
 
   it.skip("should drill-through a custom question that joins a native SQL question (metabase#14495)", () => {
@@ -613,7 +614,7 @@ describe("scenarios > visualizations > drillthroughs > chart drill", () => {
       .first()
       .as("doohickeyChart")
       .trigger("mousemove");
-    popover().within(() => {
+    tooltip().within(() => {
       cy.findByText("Doohickey");
       cy.findByText("42");
     });
diff --git a/frontend/test/metabase/scenarios/visualizations/line_chart.cy.spec.js b/frontend/test/metabase/scenarios/visualizations/line_chart.cy.spec.js
index 7fc69e7e64e..711d57ad99f 100644
--- a/frontend/test/metabase/scenarios/visualizations/line_chart.cy.spec.js
+++ b/frontend/test/metabase/scenarios/visualizations/line_chart.cy.spec.js
@@ -1,4 +1,9 @@
-import { restore, visitQuestionAdhoc, popover } from "__support__/e2e/cypress";
+import {
+  restore,
+  visitQuestionAdhoc,
+  popover,
+  tooltip,
+} from "__support__/e2e/cypress";
 import { SAMPLE_DATASET } from "__support__/e2e/cypress_sample_dataset";
 
 const { ORDERS, ORDERS_ID, PRODUCTS, PRODUCTS_ID } = SAMPLE_DATASET;
@@ -123,7 +128,7 @@ describe("scenarios > visualizations > line chart", () => {
       .find(".dot")
       .eq(3)
       .trigger("mousemove", { force: true });
-    popover().within(() => {
+    tooltip().within(() => {
       testPairedTooltipValues("Product → Rating", "2.7");
       testPairedTooltipValues("Count", "191");
       testPairedTooltipValues("Sum of Total", "14,747.05");
@@ -240,13 +245,13 @@ describe("scenarios > visualizations > line chart", () => {
             assertOnYAxisValues();
 
             showTooltipForFirstCircleInSeries(0);
-            popover().within(() => {
+            tooltip().within(() => {
               testPairedTooltipValues("Created At", "2016");
               testPairedTooltipValues(RENAMED_FIRST_SERIES, "42,156.87");
             });
 
             showTooltipForFirstCircleInSeries(1);
-            popover().within(() => {
+            tooltip().within(() => {
               testPairedTooltipValues("Created At", "2016");
               testPairedTooltipValues(RENAMED_SECOND_SERIES, "54.44");
             });
diff --git a/frontend/test/metabase/scenarios/visualizations/reproductions/18063-maps-null-location-wrong-tooltip.cy.spec.js b/frontend/test/metabase/scenarios/visualizations/reproductions/18063-maps-null-location-wrong-tooltip.cy.spec.js
index 59303eb4d74..944c0528cc9 100644
--- a/frontend/test/metabase/scenarios/visualizations/reproductions/18063-maps-null-location-wrong-tooltip.cy.spec.js
+++ b/frontend/test/metabase/scenarios/visualizations/reproductions/18063-maps-null-location-wrong-tooltip.cy.spec.js
@@ -1,4 +1,4 @@
-import { restore, popover } from "__support__/e2e/cypress";
+import { restore, popover, tooltip } from "__support__/e2e/cypress";
 
 const questionDetails = {
   name: "18063",
@@ -39,7 +39,7 @@ describe.skip("issue 18063", () => {
 
     cy.get(".leaflet-marker-icon").trigger("mousemove");
 
-    popover().within(() => {
+    tooltip().within(() => {
       testPairedTooltipValues("LATITUDE", "55.6761");
       testPairedTooltipValues("LONGITUDE", "12.5683");
       testPairedTooltipValues("COUNT", "1");
diff --git a/frontend/test/metabase/scenarios/visualizations/scatter.cy.spec.js b/frontend/test/metabase/scenarios/visualizations/scatter.cy.spec.js
index 065dfce2939..3ece69151f9 100644
--- a/frontend/test/metabase/scenarios/visualizations/scatter.cy.spec.js
+++ b/frontend/test/metabase/scenarios/visualizations/scatter.cy.spec.js
@@ -1,4 +1,4 @@
-import { restore, visitQuestionAdhoc, popover } from "__support__/e2e/cypress";
+import { restore, visitQuestionAdhoc, tooltip } from "__support__/e2e/cypress";
 import { SAMPLE_DATASET } from "__support__/e2e/cypress_sample_dataset";
 
 const { ORDERS, ORDERS_ID, PRODUCTS } = SAMPLE_DATASET;
@@ -38,7 +38,7 @@ describe("scenarios > visualizations > scatter", () => {
     });
 
     triggerPopoverForBubble();
-    popover().within(() => {
+    tooltip().within(() => {
       cy.findByText("Created At:");
       cy.findByText("Count:");
       cy.findByText(/Distinct values of Products? → ID:/);
@@ -64,7 +64,7 @@ describe("scenarios > visualizations > scatter", () => {
     });
 
     triggerPopoverForBubble();
-    popover().within(() => {
+    tooltip().within(() => {
       cy.findByText("Created At:");
       cy.findByText("Orders count:");
       cy.findByText("Products count:");
diff --git a/package.json b/package.json
index 3470eab0eaa..9a6954ab9fe 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
   },
   "dependencies": {
     "@snowplow/browser-tracker": "^3.1.6",
+    "@tippyjs/react": "^4.2.6",
     "@visx/axis": "1.8.0",
     "@visx/clip-path": "^2.1.0",
     "@visx/grid": "1.16.0",
@@ -66,6 +67,7 @@
     "react-element-to-jsx-string": "^13.1.0",
     "react-grid-layout": "^1.2.5",
     "react-hot-loader": "^4.13.0",
+    "react-is": "^17.0.2",
     "react-markdown": "^6.0.2",
     "react-motion": "^0.4.5",
     "react-redux": "^5.0.4",
@@ -94,6 +96,7 @@
     "styled-components": "3.2.6",
     "styled-system": "2.2.5",
     "tether": "^1.2.0",
+    "tippy.js": "^6.3.5",
     "ttag": "1.7.15",
     "underscore": "^1.8.3",
     "yarn.lock": "^0.0.1-security",
@@ -157,6 +160,7 @@
     "@types/react-copy-to-clipboard": "^5.0.2",
     "@types/react-dom": "~17.0.9",
     "@types/react-grid-layout": "^1.1.3",
+    "@types/react-is": "^17.0.3",
     "@types/react-motion": "^0.0.31",
     "@types/react-redux": "^5.0.22",
     "@types/react-resizable": "^1.7.4",
diff --git a/yarn.lock b/yarn.lock
index 31c3d8e21f8..92587f7e219 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2137,6 +2137,11 @@
   dependencies:
     "@percy/logger" "1.0.0-beta.65"
 
+"@popperjs/core@^2.9.0":
+  version "2.10.2"
+  resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
+  integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==
+
 "@samverschueren/stream-to-observable@^0.3.0":
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301"
@@ -2270,6 +2275,13 @@
   dependencies:
     "@babel/runtime" "^7.12.5"
 
+"@tippyjs/react@^4.2.6":
+  version "4.2.6"
+  resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.6.tgz#971677a599bf663f20bb1c60a62b9555b749cc71"
+  integrity sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==
+  dependencies:
+    tippy.js "^6.3.1"
+
 "@tootallnate/once@1":
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -2877,6 +2889,13 @@
   dependencies:
     "@types/react" "*"
 
+"@types/react-is@^17.0.3":
+  version "17.0.3"
+  resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a"
+  integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react-motion@^0.0.31":
   version "0.0.31"
   resolved "https://registry.yarnpkg.com/@types/react-motion/-/react-motion-0.0.31.tgz#93e2abfc5056c5c0f47650ff9ed23533857245c5"
@@ -13691,7 +13710,7 @@ react-is@^16.12.0, react-is@^16.13.0, react-is@^16.3.1, react-is@^16.6.0, react-
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
-react-is@^17.0.0:
+react-is@^17.0.0, react-is@^17.0.2:
   version "17.0.2"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
   integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
@@ -15820,6 +15839,13 @@ tinycolor2@^1.4.1:
   resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
   integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
 
+tippy.js@^6.3.1, tippy.js@^6.3.5:
+  version "6.3.5"
+  resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.5.tgz#cbc99d34f87ccc127e6460032b86c8d47971d38f"
+  integrity sha512-B9hAQ5KNF+jDJRg6cRysV6Y3J+5fiNfD60GuXR5TP0sfrcltpgdzVc7f1wMtjQ3W0+Xsy80CDvk0Z+Vr0cM4sQ==
+  dependencies:
+    "@popperjs/core" "^2.9.0"
+
 tmp@~0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
-- 
GitLab