mirror of
https://github.com/facebook/sapling.git
synced 2024-10-05 14:28:17 +03:00
tooltip: support click-to-show-component and hover-to-show-title
Summary: It seems tooltip is used as a Popover (trigger=click, show `component`) and a Tooltip (trigger=hover, show `title`). I think it's also useful to work for both - show tooltip on hover, then show popover on click. This diff implements that. Reviewed By: evangrayk Differential Revision: D46188945 fbshipit-source-id: 30b7f64f5cbca7d031861cbb166dc0ba5c2b8b9c
This commit is contained in:
parent
c6ec8f1e4c
commit
c92082a1df
@ -21,13 +21,34 @@ type Placement = 'top' | 'bottom' | 'left' | 'right';
|
||||
*/
|
||||
export const DOCUMENTATION_DELAY = 750;
|
||||
|
||||
type TooltipProps = {
|
||||
children: ReactNode;
|
||||
placement?: Placement;
|
||||
/**
|
||||
* Applies delay to visual appearance of tooltip.
|
||||
* Note element is always constructed immediately.
|
||||
* This delay applies to all trigger methods except 'click'.
|
||||
* The delay is only on the leading-edge; disappearing is always instant.
|
||||
*/
|
||||
delayMs?: number;
|
||||
} & ExclusiveOr<
|
||||
ExclusiveOr<{trigger: 'manual'; shouldShow: boolean}, {trigger?: 'hover' | 'disabled'}> &
|
||||
ExclusiveOr<{component: (props: {dismiss: () => void}) => JSX.Element}, {title: string}>,
|
||||
{trigger: 'click'; component: (props: {dismiss: () => void}) => JSX.Element; title?: string}
|
||||
>;
|
||||
|
||||
type VisibleState =
|
||||
| true /* primary content (prefers component) is visible */
|
||||
| false
|
||||
| 'title' /* 'title', not 'component' is visible */;
|
||||
|
||||
/**
|
||||
* Enables child elements to render a tooltip when hovered/clicked.
|
||||
* Children are always rendered, but the tooltip is not rendered until triggered.
|
||||
* Tooltip is centered on bounding box of children.
|
||||
* You can adjust the trigger method:
|
||||
* - 'hover' (default) to appear when mouse hovers container element
|
||||
* - 'click' to appear when mouse clicks container element
|
||||
* - 'click' to render `component` on click, render `title` on hover.
|
||||
* - 'manual' to control programmatically by providing `shouldShow` prop.
|
||||
* - 'disabled' to turn off hover/click support programmatically
|
||||
*
|
||||
@ -52,26 +73,15 @@ export function Tooltip({
|
||||
trigger: triggerProp,
|
||||
delayMs,
|
||||
shouldShow,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
placement?: Placement;
|
||||
/**
|
||||
* Applies delay to visual appearance of tooltip.
|
||||
* Note element is always constructed immediately.
|
||||
* This delay applies to all trigger methods, even 'manual'.
|
||||
* The delay is only on the leading-edge; disappearing is always instant.
|
||||
*/
|
||||
delayMs?: number;
|
||||
} & ExclusiveOr<
|
||||
{trigger: 'manual'; shouldShow: boolean},
|
||||
{trigger?: 'hover' | 'click' | 'disabled'}
|
||||
> &
|
||||
ExclusiveOr<{component: (props: {dismiss: () => void}) => JSX.Element}, {title: string}>) {
|
||||
}: TooltipProps) {
|
||||
const trigger = triggerProp ?? 'hover';
|
||||
const placement = placementProp ?? 'top';
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [visible, setVisible] = useState<VisibleState>(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const getContent = () => {
|
||||
if (visible === 'title') {
|
||||
return title;
|
||||
}
|
||||
return Component == null ? title : <Component dismiss={() => setVisible(false)} />;
|
||||
};
|
||||
|
||||
@ -123,11 +133,15 @@ export function Tooltip({
|
||||
// Using onMouseLeave directly on the div is unreliable if the component rerenders: https://github.com/facebook/react/issues/4492
|
||||
// Use a manually managed subscription instead.
|
||||
useLayoutEffect(() => {
|
||||
if (trigger !== 'hover') {
|
||||
const needHover = trigger === 'hover' || (trigger === 'click' && title != null);
|
||||
if (!needHover) {
|
||||
return;
|
||||
}
|
||||
const onMouseEnter = () => setVisible(true);
|
||||
const onMouseLeave = () => setVisible(false);
|
||||
// Do not change visible if 'click' shows the content.
|
||||
const onMouseEnter = () =>
|
||||
setVisible(vis => (trigger === 'click' ? (vis === true ? vis : 'title') : true));
|
||||
const onMouseLeave = () =>
|
||||
setVisible(vis => (trigger === 'click' && vis === true ? vis : false));
|
||||
const div = ref.current;
|
||||
div?.addEventListener('mouseenter', onMouseEnter);
|
||||
div?.addEventListener('mouseleave', onMouseLeave);
|
||||
@ -135,7 +149,10 @@ export function Tooltip({
|
||||
div?.removeEventListener('mouseenter', onMouseEnter);
|
||||
div?.removeEventListener('mouseleave', onMouseLeave);
|
||||
};
|
||||
}, [trigger]);
|
||||
}, [trigger, title]);
|
||||
|
||||
// Force delayMs to be 0 when `component` is shown by click.
|
||||
const realDelayMs = trigger === 'click' && visible === true ? 0 : delayMs;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -144,8 +161,8 @@ export function Tooltip({
|
||||
onClick={
|
||||
trigger === 'click'
|
||||
? (event: MouseEvent) => {
|
||||
if (!visible || !eventIsFromInsideTooltip(event)) {
|
||||
setVisible(val => !val);
|
||||
if (visible !== true || !eventIsFromInsideTooltip(event)) {
|
||||
setVisible(vis => vis !== true);
|
||||
// don't trigger global click listener in the same tick
|
||||
event.stopPropagation();
|
||||
}
|
||||
@ -153,7 +170,7 @@ export function Tooltip({
|
||||
: undefined
|
||||
}>
|
||||
{visible && ref.current && (
|
||||
<RenderTooltipOnto delayMs={delayMs} element={ref.current} placement={placement}>
|
||||
<RenderTooltipOnto delayMs={realDelayMs} element={ref.current} placement={placement}>
|
||||
{getContent()}
|
||||
</RenderTooltipOnto>
|
||||
)}
|
||||
|
Loading…
Reference in New Issue
Block a user