From 4e92d784e2b5e58166bce3590584d836efc7bd3c Mon Sep 17 00:00:00 2001 From: Sergei Garin Date: Wed, 5 Jun 2024 12:05:33 +0300 Subject: [PATCH] Don't hide visual tooltip when mouse goes from target to tooltip (#10177) --- .../components/AriaComponents/Text/Text.tsx | 11 -- .../AriaComponents/Text/useVisualTooltip.tsx | 121 ++++++++++++------ .../AriaComponents/Tooltip/Tooltip.tsx | 8 +- .../lib/dashboard/src/components/aria.tsx | 1 + .../lib/dashboard/src/tailwind.css | 9 +- 5 files changed, 95 insertions(+), 55 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Text/Text.tsx b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Text/Text.tsx index 1eb14d249ad..3d2893e4fbb 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Text/Text.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Text/Text.tsx @@ -126,19 +126,8 @@ export const Text = React.forwardRef(function Text( balance, }) - const tooltipTextClasses = TEXT_STYLE({ - variant, - weight, - transform, - monospace, - italic, - balance, - className: 'pointer-events-none', - }) - const { tooltip, targetProps } = visualTooltip.useVisualTooltip({ isDisabled: !truncate, - className: tooltipTextClasses, targetRef: textElementRef, display: 'whenOverflowing', children, diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Text/useVisualTooltip.tsx b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Text/useVisualTooltip.tsx index ac1a9a91d6a..8340fea445e 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Text/useVisualTooltip.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Text/useVisualTooltip.tsx @@ -6,16 +6,19 @@ import * as React from 'react' -import clsx from 'clsx' +import * as eventCallback from '#/hooks/eventCallbackHooks' import * as aria from '#/components/aria' import * as ariaComponents from '#/components/AriaComponents' import Portal from '#/components/Portal' +import * as mergeRefs from '#/utilities/mergeRefs' + /** * Props for {@link useVisualTooltip}. */ -export interface VisualTooltipProps { +export interface VisualTooltipProps + extends Pick { readonly children: React.ReactNode readonly className?: string readonly targetRef: React.RefObject @@ -40,6 +43,7 @@ export interface VisualTooltipProps { type DisplayStrategy = 'always' | 'whenOverflowing' const DEFAULT_OFFSET = 6 +const DEFAULT_DELAY = 250 /** * Creates a tooltip that appears when the target element is hovered over. @@ -57,6 +61,10 @@ export function useVisualTooltip(props: VisualTooltipProps) { overlayPositionProps = {}, display = 'always', testId = 'visual-tooltip', + rounded, + variant, + size, + maxWidth, } = props const { @@ -68,27 +76,43 @@ export function useVisualTooltip(props: VisualTooltipProps) { const popoverRef = React.useRef(null) const id = React.useId() - const { hoverProps, isHovered } = aria.useHover({ - onHoverStart: () => { - if (targetRef.current) { - const shouldDisplay = - typeof display === 'function' - ? display(targetRef.current) - : DISPLAY_STRATEGIES[display](targetRef.current) - - if (shouldDisplay) { - popoverRef.current?.showPopover() - } - } - }, - onHoverEnd: () => { - popoverRef.current?.hidePopover() - }, - isDisabled, + const state = aria.useTooltipTriggerState({ + closeDelay: DEFAULT_DELAY, + delay: DEFAULT_DELAY, }) - const { overlayProps } = aria.useOverlayPosition({ - isOpen: isHovered, + const handleHoverChange = eventCallback.useEventCallback((isHovered: boolean) => { + const shouldDisplay = () => { + if (isHovered && targetRef.current != null) { + return typeof display === 'function' + ? display(targetRef.current) + : DISPLAY_STRATEGIES[display](targetRef.current) + } else { + return false + } + } + + if (shouldDisplay()) { + state.open() + } else { + state.close() + } + }) + + const { hoverProps: targetHoverProps } = aria.useHover({ + isDisabled, + onHoverChange: handleHoverChange, + }) + const { hoverProps: tooltipHoverProps } = aria.useHover({ + isDisabled, + onHoverChange: handleHoverChange, + }) + + const { tooltipProps } = aria.useTooltipTrigger({}, state, targetRef) + + // eslint-disable-next-line @typescript-eslint/unbound-method + const { overlayProps, updatePosition } = aria.useOverlayPosition({ + isOpen: state.isOpen, overlayRef: popoverRef, targetRef, offset, @@ -96,27 +120,42 @@ export function useVisualTooltip(props: VisualTooltipProps) { containerPadding, }) + const createTooltipElement = () => ( + + ref?.showPopover())} + {...aria.mergeProps>()( + overlayProps, + tooltipProps, + tooltipHoverProps, + { + id, + className: ariaComponents.TOOLTIP_STYLES({ + className, + variant, + rounded, + size, + maxWidth, + }), + // eslint-disable-next-line @typescript-eslint/naming-convention + 'aria-hidden': true, + popover: '', + role: 'presentation', + 'data-testid': testId, + // Remove z-index from the overlay style + // because it's not needed(we show latest element on top) and can cause issues with stacking context + style: { zIndex: '' }, + } + )} + > + {children} + + + ) + return { - targetProps: aria.mergeProps>()(hoverProps, { id }), - tooltip: isDisabled ? null : ( - - - - ), + targetProps: aria.mergeProps>()(targetHoverProps, { id }), + tooltip: state.isOpen ? createTooltipElement() : null, } as const } diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx index 5de7033a042..79ebdb1cccf 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx @@ -23,6 +23,9 @@ export const TOOLTIP_STYLES = twv.tv({ rounded: { custom: '', full: 'rounded-full', + xxxlarge: 'rounded-3xl', + xxlarge: 'rounded-2xl', + xlarge: 'rounded-xl', large: 'rounded-lg', medium: 'rounded-md', small: 'rounded-sm', @@ -47,7 +50,7 @@ export const TOOLTIP_STYLES = twv.tv({ variant: 'primary', size: 'medium', maxWidth: 'xsmall', - rounded: 'full', + rounded: 'xxxlarge', }, }) @@ -60,7 +63,8 @@ const DEFAULT_OFFSET = 9 /** Props for a {@link Tooltip}. */ export interface TooltipProps - extends Omit, 'offset' | 'UNSTABLE_portalContainer'> {} + extends Omit, 'offset' | 'UNSTABLE_portalContainer'>, + Omit, 'isEntering' | 'isExiting'> {} /** Displays the description of an element on hover or focus. */ export function Tooltip(props: TooltipProps) { diff --git a/app/ide-desktop/lib/dashboard/src/components/aria.tsx b/app/ide-desktop/lib/dashboard/src/components/aria.tsx index a85ef010d2b..d24612f4215 100644 --- a/app/ide-desktop/lib/dashboard/src/components/aria.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/aria.tsx @@ -7,6 +7,7 @@ export * from 'react-aria' export * from 'react-aria-components' // @ts-expect-error The conflicting exports are props types ONLY. export * from '@react-stately/overlays' +export * from '@react-stately/tooltip' /** Merges multiple props objects together. * Event handlers are chained, classNames are combined, and ids are deduplicated - diff --git a/app/ide-desktop/lib/dashboard/src/tailwind.css b/app/ide-desktop/lib/dashboard/src/tailwind.css index 2ae954d86c6..ea0c360733f 100644 --- a/app/ide-desktop/lib/dashboard/src/tailwind.css +++ b/app/ide-desktop/lib/dashboard/src/tailwind.css @@ -566,7 +566,14 @@ body[data-debug] [data-navigator2d-neighbor] { @apply text-amber-500; } -:where([popover]) { +/** The default styles for the popover. */ +:where( + :is( + .enso-dashboard [popover], + .enso-chat [popover], + .enso-portal-root [popover] + ) + ) { inset: unset; margin: unset; width: unset;