Skip to content

feat: migrate Tooltip component #801

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jan 25, 2022
Merged
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
},
"dependencies": {
"@chanzuckerberg/eds-tokens": "^0.5.0",
"@tippyjs/react": "^4.2.5",
"clsx": "^1.1.1",
"react-uid": "^2.3.1"
},
Expand Down
88 changes: 88 additions & 0 deletions packages/components/src/Tooltip/Tooltip.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* stylelint-disable selector-pseudo-class-no-unknown */
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better way via stylelintrc? Or would this be the only file with :global use?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine. 🤷

Or would this be the only file with :global use?

My guess is that we'll eventually end up doing it again as we start building on top of other components that are not from Headless UI and require us to dig in and target a specific element that's not exposed to us. But I don't have any specific examples; it's just a feeling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I think we can add to stylelint when we have to get across that bridge


.tooltip {
@apply border border-solid shadow-2 rounded-lg;
@apply motion-reduce:transition-none;
}

/* Enables opacity fade animation */
.tooltip[data-state='hidden'] {
@apply opacity-0;
}

.tooltip :global(.tippy-content) {
@apply px-4 py-2;
}

/* Color Variants */
.variantLight {
@apply border-neutral-300 text-neutral-700 bg-neutral-100;
--arrow-color: var(--eds-color-neutral-300);
}

.variantDark {
@apply border-neutral-700 text-white bg-neutral-700;
--arrow-color: var(--eds-color-neutral-700);
}

/* Add Arrows */
.tooltip :global(.tippy-arrow) {
@apply absolute w-4 h-4;
}

.tooltip :global(.tippy-arrow::before) {
@apply absolute border-solid border-transparent border-8;

content: '';
}

.tooltip[data-placement^='top'] :global(.tippy-arrow) {
@apply left-0;

transform: translate3d(73px, 0, 0);
}

.tooltip[data-placement^='bottom'] :global(.tippy-arrow) {
@apply top-0 left-0;

transform: translate3d(73px, 0, 0);
}

.tooltip[data-placement^='left'] :global(.tippy-arrow) {
@apply top-0 right-0;

transform: translate3d(0, 19px, 0);
}

.tooltip[data-placement^='right'] :global(.tippy-arrow) {
@apply top-0 left-0;

transform: translate3d(0, 25px, 0);
}

.tooltip[data-placement^='top'] :global(.tippy-arrow::before) {
@apply left-0;

border-top-color: var(--arrow-color);
border-bottom-width: 0;
}

.tooltip[data-placement^='bottom'] :global(.tippy-arrow::before) {
@apply left-0;

border-bottom-color: var(--arrow-color);
border-top-width: 0;
top: -7px;
}

.tooltip[data-placement^='left'] :global(.tippy-arrow::before) {
border-left-color: var(--arrow-color);
border-right-width: 0;
right: -7px;
}

.tooltip[data-placement^='right'] :global(.tippy-arrow::before) {
border-right-color: var(--arrow-color);
border-left-width: 0;
left: -7px;
}
10 changes: 10 additions & 0 deletions packages/components/src/Tooltip/Tooltip.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { generateSnapshots } from "@chanzuckerberg/story-utils";
import * as TooltipStoryFile from "./Tooltip.stories";

describe("<Tooltip />", () => {
generateSnapshots(TooltipStoryFile, {
getElement: (wrapper) => {
return wrapper.baseElement;
},
});
});
112 changes: 112 additions & 0 deletions packages/components/src/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { Meta, StoryObj } from "@storybook/react";
import * as React from "react";
import Button from "../Button";
import Tooltip from "./Tooltip";

const defaultArgs = {
content: (
<span>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.{" "}
<b>Donec a erat eu augue consequat eleifend non vel sem.</b> Praesent
efficitur mauris ac leo semper accumsan.
</span>
),
children: <Button className="mx-32 my-32">Tooltip trigger</Button>,
placement: "right",
visible: true,
};

