storybook/lib/components/src/tooltip/WithTooltip.tsx
2020-03-03 10:47:15 +01:00

158 lines
4.3 KiB
TypeScript

import React, { FunctionComponent, ReactNode, useState, useEffect } from 'react';
import { styled } from '@storybook/theming';
import { logger } from '@storybook/client-logger';
// import { withState, lifecycle } from 'recompose';
import { document } from 'global';
import TooltipTrigger from 'react-popper-tooltip';
import { Modifiers, Placement } from 'popper.js';
import { Tooltip } from './Tooltip';
// A target that doesn't speak popper
const TargetContainer = styled.div<{ mode: string }>`
display: inline-block;
cursor: ${props => (props.mode === 'hover' ? 'default' : 'pointer')};
`;
const TargetSvgContainer = styled.g<{ mode: string }>`
cursor: ${props => (props.mode === 'hover' ? 'default' : 'pointer')};
`;
interface WithHideFn {
onHide: () => void;
}
export interface WithTooltipPureProps {
svg?: boolean;
trigger?: 'none' | 'hover' | 'click' | 'right-click';
closeOnClick?: boolean;
placement?: Placement;
modifiers?: Modifiers;
hasChrome?: boolean;
tooltip: ReactNode | ((p: WithHideFn) => ReactNode);
children: ReactNode;
tooltipShown?: boolean;
onVisibilityChange?: (visibility: boolean) => void;
onDoubleClick?: () => void;
}
// Pure, does not bind to the body
const WithTooltipPure: FunctionComponent<WithTooltipPureProps> = ({
svg,
trigger,
closeOnClick,
placement,
modifiers,
hasChrome,
tooltip,
children,
tooltipShown,
onVisibilityChange,
...props
}) => {
const Container = svg ? TargetSvgContainer : TargetContainer;
return (
<TooltipTrigger
placement={placement}
trigger={trigger}
modifiers={modifiers}
tooltipShown={tooltipShown}
onVisibilityChange={onVisibilityChange}
tooltip={({
getTooltipProps,
getArrowProps,
tooltipRef,
arrowRef,
placement: tooltipPlacement,
}) => (
<Tooltip
hasChrome={hasChrome}
placement={tooltipPlacement}
tooltipRef={tooltipRef}
arrowRef={arrowRef}
arrowProps={getArrowProps()}
{...getTooltipProps()}
>
{typeof tooltip === 'function'
? tooltip({ onHide: () => onVisibilityChange(false) })
: tooltip}
</Tooltip>
)}
>
{({ getTriggerProps, triggerRef }) => (
// @ts-ignore
<Container ref={triggerRef} {...getTriggerProps()} {...props}>
{children}
</Container>
)}
</TooltipTrigger>
);
};
WithTooltipPure.defaultProps = {
svg: false,
trigger: 'hover',
closeOnClick: false,
placement: 'top',
modifiers: {},
hasChrome: true,
tooltipShown: false,
};
const WithToolTipState: FunctionComponent<WithTooltipPureProps & {
startOpen?: boolean;
}> = ({ startOpen, ...rest }) => {
const [tooltipShown, onVisibilityChange] = useState(startOpen || false);
useEffect(() => {
const hide = () => onVisibilityChange(false);
document.addEventListener('keydown', hide, false);
// Find all iframes on the screen and bind to clicks inside them (waiting until the iframe is ready)
const iframes: HTMLIFrameElement[] = Array.from(document.getElementsByTagName('iframe'));
const unbinders: (() => void)[] = [];
iframes.forEach(iframe => {
const bind = () => {
try {
if (iframe.contentWindow.document) {
iframe.contentWindow.document.addEventListener('click', hide);
unbinders.push(() => {
try {
iframe.contentWindow.document.removeEventListener('click', hide);
} catch (e) {
logger.debug('Removing a click listener from iframe failed: ', e);
}
});
}
} catch (e) {
logger.debug('Adding a click listener to iframe failed: ', e);
}
};
bind(); // I don't know how to find out if it's already loaded so I potentially will bind twice
iframe.addEventListener('load', bind);
unbinders.push(() => {
iframe.removeEventListener('load', bind);
});
});
return () => {
document.removeEventListener('keydown', hide);
unbinders.forEach(unbind => {
unbind();
});
};
});
return (
<WithTooltipPure
{...rest}
tooltipShown={tooltipShown}
onVisibilityChange={onVisibilityChange}
/>
);
};
export { WithTooltipPure, WithToolTipState, WithToolTipState as WithTooltip };