Don't hide visual tooltip when mouse goes from target to tooltip (#10177)

This commit is contained in:
Sergei Garin 2024-06-05 12:05:33 +03:00 committed by GitHub
parent 8332118ff4
commit 4e92d784e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 95 additions and 55 deletions

View File

@ -126,19 +126,8 @@ export const Text = React.forwardRef(function Text(
balance, balance,
}) })
const tooltipTextClasses = TEXT_STYLE({
variant,
weight,
transform,
monospace,
italic,
balance,
className: 'pointer-events-none',
})
const { tooltip, targetProps } = visualTooltip.useVisualTooltip({ const { tooltip, targetProps } = visualTooltip.useVisualTooltip({
isDisabled: !truncate, isDisabled: !truncate,
className: tooltipTextClasses,
targetRef: textElementRef, targetRef: textElementRef,
display: 'whenOverflowing', display: 'whenOverflowing',
children, children,

View File

@ -6,16 +6,19 @@
import * as React from 'react' import * as React from 'react'
import clsx from 'clsx' import * as eventCallback from '#/hooks/eventCallbackHooks'
import * as aria from '#/components/aria' import * as aria from '#/components/aria'
import * as ariaComponents from '#/components/AriaComponents' import * as ariaComponents from '#/components/AriaComponents'
import Portal from '#/components/Portal' import Portal from '#/components/Portal'
import * as mergeRefs from '#/utilities/mergeRefs'
/** /**
* Props for {@link useVisualTooltip}. * Props for {@link useVisualTooltip}.
*/ */
export interface VisualTooltipProps { export interface VisualTooltipProps
extends Pick<ariaComponents.TooltipProps, 'maxWidth' | 'rounded' | 'size' | 'variant'> {
readonly children: React.ReactNode readonly children: React.ReactNode
readonly className?: string readonly className?: string
readonly targetRef: React.RefObject<HTMLElement> readonly targetRef: React.RefObject<HTMLElement>
@ -40,6 +43,7 @@ export interface VisualTooltipProps {
type DisplayStrategy = 'always' | 'whenOverflowing' type DisplayStrategy = 'always' | 'whenOverflowing'
const DEFAULT_OFFSET = 6 const DEFAULT_OFFSET = 6
const DEFAULT_DELAY = 250
/** /**
* Creates a tooltip that appears when the target element is hovered over. * Creates a tooltip that appears when the target element is hovered over.
@ -57,6 +61,10 @@ export function useVisualTooltip(props: VisualTooltipProps) {
overlayPositionProps = {}, overlayPositionProps = {},
display = 'always', display = 'always',
testId = 'visual-tooltip', testId = 'visual-tooltip',
rounded,
variant,
size,
maxWidth,
} = props } = props
const { const {
@ -68,27 +76,43 @@ export function useVisualTooltip(props: VisualTooltipProps) {
const popoverRef = React.useRef<HTMLDivElement>(null) const popoverRef = React.useRef<HTMLDivElement>(null)
const id = React.useId() const id = React.useId()
const { hoverProps, isHovered } = aria.useHover({ const state = aria.useTooltipTriggerState({
onHoverStart: () => { closeDelay: DEFAULT_DELAY,
if (targetRef.current) { delay: DEFAULT_DELAY,
const shouldDisplay =
typeof display === 'function'
? display(targetRef.current)
: DISPLAY_STRATEGIES[display](targetRef.current)
if (shouldDisplay) {
popoverRef.current?.showPopover()
}
}
},
onHoverEnd: () => {
popoverRef.current?.hidePopover()
},
isDisabled,
}) })
const { overlayProps } = aria.useOverlayPosition({ const handleHoverChange = eventCallback.useEventCallback((isHovered: boolean) => {
isOpen: isHovered, 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, overlayRef: popoverRef,
targetRef, targetRef,
offset, offset,
@ -96,27 +120,42 @@ export function useVisualTooltip(props: VisualTooltipProps) {
containerPadding, containerPadding,
}) })
const createTooltipElement = () => (
<Portal onMount={updatePosition}>
<span
ref={mergeRefs.mergeRefs(popoverRef, ref => ref?.showPopover())}
{...aria.mergeProps<React.HTMLAttributes<HTMLDivElement>>()(
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}
</span>
</Portal>
)
return { return {
targetProps: aria.mergeProps<React.HTMLAttributes<HTMLElement>>()(hoverProps, { id }), targetProps: aria.mergeProps<React.HTMLAttributes<HTMLElement>>()(targetHoverProps, { id }),
tooltip: isDisabled ? null : ( tooltip: state.isOpen ? createTooltipElement() : null,
<Portal>
<span
id={id}
ref={popoverRef}
className={ariaComponents.TOOLTIP_STYLES({
className: clsx(className, 'hidden animate-in fade-in [&:popover-open]:flex'),
})}
// @ts-expect-error popover attribute does not exist on React.HTMLAttributes yet
popover=""
aria-hidden="true"
role="presentation"
data-testid={testId}
{...overlayProps}
>
{children}
</span>
</Portal>
),
} as const } as const
} }

View File

@ -23,6 +23,9 @@ export const TOOLTIP_STYLES = twv.tv({
rounded: { rounded: {
custom: '', custom: '',
full: 'rounded-full', full: 'rounded-full',
xxxlarge: 'rounded-3xl',
xxlarge: 'rounded-2xl',
xlarge: 'rounded-xl',
large: 'rounded-lg', large: 'rounded-lg',
medium: 'rounded-md', medium: 'rounded-md',
small: 'rounded-sm', small: 'rounded-sm',
@ -47,7 +50,7 @@ export const TOOLTIP_STYLES = twv.tv({
variant: 'primary', variant: 'primary',
size: 'medium', size: 'medium',
maxWidth: 'xsmall', maxWidth: 'xsmall',
rounded: 'full', rounded: 'xxxlarge',
}, },
}) })
@ -60,7 +63,8 @@ const DEFAULT_OFFSET = 9
/** Props for a {@link Tooltip}. */ /** Props for a {@link Tooltip}. */
export interface TooltipProps export interface TooltipProps
extends Omit<Readonly<aria.TooltipProps>, 'offset' | 'UNSTABLE_portalContainer'> {} extends Omit<Readonly<aria.TooltipProps>, 'offset' | 'UNSTABLE_portalContainer'>,
Omit<twv.VariantProps<typeof TOOLTIP_STYLES>, 'isEntering' | 'isExiting'> {}
/** Displays the description of an element on hover or focus. */ /** Displays the description of an element on hover or focus. */
export function Tooltip(props: TooltipProps) { export function Tooltip(props: TooltipProps) {

View File

@ -7,6 +7,7 @@ export * from 'react-aria'
export * from 'react-aria-components' export * from 'react-aria-components'
// @ts-expect-error The conflicting exports are props types ONLY. // @ts-expect-error The conflicting exports are props types ONLY.
export * from '@react-stately/overlays' export * from '@react-stately/overlays'
export * from '@react-stately/tooltip'
/** Merges multiple props objects together. /** Merges multiple props objects together.
* Event handlers are chained, classNames are combined, and ids are deduplicated - * Event handlers are chained, classNames are combined, and ids are deduplicated -

View File

@ -566,7 +566,14 @@ body[data-debug] [data-navigator2d-neighbor] {
@apply text-amber-500; @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; inset: unset;
margin: unset; margin: unset;
width: unset; width: unset;