Skip to content

Commit

Permalink
Merge pull request #1257 from chanzuckerberg/diedra/tooltip/disabled-…
Browse files Browse the repository at this point in the history
…button

feat(tooltip): add childDisabled prop
  • Loading branch information
dierat authored Aug 31, 2022
2 parents abd6054 + b18d657 commit 75d8fcf
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 26 deletions.
8 changes: 8 additions & 0 deletions src/components/Tooltip/Tooltip.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@
--arrow-color: var(--eds-color-neutral-700);
}

/**
* 1) When childDisabled is true and alignment is top or bottom
*/
.tooltip__child-disabled-wrapper--vertical {
/* Needed for the top and bottom alignment to work properly */
display: inline-flex;
}

/**
* 1) Add arrows
*/
Expand Down
66 changes: 56 additions & 10 deletions src/components/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { BADGE } from '@geometricpanda/storybook-addon-badges';
import type { Meta, Story, StoryObj } from '@storybook/react';
import { within, userEvent } from '@storybook/testing-library';
import clsx from 'clsx';
import React from 'react';
import { Tooltip } from './Tooltip';
Expand Down Expand Up @@ -134,31 +133,78 @@ export const LongButtonText: StoryObj<Args> = {
},
};

export const Disabled: StoryObj<Args> = {
render: () => (
<div
className={clsx(
styles['trigger--spacing-top'],
styles['trigger--spacing-left'],
)}
>
<Tooltip
align="top"
childDisabled={true}
text={defaultArgs.text}
visible={true}
>
<Button disabled variant="primary">
Tooltip trigger
</Button>
</Tooltip>
</div>
),
};