export default {
title: "Tooltip",
component: Tooltip,
args: defaultArgs,
argTypes: {
variant: {
control: {
type: "select",
options: ["light", "dark"],
},
},
placement: {
control: {
type: "radio",
options: ["left", "right", "top", "bottom"],
},
},
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you don't need these in this repo. Try taking them out and see if you still see the controls on the first story on the docs tab.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh you are right, removed

} as Meta<Args>;

type Args = React.ComponentProps<typeof Tooltip>;

export const LightVariant: StoryObj<Args> = {};

export const DarkVariant: StoryObj<Args> = {
...LightVariant,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does using the LightVariant here do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not much, oops, removed from all the stories

args: {
variant: "dark",
},
};

export const LeftPlacement: StoryObj<Args> = {
...LightVariant,
args: {
placement: "left",
children: <Button className="ml-96 my-32">Tooltip trigger</Button>,
},
};

export const TopPlacement: StoryObj<Args> = {
...LightVariant,
args: {
placement: "top",
children: <Button className="mt-32 ml-32">Tooltip trigger</Button>,
},
};

export const BottomPlacement: StoryObj<Args> = {
...LightVariant,
args: {
placement: "bottom",
children: <Button className="mb-32 ml-32">Tooltip trigger</Button>,
},
};

export const LongText: StoryObj<Args> = {
...LightVariant,
args: {
content: (
<span>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.{" "}
<b>Donec a erat eu augue consequat eleifend non vel sem.</b> Praesent
efficitur mauris ac leo semper accumsan. Donec posuere semper fermentum.
Vivamus venenatis laoreet venenatis. Sed consectetur, dolor sed
tristique vehicula, sapien nulla convallis odio, et tempus urna mi eu
leo. Phasellus a venenatis sapien. Cras massa lectus, sollicitudin id
nulla id, laoreet facilisis est.
</span>
),
},
};

export const LongButtonText: StoryObj<Args> = {
...LightVariant,
args: {
children: (
<Button className="my-20">
Tooltip trigger with longer text to test placement
</Button>
),
},
};

export const Interactive = {
...LightVariant,
args: {
visible: undefined,
},
parameters: {
chromatic: {
disableSnapshot: true,
},
},
};
84 changes: 84 additions & 0 deletions packages/components/src/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Tippy from "@tippyjs/react";
import clsx from "clsx";
import * as React from "react";
import styles from "./Tooltip.module.css";

// Full list of Tippy props: https://atomiks.github.io/tippyjs/v6/all-props/
type TooltipProps = {
/**
* The trigger element the tooltip appears next to.
*/
children?: React.ReactElement;
/**
* The trigger element the tooltip appears next to.
*
* Use this instead of `children` if the trigger element is being
* stored in state or a ref. Most cases will use `children` and not
* `reference`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice prop doc!

Did the idea/reference/wording for storing an element in state come from us or from Tippy?

For my own knowledge, I'm curious as to the intended use case. Ref makes sense, but storing an element in state isn't usually necessary (and according to my own mental pattern matching would still work with children anyway).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think @dierat deserves all the praise for the work, I'm just migrating to EDS haha. Any thought's on Andrew's question?

Copy link
Contributor

@dierat dierat Jan 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember exactly where "state" came from here; I'm fine with dropping it. This is where we're currently using reference in the platform (and why we added this documentation): https://github.com/FB-PLP/traject/blob/7ab73ba57ce427d8d9b82ec587d89e6bd8121e8d/app/assets/javascripts/v2/teachers/schoolData/components/MetricCard/HorizontalBar.jsx#L171

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I say lets drop reference to state.

*/
reference?: React.RefObject<Element> | Element;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was React.Node in traject, but React.ReactNode not compatible with TippyProp's reference: React.RefObject<Element> | Element | null.

/**
* The content of the tooltip bubble.
*/
content?: React.ReactNode;
/**
* Where the tooltip should be placed in relation to the element it's attached to.
*
* Tippy also supports 'top-start', 'top-end', 'right-start', 'right-end', etc,
* but our CSS currently only supports the 4 main sides.
*/
placement?: "top" | "right" | "bottom" | "left";
/**
* Whether the tooltip is always visible or always invisible.
*
* This is most often left undefined so the Tooltip component
* controls if/when the bubble appears (on hover, click, focus, etc).
*/
visible?: boolean;
/**
* Custom classname for additional styles.
*
* These styles will only affect the tooltip bubble.
*/
className?: string;
/**
* Whether the tooltip has a light or dark background.
*/
variant?: "light" | "dark";
/**
* How long to delay the Tooltip showing and hiding, in milliseconds.
*
* If a single number is provided, it will be applied to showing and hiding.
* If an array with 2 numbers is provided, the first will apply to showing and
* the second will be applied to hiding.
* https://atomiks.github.io/tippyjs/v6/all-props/#delay
*/
delay?: number | [number | null, number | null];
};

/**
* A styled tooltip built on Tippy.js.
*
* https://atomiks.github.io/tippyjs/
* https://github.com/atomiks/tippyjs-react
*/
export default function Tooltip({
variant = "light",
placement = "top",
className,
...rest
}: TooltipProps) {
return (
<Tippy
{...rest}
className={clsx(
styles.tooltip,
className,
variant === "light" && styles.variantLight,
variant === "dark" && styles.variantDark,
)}
duration={200}
placement={placement}
/>
);
}
Loading