Show tooltip immediately if a tooltip is already open (#28870)

* Memoize delayProps
* Refactor Escape key handler
* Use useTooltipContext
* Remove delay: 0 from tooltips
* Only use isTooltipOpen if available
* Only show transition for initial tooltip

GitOrigin-RevId: 74950ea7e705acb8f42dea552b23ce93c66058c7
This commit is contained in:
Alf Eaton
2025-10-08 10:13:20 +01:00
committed by Copybot
parent 3a8d383ac3
commit d3f05fda77
12 changed files with 106 additions and 29 deletions

View File

@@ -74,7 +74,6 @@ export default function DictionaryModalContent({
<OLTooltip
id={`tooltip-remove-learned-word-${learnedWord}`}
description={t('edit_dictionary_remove')}
overlayProps={{ delay: 0 }}
>
<OLIconButton
variant="danger"

View File

@@ -30,6 +30,7 @@ import { UserFeaturesProvider } from '@/shared/context/user-features-context'
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
import { IdeRedesignSwitcherProvider } from './ide-redesign-switcher-context'
import { CommandRegistryProvider } from './command-registry-context'
import { TooltipProvider } from '@/shared/context/tooltip-provider'
export const ReactContextRoot: FC<
React.PropsWithChildren<{
@@ -68,6 +69,7 @@ export const ReactContextRoot: FC<
IdeRedesignSwitcherProvider,
CommandRegistryProvider,
UserFeaturesProvider,
TooltipProvider,
...providers,
}
@@ -103,7 +105,9 @@ export const ReactContextRoot: FC<
<Providers.OutlineProvider>
<Providers.IdeRedesignSwitcherProvider>
<Providers.CommandRegistryProvider>
{children}
<Providers.TooltipProvider>
{children}
</Providers.TooltipProvider>
</Providers.CommandRegistryProvider>
</Providers.IdeRedesignSwitcherProvider>
</Providers.OutlineProvider>

View File

@@ -78,7 +78,6 @@ const OnlineUserWidget = ({
overlayProps={{
placement: 'bottom',
trigger: ['hover', 'focus'],
delay: 0,
}}
>
<button className="online-users-row-button" onClick={onClick}>

View File

@@ -43,7 +43,7 @@ export default function RailActionElement({ action }: { action: RailAction }) {
<OLTooltip
id={`rail-dropdown-tooltip-${action.key}`}
description={action.title}
overlayProps={{ delay: 0, placement: 'right' }}
overlayProps={{ placement: 'right' }}
>
<span>
<DropdownToggle
@@ -68,7 +68,7 @@ export default function RailActionElement({ action }: { action: RailAction }) {
<OLTooltip
id={`rail-tab-tooltip-${action.key}`}
description={action.title}
overlayProps={{ delay: 0, placement: 'right' }}
overlayProps={{ placement: 'right' }}
>
<button
onClick={onActionClick}

View File

@@ -21,7 +21,7 @@ const RailTab = forwardRef<
<OLTooltip
id={`rail-tab-tooltip-${eventKey}`}
description={title}
overlayProps={{ delay: 0, placement: 'right' }}
overlayProps={{ placement: 'right' }}
>
<NavLink
ref={ref}

View File

@@ -23,7 +23,7 @@ export default function ChangeLayoutButton() {
<OLTooltip
id="tooltip-open-layout-options"
description={t('layout_options')}
overlayProps={{ delay: 0, placement: 'bottom' }}
overlayProps={{ placement: 'bottom' }}
>
<span>
<DropdownToggle

View File

@@ -15,7 +15,7 @@ export const ToolbarLogos = ({ cobranding }: ToolbarLogosProps) => {
<OLTooltip
id="tooltip-home-button"
description={t('back_to_your_projects')}
overlayProps={{ delay: 0, placement: 'bottom' }}
overlayProps={{ placement: 'bottom' }}
>
<div className="ide-redesign-toolbar-home-button">
<a href="/project" className="ide-redesign-toolbar-home-link">

View File

@@ -25,7 +25,7 @@ export default function ShowHistoryButton() {
<OLTooltip
id="tooltip-open-history"
description={t('history')}
overlayProps={{ delay: 0, placement: 'bottom' }}
overlayProps={{ placement: 'bottom' }}
>
<OLIconButton
icon="history"

View File

@@ -40,7 +40,7 @@ export const ColumnSizeIndicator = ({
width: formattedWidth,
})
}
overlayProps={{ delay: 0, placement: 'bottom' }}
overlayProps={{ placement: 'bottom' }}
>
<button
className="btn table-generator-column-indicator-button"

View File

@@ -186,7 +186,7 @@ const ColumnWidthModalBody = () => {
<OLTooltip
id="table-generator-unit-tooltip"
description={unitHelp.tooltip}
overlayProps={{ delay: 0, placement: 'top' }}
overlayProps={{ placement: 'top' }}
>
<span>
<MaterialIcon type="help" className="align-middle" />

View File

@@ -3,6 +3,7 @@ import {
useEffect,
forwardRef,
useState,
useMemo,
useCallback,
} from 'react'
import {
@@ -12,6 +13,7 @@ import {
TooltipProps as BSTooltipProps,
} from 'react-bootstrap'
import { callFnsInSequence } from '@/utils/functions'
import { useTooltipContext } from '@/shared/context/tooltip-provider'
const DEFAULT_DELAY_SHOW = 300
// Slightly lower value avoids flickering when an adjacent tooltip is shown before the previous one hides
@@ -34,6 +36,26 @@ const UpdatingTooltip = forwardRef<HTMLDivElement, BSTooltipProps>(
)
UpdatingTooltip.displayName = 'UpdatingTooltip'
const chooseDelayOptions = (
delay?: number | { show: number; hide: number }
): { show: number; hide: number } => {
if (typeof delay === 'object') {
return delay
}
if (typeof delay === 'number') {
return {
show: delay,
hide: Math.max(delay - 10, 0),
}
}
return {
show: DEFAULT_DELAY_SHOW,
hide: DEFAULT_DELAY_HIDE,
}
}
export type TooltipProps = {
description: React.ReactNode
id: string
@@ -53,20 +75,22 @@ function Tooltip({
}: TooltipProps) {
const [show, setShow] = useState(false)
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
const tooltipContext = useTooltipContext()
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (show && e.key === 'Escape') {
setShow(false)
e.stopPropagation()
}
},
[show, setShow]
)
}
useEffect(() => {
document.addEventListener('keydown', handleKeyDown, true)
return () => document.removeEventListener('keydown', handleKeyDown, true)
}, [handleKeyDown])
document.addEventListener('keydown', listener, true)
return () => {
document.removeEventListener('keydown', listener, true)
}
}, [show])
const hideTooltip = (e: React.MouseEvent) => {
if (e.currentTarget instanceof HTMLElement) {
@@ -75,13 +99,22 @@ function Tooltip({
setShow(false)
}
const delay = overlayProps?.delay
let delayShow = DEFAULT_DELAY_SHOW
let delayHide = DEFAULT_DELAY_HIDE
if (delay !== undefined) {
delayShow = typeof delay === 'number' ? delay : delay.show
delayHide = typeof delay === 'number' ? Math.max(delay - 10, 0) : delay.hide
}
const delayProps = useMemo(() => {
const delayOptions = chooseDelayOptions(overlayProps?.delay)
if (tooltipContext?.isTooltipOpen) {
delayOptions.show = 0
delayOptions.hide = 0
}
return delayOptions
}, [overlayProps?.delay, tooltipContext])
const handleToggle = useCallback(
(value: boolean) => {
tooltipContext?.setIsTooltipOpen(value)
setShow(value)
},
[tooltipContext]
)
return (
<OverlayTrigger
@@ -95,10 +128,11 @@ function Tooltip({
</UpdatingTooltip>
}
{...overlayProps}
delay={{ show: delayShow, hide: delayHide }}
delay={delayProps}
placement={overlayProps?.placement || 'top'}
show={show}
onToggle={setShow}
onToggle={handleToggle}
transition={!tooltipContext?.isTooltipOpen}
>
{overlayProps?.trigger === 'click'
? children

View File

@@ -0,0 +1,41 @@
import {
createContext,
Dispatch,
FC,
PropsWithChildren,
SetStateAction,
useContext,
useMemo,
useState,
} from 'react'
import useDebounce from '@/shared/hooks/use-debounce'
const TooltipContext = createContext<
| {
isTooltipOpen: boolean
setIsTooltipOpen: Dispatch<SetStateAction<boolean>>
}
| undefined
>(undefined)
export const TooltipProvider: FC<PropsWithChildren> = ({ children }) => {
const [isTooltipOpen, setIsTooltipOpen] = useState(false)
const debouncedIsTooltipOpen = useDebounce(isTooltipOpen, 100)
const value = useMemo(
() => ({
isTooltipOpen: debouncedIsTooltipOpen,
setIsTooltipOpen,
}),
[debouncedIsTooltipOpen, setIsTooltipOpen]
)
return (
<TooltipContext.Provider value={value}>{children}</TooltipContext.Provider>
)
}
export const useTooltipContext = () => {
return useContext(TooltipContext)
}