Skip to content
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

feat(Tooltip)!: introduce 2.0 component #1905

Merged
merged 1 commit into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/PopoverContainer/PopoverContainer-v2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const defaultPopoverModifiers: Options['modifiers'] = [
{
name: 'offset',
options: {
offset: [0, 10], // spaces the popover from the trigger element
offset: [0, 12], // spaces the popover from the trigger element
},
},
{
Expand Down
5 changes: 4 additions & 1 deletion src/components/PopoverContainer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@ export {
PopoverContainer as default,
defaultPopoverModifiers,
} from './PopoverContainer';
export { PopoverContainer as PopoverContainerV2 } from './PopoverContainer-v2';
export {
PopoverContainer as PopoverContainerV2,
defaultPopoverModifiers as defaultPopoverModifiersV2,
} from './PopoverContainer-v2';
export type { PopoverOptions, PopoverContext } from './PopoverContainer';
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
Loading