Просмотр исходного кода

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
Alf Eaton 10 месяцев назад
Родитель
Сommit
d3f05fda77

+ 0 - 1
services/web/frontend/js/features/dictionary/components/dictionary-modal-content.tsx

@@ -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"

+ 5 - 1
services/web/frontend/js/features/ide-react/context/react-context-root.tsx

@@ -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>

+ 0 - 1
services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx

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

+ 2 - 2
services/web/frontend/js/features/ide-redesign/components/rail/rail-action-element.tsx

@@ -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}

+ 1 - 1
services/web/frontend/js/features/ide-redesign/components/rail/rail-tab.tsx

@@ -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}

+ 1 - 1
services/web/frontend/js/features/ide-redesign/components/toolbar/change-layout-button.tsx

@@ -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

+ 1 - 1
services/web/frontend/js/features/ide-redesign/components/toolbar/logos.tsx

@@ -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">

+ 1 - 1
services/web/frontend/js/features/ide-redesign/components/toolbar/show-history-button.tsx

@@ -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"

+ 1 - 1
services/web/frontend/js/features/source-editor/components/table-generator/column-size-indicator.tsx

@@ -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"

+ 1 - 1
services/web/frontend/js/features/source-editor/components/table-generator/toolbar/column-width-modal/modal.tsx

@@ -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" />

+ 52 - 18
services/web/frontend/js/shared/components/tooltip.tsx

@@ -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

+ 41 - 0
services/web/frontend/js/shared/context/tooltip-provider.tsx

@@ -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)
+}