Skip to content

Commit

Permalink
new(Tooltip): Add popover prop (#382)
Browse files Browse the repository at this point in the history
* Add `popover` prop, story, and tests to Tooltip component

* Address PR comments (part 1)

* Remove outdated comment

* Address PR comments
  • Loading branch information
FelixCodes authored Jun 30, 2020
1 parent db23e42 commit 19f4364
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 15 deletions.
96 changes: 81 additions & 15 deletions packages/core/src/components/Tooltip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const EMPTY_TARGET_RECT: ClientRect = {
width: 0,
};

const MOUSE_LEAVE_DELAY_MS = 100;

export type TooltipProps = {
/** Accessibility label. If not specified, all tooltip content is duplicated, rendered in an off-screen element with a separate layer. */
accessibilityLabel?: string;
Expand All @@ -34,6 +36,8 @@ export type TooltipProps = {
onClose?: () => void;
/** Callback fired when the tooltip is shown. */
onShow?: () => void;
/** True to enable interactive popover functionality */
popover?: boolean;
/** True to prevent dismissmal on mouse down. */
remainOnMouseDown?: boolean;
/** True to toggle tooltip display on click. */
Expand Down Expand Up @@ -73,6 +77,7 @@ export class Tooltip extends React.Component<TooltipProps & WithStylesProps, Too
inverted: false,
onClose() {},
onShow() {},
popover: false,
remainOnMouseDown: false,
toggleOnClick: false,
underlined: false,
Expand All @@ -95,6 +100,8 @@ export class Tooltip extends React.Component<TooltipProps & WithStylesProps, Too

rafHandle: number = 0;

popoverDelayTimeoutId = 0;

static getDerivedStateFromProps({ disabled }: TooltipProps) {
if (disabled) {
return {
Expand Down Expand Up @@ -194,7 +201,11 @@ export class Tooltip extends React.Component<TooltipProps & WithStylesProps, Too

private handleMouseEnter = () => {
if (!this.props.toggleOnClick) {
this.handleOpen();
if (this.props.popover) {
this.delayedSetPopoverVisible(true);
} else {
this.handleOpen();
}
}
};

Expand All @@ -213,10 +224,49 @@ export class Tooltip extends React.Component<TooltipProps & WithStylesProps, Too

private handleMouseLeave = () => {
if (!this.props.toggleOnClick) {
this.handleClose();
if (this.props.popover) {
this.delayedSetPopoverVisible(false, MOUSE_LEAVE_DELAY_MS);
} else {
this.handleClose();
}
}
};

setPopoverVisible(open: boolean) {
const { open: previousOpen } = this.state;
this.clearDelayTimer();
if (previousOpen !== open) {
if (open) {
this.handleOpen();
} else {
this.handleClose();
}
}
}

clearDelayTimer() {
clearTimeout(this.popoverDelayTimeoutId);
}

delayedSetPopoverVisible(open: boolean, delayMs: number = 0) {
this.clearDelayTimer();
if (delayMs) {
this.popoverDelayTimeoutId = window.setTimeout(() => {
this.setPopoverVisible(open);
}, delayMs);
} else {
this.setPopoverVisible(open);
}
}

handlePopoverMouseEnter = () => {
this.clearDelayTimer();
};

handlePopoverMouseLeave = () => {
this.delayedSetPopoverVisible(false, MOUSE_LEAVE_DELAY_MS);
};

private renderPopUp() {
const {
horizontalAlign,
Expand All @@ -227,6 +277,7 @@ export class Tooltip extends React.Component<TooltipProps & WithStylesProps, Too
content,
inverted,
verticalAlign,
popover,
} = this.props;
const { open, targetRect, tooltipHeight, targetRectReady } = this.state;

Expand All @@ -250,22 +301,37 @@ export class Tooltip extends React.Component<TooltipProps & WithStylesProps, Too
const distance = unit / 2;
const invert = inverted || Tooltip.inverted;

return (
<Overlay noBackground open={open} onClose={this.handleClose}>
const handleMouseEnter = popover ? this.handlePopoverMouseEnter : undefined;
const handleMouseLeave = popover ? this.handlePopoverMouseLeave : undefined;

const popupContent = (
<div
ref={this.handleTooltipRef}
role="tooltip"
className={cx(styles.tooltip, above ? styles.tooltip_above : styles.tooltip_below, {
width,
marginLeft: marginLeft[align as keyof StyleStruct],
marginTop: above ? -(tooltipHeight + targetRect.height + distance) : distance,
textAlign: align,
})}
>
<div
ref={this.handleTooltipRef}
role="tooltip"
className={cx(styles.tooltip, above ? styles.tooltip_above : styles.tooltip_below, {
width,
marginLeft: marginLeft[align as keyof StyleStruct],
marginTop: above ? -(tooltipHeight + targetRect.height + distance) : distance,
textAlign: align,
})}
className={cx(styles.content, invert && styles.content_inverted)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className={cx(styles.content, invert && styles.content_inverted)}>
<Text inverted={invert}>{content}</Text>
</div>
<Text inverted={invert}>{content}</Text>
</div>
</div>
);

if (popover) {
return open && <div className={cx(styles.popover)}>{popupContent}</div>;
}

return (
<Overlay noBackground open={open} onClose={this.handleClose}>
{popupContent}
</Overlay>
);
}
Expand Down
103 changes: 103 additions & 0 deletions packages/core/src/components/Tooltip/story.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from 'react';
import LoremIpsum from ':storybook/components/LoremIpsum';
import { withexpandMultiple } from '../Accordion/story';
import Button from '../Button';
import Text from '../Text';
import Spacing from '../Spacing';
import Link from '../Link';
import Tooltip from '.';

class TooltipDemo extends React.Component<{}, { text: string; clicked: boolean }> {
Expand Down Expand Up @@ -286,3 +288,104 @@ export function withAccessibilityLabel() {
withAccessibilityLabel.story = {
name: 'With accessibility label',
};

export function popoverDisplaysWhenAnElementIsHovered() {
const content = (
<>
<Text>Links inside popovers are clickable!</Text>
<Link openInNewWindow href="https://github.com/airbnb/lunar">
Click me!
</Link>
</>
);
return (
<div>
<Tooltip popover content={content}>
<Button>Hover Me</Button>
</Tooltip>

<Text inline>← Has a popover</Text>

<Text>
<LoremIpsum />
</Text>

<div style={{ textAlign: 'right' }}>
<Text inline>Also has a popover →</Text>
<Tooltip
inverted
popover
content={
<Text inverted>This uncomfortably wide Popover should have left-aligned text.</Text>
}
width={100}
>
<Button>Hover Me</Button>
</Tooltip>
</div>

<Text>
<LoremIpsum />
</Text>

<div style={{ textAlign: 'center' }}>
<Tooltip popover content="This Popover should most definitely be centered" width={21}>
<Button>
Hover Me too
<br />
please
</Button>
</Tooltip>
</div>
</div>
);
}

popoverDisplaysWhenAnElementIsHovered.story = {
name: 'Popover displays when an element is hovered and popover prop is enabled.',
};

export function popoverExpandsAndShrinksWithContent() {
return (
<div>
<Tooltip popover content={withexpandMultiple()}>
<Button>Hover Me</Button>
</Tooltip>

<Text inline>← Has a popover that can expand and collapse</Text>

<Text>
<LoremIpsum />
</Text>

<Spacing top={10}>
<div style={{ textAlign: 'right' }}>
<Text inline>Also has a popover that can expand and collapse →</Text>
<Tooltip popover content={withexpandMultiple()} width={100}>
<Button>Hover Me</Button>
</Tooltip>
</div>
</Spacing>

<Text>
<LoremIpsum />
</Text>

<Spacing top={10}>
<div style={{ textAlign: 'center' }}>
<Tooltip popover content={withexpandMultiple()} width={100}>
<Button>
Hover Me too
<br />
please
</Button>
</Tooltip>
</div>
</Spacing>
</div>
);
}

popoverExpandsAndShrinksWithContent.story = {
name: 'Popover expands and shrinks with content.',
};
5 changes: 5 additions & 0 deletions packages/core/src/components/Tooltip/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,9 @@ export const styleSheetTooltip: StyleSheet = ({ unit, color, pattern, ui }) => (
content_inverted: {
backgroundColor: color.accent.blackout,
},

popover: {
position: 'absolute',
zIndex: 1,
},
});
35 changes: 35 additions & 0 deletions packages/core/test/components/Tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,41 @@ describe('<Tooltip />', () => {
});
});

describe('popover', () => {
beforeEach(() => {
wrapper.setProps({ popover: true });
wrapper.setState({ open: true });
});

it('does not yet close when the mouse left the popup for fewer than 100ms', () => {
jest.useFakeTimers();

childContainer.simulate('mouseleave');
// Popover is programmed with a 100ms lag between mouseleave and close
// so this shouldn't be closed yet
setTimeout(() => {
expect(wrapper.state('open')).toBeTruthy();
}, 99);
jest.runAllTimers();
});

it('closes when child exited for 100ms', () => {
jest.useFakeTimers();

childContainer.simulate('mouseleave');
// Popover is programmed with a 100ms lag between mouseleave and close
setTimeout(() => {
expect(wrapper.state('open')).not.toBeTruthy();
}, 100);
jest.runAllTimers();
});

it('closes when child mousedowned', () => {
childContainer.simulate('mousedown');
expect(wrapper.state('open')).not.toBeTruthy();
});
});

it('unmounts cleanly', () => {
const instance = wrapper.instance() as BaseTooltip;
wrapper.unmount();
Expand Down

0 comments on commit 19f4364

Please sign in to comment.