export const Interactive: StoryObj<Args> = {
args: {
// reset prop values defined in defaultArgs
duration: undefined,
visible: undefined,
children: (
<Button className={clsx(styles['trigger--spacing'])} variant="primary">
Hover here to see tooltip after clicking somewhere outside.
Tooltip trigger
</Button>
),
},
decorators: [
(Story: Story) => (
<div>
<p>
Click somewhere in this area to dismiss the tooltip, then hover over
the button to make it reappear.
</p>
<p>Hover over the button to make the tooltip appear.</p>
<Story />
</div>
),
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const trigger = await canvas.findByRole('button');
await userEvent.hover(trigger);
};

export const InteractiveDisabled: StoryObj<Args> = {
args: {
duration: undefined,
},
render: (args) => (
<div
className={clsx(
styles['trigger--spacing-top'],
styles['trigger--spacing-left'],
)}
>
<Tooltip
align="top"
childDisabled={true}
duration={args.duration}
text={defaultArgs.text}
>
<Button disabled variant="primary">
Tooltip trigger
</Button>
</Tooltip>
</div>
),
decorators: [
(Story: Story) => (
<div>
<p>Hover over the button to make the tooltip appear.</p>
<Story />
</div>
),
],
};
13 changes: 12 additions & 1 deletion src/components/Tooltip/Tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React from 'react';
import * as TooltipStoryFile from './Tooltip.stories';
import consoleWarnMockHelper from '../../../jest/helpers/consoleWarnMock';

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

describe('<Tooltip />', () => {
const warnMock = consoleWarnMockHelper();
Expand All @@ -29,6 +29,17 @@ describe('<Tooltip />', () => {
expect(screen.queryByTestId('tooltip-content')).not.toBeInTheDocument();
});

it('should close tooltip via escape key for disabled buttons', async () => {
// 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 userEvent.hover(trigger);
expect(screen.getByTestId('tooltip-content')).toBeInTheDocument();
await userEvent.keyboard('{esc}');
expect(screen.queryByTestId('tooltip-content')).not.toBeInTheDocument();
});

it('should display warning message when attempting to use dark variant', () => {
render(<Interactive variant="dark" />);
expect(warnMock).toHaveBeenCalledTimes(1);
Expand Down
44 changes: 43 additions & 1 deletion src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ type TooltipProps = {
* The trigger element the tooltip appears next to.
*/
children?: React.ReactElement;
/**
* If the child being passed into the Tooltip via the `children` prop is disabled (e.g. a disabled button).
*
* Please note that spacing and placement styling will need to be added to a wrapper around the Tooltip,
* not on the button child inside the Tooltip, because there will be a wrapper around the button child. Example:
* <div className="spacing-goes-here"><Tooltip text="Tooltip text"><Button disabled>Button text</Button></Tooltip></div>
*/
childDisabled?: boolean;
/**
* Custom classname for additional styles.
*
Expand Down Expand Up @@ -82,10 +90,21 @@ type Plugin = Plugins[number];
*
* https://atomiks.github.io/tippyjs/
* https://github.com/atomiks/tippyjs-react
*
* Example usage:
*
* ```tsx
* <Tooltip>
* <Button variant="primary">
* Tooltip trigger
* </Button>
* </Tooltip>
* ```
*/
export const Tooltip = ({
variant = 'light',
align = 'top',
childDisabled,
className,
duration = 200,
text,
Expand All @@ -96,6 +115,7 @@ export const Tooltip = ({
'The dark variant is deprecated and will be removed in an upcoming release. Please use the default light variant instead.',
);
}

// Hides tooltip when escape key is pressed, following:
// https://atomiks.github.io/tippyjs/v6/plugins/#hideonesc
const hideOnEsc: Plugin = {
Expand All @@ -117,6 +137,26 @@ export const Tooltip = ({
};
},
};

let children = rest.children;
// Tippy only works on elements with a tabindex. If the child is disabled, we need to
// wrap it in an element with a tabindex in order for it to work.
if (childDisabled) {
children = (
<span
className={clsx(
(align === 'bottom' || align === 'top') &&
styles['tooltip__child-disabled-wrapper--vertical'],
)}
data-testid="disabled-child-tooltip-wrapper"
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
>
{rest.children}
</span>
);
}

return (
<Tippy
{...rest}
Expand All @@ -130,6 +170,8 @@ export const Tooltip = ({
duration={duration}
placement={align}
plugins={[hideOnEsc]}
/>
>
{children}
</Tippy>
);
};
85 changes: 71 additions & 14 deletions src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -110,33 +110,40 @@ exports[`<Tooltip /> DarkVariant story renders snapshot 1`] = `
</body>
`;

exports[`<Tooltip /> Interactive story renders snapshot 1`] = `
exports[`<Tooltip /> Disabled story renders snapshot 1`] = `
<body>
<div>
<div>
<p>
Click somewhere in this area to dismiss the tooltip, then hover over the button to make it reappear.
</p>
<button
<div
class="trigger--spacing-top trigger--spacing-left"
>
<span
aria-describedby="tippy-8"
class="clickable-style clickable-style--lg clickable-style--primary clickable-style--brand button button--primary trigger--spacing"
data-bootstrap-override="clickable-style-primary"
type="button"
class="tooltip__child-disabled-wrapper--vertical"
data-testid="disabled-child-tooltip-wrapper"
tabindex="0"
>
Hover here to see tooltip after clicking somewhere outside.
</button>
<button
class="clickable-style clickable-style--lg clickable-style--primary clickable-style--brand button button--primary button--disabled"
data-bootstrap-override="clickable-style-primary"
disabled=""
tabindex="-1"
type="button"
>
Tooltip trigger
</button>
</span>
</div>
</div>
<div
data-tippy-root=""
id="tippy-8"
style="pointer-events: none; z-index: 9999; visibility: visible; position: absolute; left: 0px; top: 0px; margin: 0px; transform: translate(10px, 0px);"
style="pointer-events: none; z-index: 9999; visibility: visible; position: absolute; left: 0px; top: 0px; margin: 0px; bottom: 0px; transform: translate(0px, -10px);"
>
<div
class="tippy-box tooltip tooltip--light"
data-animation="fade"
data-escaped=""
data-placement="right"
data-placement="top"
data-reference-hidden=""
data-state="visible"
role="tooltip"
Expand All @@ -163,13 +170,63 @@ exports[`<Tooltip /> Interactive story renders snapshot 1`] = `
</div>
<div
class="tippy-arrow"
style="position: absolute; top: 0px; transform: translate(0px, 3px);"
style="position: absolute; left: 0px; transform: translate(3px, 0px);"
/>
</div>
</div>
</body>
`;

exports[`<Tooltip /> Interactive story renders snapshot 1`] = `
<body>
<div>
<div>
<p>
Hover over the button to make the tooltip appear.
</p>
<button
class="clickable-style clickable-style--lg clickable-style--primary clickable-style--brand button button--primary trigger--spacing"
data-bootstrap-override="clickable-style-primary"
type="button"
>
Tooltip trigger
</button>
</div>
</div>
</body>
`;

exports[`<Tooltip /> InteractiveDisabled story renders snapshot 1`] = `
<body>
<div>
<div>
<p>
Hover over the button to make the tooltip appear.
</p>
<div
class="trigger--spacing-top trigger--spacing-left"
>
<span
class="tooltip__child-disabled-wrapper--vertical"
data-testid="disabled-child-tooltip-wrapper"
tabindex="0"
>
<button
class="clickable-style clickable-style--lg clickable-style--primary clickable-style--brand button button--primary button--disabled"
data-bootstrap-override="clickable-style-primary"
disabled=""
tabindex="-1"
type="button"
>
Tooltip trigger
</button>
</span>
</div>
</div>
</div>
</body>
`;

exports[`<Tooltip /> LeftPlacement story renders snapshot 1`] = `
<body>
<div>
Expand Down

0 comments on commit 75d8fcf

Please sign in to comment.