Skip to content

Commit

Permalink
feat(Tooltip)!: introduce 2.0 component
Browse files Browse the repository at this point in the history
- add in new styles
- preserve existing API, tests, etc.
  • Loading branch information
booc0mtaco committed Mar 25, 2024
1 parent 9dfd62d commit 3870b47
Show file tree
Hide file tree
Showing 7 changed files with 743 additions and 0 deletions.
109 changes: 109 additions & 0 deletions src/components/Tooltip/Tooltip-v2.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*------------------------------------*\
# TOOLTIP
\*------------------------------------*/

.tooltip {
border-style: solid;
border-width: var(--eds-border-width-sm);
border-radius: calc(var(--eds-theme-border-radius-surfaces-md) * 1px);
box-shadow: var(--eds-box-shadow-md);
max-width: 14rem;

@media (prefers-reduced-motion) {
transition: none;
}

border-color: var(--eds-theme-color-background-utility-default-high-emphasis);
color: var(--eds-theme-color-text-utility-inverse);
background-color: var(--eds-theme-color-background-utility-default-high-emphasis);
--arrow-color: var(--eds-theme-color-background-utility-default-high-emphasis);
}

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

/* TODO-AH: consider finding a way to not use these module semantics, e.g., global: */
.tooltip :global(.tippy-content) {
padding-left: 1rem;
padding-right: 1rem;
padding-bottom: 0.5rem;
padding-top: 0.5rem;
}

/**
* Add arrows
*/
.tooltip :global(.tippy-arrow) {
position: absolute;

width: 1rem;
height: 1rem;
}

.tooltip :global(.tippy-arrow::before) {
position: absolute;

border-style: solid;
border-color: transparent;
border-width: 0.5rem;

content: '';
}

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

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

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

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

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

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

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

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

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

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

.tooltip[data-placement^='bottom'] :global(.tippy-arrow::before) {
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;
}
123 changes: 123 additions & 0 deletions src/components/Tooltip/Tooltip-v2.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { Tooltip } from './Tooltip-v2';

// diminishing the threshold of this component to avoid sub-pixel jittering
// https://www.chromatic.com/docs/threshold
const diffThreshold = 0.75;
const defaultArgs = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a erat eu augue consequat eleifend non vel sem. Praesent efficitur mauris ac leo semper accumsan.',
children: <div className="fpo p-1">Target Component</div>,
placement: 'right',
// most stories show a visible, non-interactive tooltip.
// this turns animation off to ensure stable visual snapshots
duration: 0,
visible: true,
};

export default {
title: 'Components/V2/Tooltip',
component: Tooltip,
args: defaultArgs,
argTypes: {
text: {
control: {
type: 'text',
},
},
children: {
control: {
type: null,
},
},
placement: {
table: {
defaultValue: { summary: 'top' },
},
},
},
parameters: {
layout: 'centered',
badges: ['intro-1.0', 'current-2.0'],
chromatic: {
diffThreshold,
diffIncludeAntiAliasing: false,
},
},
decorators: [(Story) => <div className="p-8">{Story()}</div>],
} as Meta<Args>;

type Args = React.ComponentProps<typeof Tooltip>;
type Story = StoryObj<Args>;

/**
* The following stories demonstrate how `Tooltip` can be made to appear on different sides of the trigger.
* Each story name denotes a value pased to `placement`.
*/
export const LeftPlacement: Story = {
args: {
placement: 'left',
children: <div className="fpo p-1">Target Component</div>,
},
parameters: {
chromatic: { disableSnapshot: true },
},
};

export const TopPlacement: Story = {
args: {
placement: 'top',
children: <div className="fpo p-1">Target Component</div>,
},
};

export const BottomPlacement: Story = {
args: {
placement: 'bottom',
children: <div className="fpo p-1">Target Component</div>,
},
};

export const LongText: Story = {
args: {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a erat eu augue consequat eleifend non vel sem. 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.',
},
};

export const LongTriggerText: Story = {
args: {
children: <div className="fpo p-1">Longer text to test placement</div>,
},
};

/**
* Hover over the button to make the tooltip appear.
*/
export const Interactive: Story = {
args: {
// reset prop values defined in defaultArgs
duration: undefined,
visible: undefined,
children: <button className="fpo p-1">Target Component</button>,
},
};

/**
* Hover over the button to make the tooltip appear.
*/
export const InteractiveDisabled: Story = {
args: {
duration: undefined,
},
render: (args) => (
<Tooltip
childNotInteractive
duration={args.duration}
placement="top"
text={defaultArgs.text}
>
<div className="fpo p-1">Target Component</div>
</Tooltip>
),
};
42 changes: 42 additions & 0 deletions src/components/Tooltip/Tooltip-v2.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { generateSnapshots } from '@chanzuckerberg/story-utils';
import { composeStories } from '@storybook/testing-react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import * as TooltipStoryFile from './Tooltip-v2.stories';

const { Interactive, InteractiveDisabled } = composeStories(TooltipStoryFile);

describe('<Tooltip /> (v2)', () => {
generateSnapshots(TooltipStoryFile, {
// Tippy renders tooltip as a child of <body> and hence is why baseElement needs to be targetted
getElement: (wrapper) => {
return wrapper.baseElement;
},
});

it('should close tooltip via escape key', async () => {
const user = userEvent.setup();
// disable animation for test
render(<Interactive duration={0} />);
const trigger = await screen.findByRole('button');
expect(screen.queryByTestId('tooltip-content')).not.toBeInTheDocument();
await user.hover(trigger);
expect(screen.getByTestId('tooltip-content')).toBeInTheDocument();
await user.keyboard('{Escape}');
expect(screen.queryByTestId('tooltip-content')).not.toBeInTheDocument();
});

it('should close tooltip via escape key for disabled buttons', async () => {
const user = userEvent.setup();
// disable animation for test
render(<InteractiveDisabled duration={0} />);
const trigger = await screen.findByTestId('disabled-child-tooltip-wrapper');
expect(screen.queryByTestId('tooltip-content')).not.toBeInTheDocument();
await user.hover(trigger);
expect(screen.getByTestId('tooltip-content')).toBeInTheDocument();
await user.keyboard('{Escape}');
expect(screen.queryByTestId('tooltip-content')).not.toBeInTheDocument();
});
});
Loading

0 comments on commit 3870b47

Please sign in to comment.