Skip to content
Snippets Groups Projects
Unverified Commit 80d81a10 authored by Dalton's avatar Dalton Committed by GitHub
Browse files

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
parent c7c4975b
No related branches found
No related tags found
No related merge requests found
Showing
with 463 additions and 271 deletions
......@@ -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"
......
......@@ -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;
......
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 =>
......
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;
`;
`);
......@@ -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 {
......
/* 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";
......
......@@ -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>
);
}
}
/* 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;
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;
......@@ -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)`
......
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;
......@@ -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
......
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>
),
};
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>
);
}
}
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 />,
};
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;
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();
});
});
export { default } from "./Tooltip";
/* 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);
......@@ -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}>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment